diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 69d849f..4b521a4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
lint:
timeout-minutes: 10
name: lint
- runs-on: ${{ github.repository == 'stainless-sdks/keycard-api-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -41,7 +41,7 @@ jobs:
permissions:
contents: read
id-token: write
- runs-on: ${{ github.repository == 'stainless-sdks/keycard-api-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -78,7 +78,7 @@ jobs:
test:
timeout-minutes: 10
name: test
- runs-on: ${{ github.repository == 'stainless-sdks/keycard-api-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index b4e9013..6db19b9 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.16.0"
+ ".": "0.17.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index ffd5937..b32e061 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 106
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/keycard/keycard-api-cdf14bee924cea2eaca3c1aaed26521cb91369d9462d22c21a0306b5786f4706.yml
-openapi_spec_hash: d8349acc1adec880977b6167a1e866bc
-config_hash: 7adc6b24545570dcc4a1bf0f714aa3e0
+configured_endpoints: 109
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/keycard/keycard-api-3c029b6d95f9ba550b5a983b3b021fa404cc2bd73d11495c0ae82065b4ba95ad.yml
+openapi_spec_hash: 49c3651c217697ea8899b58c8f2bb046
+config_hash: 22d144a33ba2901c82cc1f387a83f9a7
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cefd0e4..f749575 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,33 @@
# Changelog
+## 0.17.0 (2026-07-24)
+
+Full Changelog: [v0.16.0...v0.17.0](https://github.com/keycardai/keycard-python/compare/v0.16.0...v0.17.0)
+
+### Features
+
+* **ACC-709:** application assignees + expose role assignments ([6a76dbf](https://github.com/keycardai/keycard-python/commit/6a76dbf04cbd9d16c1054a262a7a42a01668f035))
+* **applications:** allow unified-gateway and mcp-server traits (ECO-128) ([414ac68](https://github.com/keycardai/keycard-python/commit/414ac68a77bb5c54837b7d4146c2e8b8735f6370))
+* filter users by identifier in management list ([147af02](https://github.com/keycardai/keycard-python/commit/147af020e8e8c31678928137fb85386b54dd4ec6))
+* **ID-365:** support additional SSO provider configuration options ([526b229](https://github.com/keycardai/keycard-python/commit/526b22958acc91a11569ba3f8453a004eb03f6f5))
+* **sdk:** generate a client for the existing /policy/bundle endpoint ([518c473](https://github.com/keycardai/keycard-python/commit/518c473dcb23f0d68e849838a070187addf422a9))
+* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([1c22f7e](https://github.com/keycardai/keycard-python/commit/1c22f7ea4ae904c4864a794dc4aaa46afa7d7e0e))
+
+
+### Bug Fixes
+
+* **ACC-613:** preserve source order of policies in draft/convert cedar_json ([6d27d21](https://github.com/keycardai/keycard-python/commit/6d27d213ac66fb9f1890409a380924c3edb44b99))
+* **ci:** resolve Stainless error diagnostics and enforce fail_on: error ([8a01934](https://github.com/keycardai/keycard-python/commit/8a01934b6ceb8636db998f0498d86ccb810cf639))
+* exact-match identifier filter on resources management list ([846f24e](https://github.com/keycardai/keycard-python/commit/846f24efdb2492e4ea0741167ebc7153af5d0d1e))
+* **internal:** resolve build failures ([15cb317](https://github.com/keycardai/keycard-python/commit/15cb317c1d1ea8462b30bff1411e52a554df0fb0))
+* **sdk:** generate /policy/bundle body as raw binary, not multipart ([d516a7f](https://github.com/keycardai/keycard-python/commit/d516a7f7b207aa4eed33d1a401721b9aa1ecd5d9))
+
+
+### Chores
+
+* de-dup and align types across API specs ([bf16103](https://github.com/keycardai/keycard-python/commit/bf161039858f079eabd6abf9102fa439633b9bc9))
+* Fixes found during Terraform work ([8389f30](https://github.com/keycardai/keycard-python/commit/8389f304ffae7c4bbe512de541a6896469be273f))
+
## 0.16.0 (2026-06-30)
Full Changelog: [v0.15.0...v0.16.0](https://github.com/keycardai/keycard-python/compare/v0.15.0...v0.16.0)
diff --git a/README.md b/README.md
index cf222f1..e364722 100644
--- a/README.md
+++ b/README.md
@@ -25,23 +25,34 @@ pip install keycardai_api
The full API of this library can be found in [api.md](api.md).
```python
+import os
from keycardai_api import KeycardAPI
-client = KeycardAPI()
+client = KeycardAPI(
+ api_key=os.environ.get("KEYCARD_API_API_KEY"), # This is the default and can be omitted
+)
zones = client.zones.list()
print(zones.items)
```
+While you can provide an `api_key` keyword argument,
+we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/)
+to add `KEYCARD_API_API_KEY="My API Key"` to your `.env` file
+so that your API Key is not stored in source control.
+
## Async usage
Simply import `AsyncKeycardAPI` instead of `KeycardAPI` and use `await` with each API call:
```python
+import os
import asyncio
from keycardai_api import AsyncKeycardAPI
-client = AsyncKeycardAPI()
+client = AsyncKeycardAPI(
+ api_key=os.environ.get("KEYCARD_API_API_KEY"), # This is the default and can be omitted
+)
async def main() -> None:
@@ -68,6 +79,7 @@ pip install keycardai_api[aiohttp]
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
```python
+import os
import asyncio
from keycardai_api import DefaultAioHttpClient
from keycardai_api import AsyncKeycardAPI
@@ -75,6 +87,7 @@ from keycardai_api import AsyncKeycardAPI
async def main() -> None:
async with AsyncKeycardAPI(
+ api_key=os.environ.get("KEYCARD_API_API_KEY"), # This is the default and can be omitted
http_client=DefaultAioHttpClient(),
) as client:
zones = await client.zones.list()
diff --git a/api.md b/api.md
index 48d50a6..81bbd7d 100644
--- a/api.md
+++ b/api.md
@@ -261,46 +261,6 @@ Methods:
- client.zones.policies.versions.list(policy_id, \*, zone_id, \*\*params) -> VersionListResponse
- client.zones.policies.versions.archive(version_id, \*, zone_id, policy_id) -> PolicyVersion
-## Packages
-
-Types:
-
-```python
-from keycardai_api.types.zones import (
- InputState,
- Package,
- PackageDraft,
- PackageInputBinding,
- PackageList,
- PackageOutputBinding,
- PackageSource,
-)
-```
-
-### Versions
-
-Types:
-
-```python
-from keycardai_api.types.zones.packages import PackageVersion, PackageVersionList
-```
-
-## Installs
-
-Types:
-
-```python
-from keycardai_api.types.zones import Install, InstallList, InstallStatus
-```
-
-## CatalogTasks
-
-Types:
-
-```python
-from keycardai_api.types.zones import Task, TaskOperation, TaskStatus
-```
-
## PolicySets
Types:
@@ -470,3 +430,11 @@ Methods:
- client.invitations.retrieve(token) -> InvitationRetrieveResponse
- client.invitations.accept(token) -> InvitationAcceptResponse
+
+# PolicyBundle
+
+Methods:
+
+- client.policy_bundle.retrieve() -> BinaryAPIResponse
+- client.policy_bundle.update(body, \*\*params) -> BinaryAPIResponse
+- client.policy_bundle.reset() -> None
diff --git a/pyproject.toml b/pyproject.toml
index 0f72996..a2de0a3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "keycardai_api"
-version = "0.16.0"
+version = "0.17.0"
description = "The official Python library for the keycard-api API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/scripts/lint b/scripts/lint
index 97303ed..7b9389c 100755
--- a/scripts/lint
+++ b/scripts/lint
@@ -13,7 +13,7 @@ else
fi
echo "==> Running pyright"
-uv run pyright
+uv run pyright -p .
echo "==> Running mypy"
uv run mypy .
diff --git a/src/keycardai_api/_client.py b/src/keycardai_api/_client.py
index 103db47..d76b6ee 100644
--- a/src/keycardai_api/_client.py
+++ b/src/keycardai_api/_client.py
@@ -37,9 +37,10 @@
)
if TYPE_CHECKING:
- from .resources import zones, invitations, organizations
+ from .resources import zones, invitations, organizations, policy_bundle
from .resources.invitations import InvitationsResource, AsyncInvitationsResource
from .resources.zones.zones import ZonesResource, AsyncZonesResource
+ from .resources.policy_bundle import PolicyBundleResource, AsyncPolicyBundleResource
from .resources.organizations.organizations import OrganizationsResource, AsyncOrganizationsResource
__all__ = [
@@ -147,6 +148,35 @@ def invitations(self) -> InvitationsResource:
return InvitationsResource(self)
+ @cached_property
+ def policy_bundle(self) -> PolicyBundleResource:
+ """Per-user Policy Bundle resource.
+
+ Allows clients (typically the Keycard CLI)
+ to GET, PUT, and DELETE the effective Policy Set for the calling user
+ on a zone. The bundle is encoded with a content-negotiated codec (currently
+ only `application/vnd.keycard.policy-bundle.v1+tar+gzip`).
+
+ ## Archive layout
+
+ The bundle is a gzip-compressed tar archive with this logical layout:
+
+ | Entry | Required on PUT | Notes |
+ |-------|-----------------|-------|
+ | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. |
+ | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** |
+ | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. |
+
+ Decode rules: duplicate entries and unrecognized/nested entries are
+ rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and
+ `policies[]` list are advisory — the server recomputes every digest from
+ the archived bytes and derives the policy set from the `policies/` files.
+ On GET every digest is authoritative.
+ """
+ from .resources.policy_bundle import PolicyBundleResource
+
+ return PolicyBundleResource(self)
+
@cached_property
def with_raw_response(self) -> KeycardAPIWithRawResponse:
return KeycardAPIWithRawResponse(self)
@@ -160,12 +190,27 @@ def with_streaming_response(self) -> KeycardAPIWithStreamedResponse:
def qs(self) -> Querystring:
return Querystring(array_format="repeat")
+ @override
+ def _auth_headers(self, security: SecurityOptions) -> dict[str, str]:
+ headers: dict[str, str] = {}
+ if security.get("bearer_auth", False):
+ for key, value in self._bearer_auth.items():
+ headers.setdefault(key, value)
+ return headers
+
@override
def _custom_auth(self, security: SecurityOptions) -> httpx.Auth | None:
if security.get("o_auth2", False) and self._o_auth2 is not None:
return self._o_auth2
return None
+ @property
+ def _bearer_auth(self) -> dict[str, str]:
+ api_key = self.api_key
+ if api_key is None:
+ return {}
+ return {"Authorization": f"Bearer {api_key}"}
+
@property
def _o_auth2(self) -> httpx.Auth | None:
if self.client_id and self.client_secret:
@@ -377,6 +422,35 @@ def invitations(self) -> AsyncInvitationsResource:
return AsyncInvitationsResource(self)
+ @cached_property
+ def policy_bundle(self) -> AsyncPolicyBundleResource:
+ """Per-user Policy Bundle resource.
+
+ Allows clients (typically the Keycard CLI)
+ to GET, PUT, and DELETE the effective Policy Set for the calling user
+ on a zone. The bundle is encoded with a content-negotiated codec (currently
+ only `application/vnd.keycard.policy-bundle.v1+tar+gzip`).
+
+ ## Archive layout
+
+ The bundle is a gzip-compressed tar archive with this logical layout:
+
+ | Entry | Required on PUT | Notes |
+ |-------|-----------------|-------|
+ | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. |
+ | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** |
+ | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. |
+
+ Decode rules: duplicate entries and unrecognized/nested entries are
+ rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and
+ `policies[]` list are advisory — the server recomputes every digest from
+ the archived bytes and derives the policy set from the `policies/` files.
+ On GET every digest is authoritative.
+ """
+ from .resources.policy_bundle import AsyncPolicyBundleResource
+
+ return AsyncPolicyBundleResource(self)
+
@cached_property
def with_raw_response(self) -> AsyncKeycardAPIWithRawResponse:
return AsyncKeycardAPIWithRawResponse(self)
@@ -390,12 +464,27 @@ def with_streaming_response(self) -> AsyncKeycardAPIWithStreamedResponse:
def qs(self) -> Querystring:
return Querystring(array_format="repeat")
+ @override
+ def _auth_headers(self, security: SecurityOptions) -> dict[str, str]:
+ headers: dict[str, str] = {}
+ if security.get("bearer_auth", False):
+ for key, value in self._bearer_auth.items():
+ headers.setdefault(key, value)
+ return headers
+
@override
def _custom_auth(self, security: SecurityOptions) -> httpx.Auth | None:
if security.get("o_auth2", False) and self._o_auth2 is not None:
return self._o_auth2
return None
+ @property
+ def _bearer_auth(self) -> dict[str, str]:
+ api_key = self.api_key
+ if api_key is None:
+ return {}
+ return {"Authorization": f"Bearer {api_key}"}
+
@property
def _o_auth2(self) -> httpx.Auth | None:
if self.client_id and self.client_secret:
@@ -538,6 +627,35 @@ def invitations(self) -> invitations.InvitationsResourceWithRawResponse:
return InvitationsResourceWithRawResponse(self._client.invitations)
+ @cached_property
+ def policy_bundle(self) -> policy_bundle.PolicyBundleResourceWithRawResponse:
+ """Per-user Policy Bundle resource.
+
+ Allows clients (typically the Keycard CLI)
+ to GET, PUT, and DELETE the effective Policy Set for the calling user
+ on a zone. The bundle is encoded with a content-negotiated codec (currently
+ only `application/vnd.keycard.policy-bundle.v1+tar+gzip`).
+
+ ## Archive layout
+
+ The bundle is a gzip-compressed tar archive with this logical layout:
+
+ | Entry | Required on PUT | Notes |
+ |-------|-----------------|-------|
+ | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. |
+ | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** |
+ | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. |
+
+ Decode rules: duplicate entries and unrecognized/nested entries are
+ rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and
+ `policies[]` list are advisory — the server recomputes every digest from
+ the archived bytes and derives the policy set from the `policies/` files.
+ On GET every digest is authoritative.
+ """
+ from .resources.policy_bundle import PolicyBundleResourceWithRawResponse
+
+ return PolicyBundleResourceWithRawResponse(self._client.policy_bundle)
+
class AsyncKeycardAPIWithRawResponse:
_client: AsyncKeycardAPI
@@ -563,6 +681,35 @@ def invitations(self) -> invitations.AsyncInvitationsResourceWithRawResponse:
return AsyncInvitationsResourceWithRawResponse(self._client.invitations)
+ @cached_property
+ def policy_bundle(self) -> policy_bundle.AsyncPolicyBundleResourceWithRawResponse:
+ """Per-user Policy Bundle resource.
+
+ Allows clients (typically the Keycard CLI)
+ to GET, PUT, and DELETE the effective Policy Set for the calling user
+ on a zone. The bundle is encoded with a content-negotiated codec (currently
+ only `application/vnd.keycard.policy-bundle.v1+tar+gzip`).
+
+ ## Archive layout
+
+ The bundle is a gzip-compressed tar archive with this logical layout:
+
+ | Entry | Required on PUT | Notes |
+ |-------|-----------------|-------|
+ | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. |
+ | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** |
+ | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. |
+
+ Decode rules: duplicate entries and unrecognized/nested entries are
+ rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and
+ `policies[]` list are advisory — the server recomputes every digest from
+ the archived bytes and derives the policy set from the `policies/` files.
+ On GET every digest is authoritative.
+ """
+ from .resources.policy_bundle import AsyncPolicyBundleResourceWithRawResponse
+
+ return AsyncPolicyBundleResourceWithRawResponse(self._client.policy_bundle)
+
class KeycardAPIWithStreamedResponse:
_client: KeycardAPI
@@ -588,6 +735,35 @@ def invitations(self) -> invitations.InvitationsResourceWithStreamingResponse:
return InvitationsResourceWithStreamingResponse(self._client.invitations)
+ @cached_property
+ def policy_bundle(self) -> policy_bundle.PolicyBundleResourceWithStreamingResponse:
+ """Per-user Policy Bundle resource.
+
+ Allows clients (typically the Keycard CLI)
+ to GET, PUT, and DELETE the effective Policy Set for the calling user
+ on a zone. The bundle is encoded with a content-negotiated codec (currently
+ only `application/vnd.keycard.policy-bundle.v1+tar+gzip`).
+
+ ## Archive layout
+
+ The bundle is a gzip-compressed tar archive with this logical layout:
+
+ | Entry | Required on PUT | Notes |
+ |-------|-----------------|-------|
+ | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. |
+ | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** |
+ | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. |
+
+ Decode rules: duplicate entries and unrecognized/nested entries are
+ rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and
+ `policies[]` list are advisory — the server recomputes every digest from
+ the archived bytes and derives the policy set from the `policies/` files.
+ On GET every digest is authoritative.
+ """
+ from .resources.policy_bundle import PolicyBundleResourceWithStreamingResponse
+
+ return PolicyBundleResourceWithStreamingResponse(self._client.policy_bundle)
+
class AsyncKeycardAPIWithStreamedResponse:
_client: AsyncKeycardAPI
@@ -613,6 +789,35 @@ def invitations(self) -> invitations.AsyncInvitationsResourceWithStreamingRespon
return AsyncInvitationsResourceWithStreamingResponse(self._client.invitations)
+ @cached_property
+ def policy_bundle(self) -> policy_bundle.AsyncPolicyBundleResourceWithStreamingResponse:
+ """Per-user Policy Bundle resource.
+
+ Allows clients (typically the Keycard CLI)
+ to GET, PUT, and DELETE the effective Policy Set for the calling user
+ on a zone. The bundle is encoded with a content-negotiated codec (currently
+ only `application/vnd.keycard.policy-bundle.v1+tar+gzip`).
+
+ ## Archive layout
+
+ The bundle is a gzip-compressed tar archive with this logical layout:
+
+ | Entry | Required on PUT | Notes |
+ |-------|-----------------|-------|
+ | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. |
+ | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** |
+ | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. |
+
+ Decode rules: duplicate entries and unrecognized/nested entries are
+ rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and
+ `policies[]` list are advisory — the server recomputes every digest from
+ the archived bytes and derives the policy set from the `policies/` files.
+ On GET every digest is authoritative.
+ """
+ from .resources.policy_bundle import AsyncPolicyBundleResourceWithStreamingResponse
+
+ return AsyncPolicyBundleResourceWithStreamingResponse(self._client.policy_bundle)
+
Client = KeycardAPI
diff --git a/src/keycardai_api/_models.py b/src/keycardai_api/_models.py
index 13d0794..90bf844 100644
--- a/src/keycardai_api/_models.py
+++ b/src/keycardai_api/_models.py
@@ -872,6 +872,7 @@ def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]:
class SecurityOptions(TypedDict, total=False):
+ bearer_auth: bool
o_auth2: bool
@@ -903,7 +904,10 @@ class FinalRequestOptions(pydantic.BaseModel):
idempotency_key: Union[str, None] = None
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
follow_redirects: Union[bool, None] = None
- security: SecurityOptions = {"o_auth2": True}
+ security: SecurityOptions = {
+ "bearer_auth": True,
+ "o_auth2": True,
+ }
content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None
# It should be noted that we cannot use `json` here as that would override
diff --git a/src/keycardai_api/_version.py b/src/keycardai_api/_version.py
index ff81ceb..cb30031 100644
--- a/src/keycardai_api/_version.py
+++ b/src/keycardai_api/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "keycardai_api"
-__version__ = "0.16.0" # x-release-please-version
+__version__ = "0.17.0" # x-release-please-version
diff --git a/src/keycardai_api/resources/__init__.py b/src/keycardai_api/resources/__init__.py
index d7c6a99..3e222d8 100644
--- a/src/keycardai_api/resources/__init__.py
+++ b/src/keycardai_api/resources/__init__.py
@@ -24,6 +24,14 @@
OrganizationsResourceWithStreamingResponse,
AsyncOrganizationsResourceWithStreamingResponse,
)
+from .policy_bundle import (
+ PolicyBundleResource,
+ AsyncPolicyBundleResource,
+ PolicyBundleResourceWithRawResponse,
+ AsyncPolicyBundleResourceWithRawResponse,
+ PolicyBundleResourceWithStreamingResponse,
+ AsyncPolicyBundleResourceWithStreamingResponse,
+)
__all__ = [
"ZonesResource",
@@ -44,4 +52,10 @@
"AsyncInvitationsResourceWithRawResponse",
"InvitationsResourceWithStreamingResponse",
"AsyncInvitationsResourceWithStreamingResponse",
+ "PolicyBundleResource",
+ "AsyncPolicyBundleResource",
+ "PolicyBundleResourceWithRawResponse",
+ "AsyncPolicyBundleResourceWithRawResponse",
+ "PolicyBundleResourceWithStreamingResponse",
+ "AsyncPolicyBundleResourceWithStreamingResponse",
]
diff --git a/src/keycardai_api/resources/organizations/sso_connection.py b/src/keycardai_api/resources/organizations/sso_connection.py
index 4a8a44f..002dbed 100644
--- a/src/keycardai_api/resources/organizations/sso_connection.py
+++ b/src/keycardai_api/resources/organizations/sso_connection.py
@@ -104,7 +104,7 @@ def update(
client_id: str | Omit = omit,
client_secret: str | Omit = omit,
identifier: str | Omit = omit,
- protocols: Optional[SSOConnectionProtocolParam] | Omit = omit,
+ protocols: Optional[sso_connection_update_params.Protocols] | Omit = omit,
x_client_request_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -125,7 +125,8 @@ def update(
identifier: SSO provider identifier (e.g., issuer URL)
- protocols: Protocol configuration for SSO connection
+ protocols: Protocol configuration for an SSO connection update. Omit a protocol to leave it
+ unchanged.
extra_headers: Send extra headers
@@ -329,7 +330,7 @@ async def update(
client_id: str | Omit = omit,
client_secret: str | Omit = omit,
identifier: str | Omit = omit,
- protocols: Optional[SSOConnectionProtocolParam] | Omit = omit,
+ protocols: Optional[sso_connection_update_params.Protocols] | Omit = omit,
x_client_request_id: str | Omit = omit,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
@@ -350,7 +351,8 @@ async def update(
identifier: SSO provider identifier (e.g., issuer URL)
- protocols: Protocol configuration for SSO connection
+ protocols: Protocol configuration for an SSO connection update. Omit a protocol to leave it
+ unchanged.
extra_headers: Send extra headers
diff --git a/src/keycardai_api/resources/policy_bundle.py b/src/keycardai_api/resources/policy_bundle.py
new file mode 100644
index 0000000..e68008f
--- /dev/null
+++ b/src/keycardai_api/resources/policy_bundle.py
@@ -0,0 +1,542 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+
+import httpx
+
+from .._files import read_file_content, async_read_file_content
+from .._types import (
+ Body,
+ Omit,
+ Query,
+ Headers,
+ NoneType,
+ NotGiven,
+ BinaryTypes,
+ FileContent,
+ AsyncBinaryTypes,
+ omit,
+ not_given,
+)
+from .._utils import strip_not_given
+from .._compat import cached_property
+from .._resource import SyncAPIResource, AsyncAPIResource
+from .._response import (
+ BinaryAPIResponse,
+ AsyncBinaryAPIResponse,
+ StreamedBinaryAPIResponse,
+ AsyncStreamedBinaryAPIResponse,
+ to_raw_response_wrapper,
+ to_streamed_response_wrapper,
+ async_to_raw_response_wrapper,
+ to_custom_raw_response_wrapper,
+ async_to_streamed_response_wrapper,
+ to_custom_streamed_response_wrapper,
+ async_to_custom_raw_response_wrapper,
+ async_to_custom_streamed_response_wrapper,
+)
+from .._base_client import make_request_options
+
+__all__ = ["PolicyBundleResource", "AsyncPolicyBundleResource"]
+
+
+class PolicyBundleResource(SyncAPIResource):
+ """Per-user Policy Bundle resource.
+
+ Allows clients (typically the Keycard CLI)
+ to GET, PUT, and DELETE the effective Policy Set for the calling user
+ on a zone. The bundle is encoded with a content-negotiated codec (currently
+ only `application/vnd.keycard.policy-bundle.v1+tar+gzip`).
+
+ ## Archive layout
+
+ The bundle is a gzip-compressed tar archive with this logical layout:
+
+ | Entry | Required on PUT | Notes |
+ |-------|-----------------|-------|
+ | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. |
+ | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** |
+ | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. |
+
+ Decode rules: duplicate entries and unrecognized/nested entries are
+ rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and
+ `policies[]` list are advisory — the server recomputes every digest from
+ the archived bytes and derives the policy set from the `policies/` files.
+ On GET every digest is authoritative.
+ """
+
+ @cached_property
+ def with_raw_response(self) -> PolicyBundleResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers
+ """
+ return PolicyBundleResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> PolicyBundleResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response
+ """
+ return PolicyBundleResourceWithStreamingResponse(self)
+
+ def retrieve(
+ self,
+ *,
+ if_none_match: str | Omit = omit,
+ x_client_request_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BinaryAPIResponse:
+ """
+ Returns the effective Policy Bundle for the user identified by the zone-issued
+ resource-scoped token. When no user-scope binding exists, one will be generated
+ from the default set.
+
+ The response body is a binary archive in the codec selected via the `Accept`
+ header. The only codec supported today is
+ `application/vnd.keycard.policy-bundle.v1+tar+gzip`. Clients SHOULD send an
+ explicit `Accept` header; absent one, the server defaults to the tar+gzip codec.
+
+ Supports conditional fetch via `If-None-Match`: when the supplied ETag matches
+ the current bundle, the server responds `304 Not Modified` with no body.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
+ extra_headers = {
+ **strip_not_given(
+ {
+ "If-None-Match": if_none_match,
+ "X-Client-Request-ID": x_client_request_id,
+ }
+ ),
+ **(extra_headers or {}),
+ }
+ return self._get(
+ "/policy/bundle",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ security={"bearer_auth": True},
+ ),
+ cast_to=BinaryAPIResponse,
+ )
+
+ def update(
+ self,
+ body: FileContent | BinaryTypes,
+ *,
+ if_match: str | Omit = omit,
+ x_client_request_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> BinaryAPIResponse:
+ """
+ Accepts an edited Policy Bundle archive and applies it as the active user-scope
+ PolicySetVersion for the calling user.
+
+ The user's policy set is seeded from the system-default policies on first
+ access, forked into customer-owned policies; a user bundle therefore contains
+ only customer-owned policies. Applying an edit creates a new version of the
+ affected policy, and a `new_policy` entry adds a further customer-owned policy.
+ Platform-owned catalog policies are never edited in place by this operation.
+
+ The request body codec is determined from `Content-Type`. The only codec
+ supported today is `application/vnd.keycard.policy-bundle.v1+tar+gzip`.
+
+ Supports optimistic concurrency via `If-Match`: when supplied, the server
+ applies the bundle only if the supplied ETag matches the current bundle ETag;
+ otherwise responds `412 Precondition Failed`.
+
+ On success the server returns the materialized bundle (in the same codec) and
+ its new `ETag`.
+
+ Args:
+ body: tar+gzip Policy Bundle archive. `manifest.json` is **required** (see
+ `PolicyBundleManifest`); `schema.cedarschema` is **optional and ignored** — the
+ server validates against its attested schema for `manifest.schema.version`. The
+ manifest's `policies[]` list is authoritative for the resulting set: each entry
+ must have a matching `policies/.cedar` (or, for a `new_policy` entry,
+ `policies/.cedar`) member, and a member with no manifest entry is
+ dropped. Only the `sha` fields are advisory and recomputed server-side.
+ Duplicate or unrecognized entries are rejected with `bundle_invalid`. See the
+ **PolicyBundle** tag for the layout.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
+ extra_headers = {
+ **strip_not_given(
+ {
+ "If-Match": if_match,
+ "X-Client-Request-ID": x_client_request_id,
+ }
+ ),
+ **(extra_headers or {}),
+ }
+ extra_headers = {"Content-Type": "application/octet-stream", **(extra_headers or {})}
+ return self._put(
+ "/policy/bundle",
+ content=read_file_content(body) if isinstance(body, os.PathLike) else body,
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ security={"bearer_auth": True},
+ ),
+ cast_to=BinaryAPIResponse,
+ )
+
+ def reset(
+ self,
+ *,
+ x_client_request_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Archives the PolicySet for the calling user (if any), causing subsequent
+ `GET /policy/bundle` requests to fall back to the default user policies.
+ Idempotent: returns `204 No Content` even when no user-scope binding exists.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ extra_headers = {**strip_not_given({"X-Client-Request-ID": x_client_request_id}), **(extra_headers or {})}
+ return self._delete(
+ "/policy/bundle",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ security={"bearer_auth": True},
+ ),
+ cast_to=NoneType,
+ )
+
+
+class AsyncPolicyBundleResource(AsyncAPIResource):
+ """Per-user Policy Bundle resource.
+
+ Allows clients (typically the Keycard CLI)
+ to GET, PUT, and DELETE the effective Policy Set for the calling user
+ on a zone. The bundle is encoded with a content-negotiated codec (currently
+ only `application/vnd.keycard.policy-bundle.v1+tar+gzip`).
+
+ ## Archive layout
+
+ The bundle is a gzip-compressed tar archive with this logical layout:
+
+ | Entry | Required on PUT | Notes |
+ |-------|-----------------|-------|
+ | `manifest.json` | **Yes** | See `PolicyBundleManifest`. The only source of the authoritative `schema.version`. |
+ | `schema.cedarschema` | No | Convenience snapshot of the Cedar schema. **Ignored on PUT** — the server validates policies against its own attested schema for `manifest.schema.version`. **Always present on GET.** |
+ | `policies/.cedar` | — | One Cedar policy per file; the filename stem is the policy's public ID. |
+
+ Decode rules: duplicate entries and unrecognized/nested entries are
+ rejected (`bundle_invalid`). On PUT the manifest's `sha` fields and
+ `policies[]` list are advisory — the server recomputes every digest from
+ the archived bytes and derives the policy set from the `policies/` files.
+ On GET every digest is authoritative.
+ """
+
+ @cached_property
+ def with_raw_response(self) -> AsyncPolicyBundleResourceWithRawResponse:
+ """
+ This property can be used as a prefix for any HTTP method call to return
+ the raw response object instead of the parsed content.
+
+ For more information, see https://www.github.com/keycardai/keycard-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncPolicyBundleResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncPolicyBundleResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/keycardai/keycard-python#with_streaming_response
+ """
+ return AsyncPolicyBundleResourceWithStreamingResponse(self)
+
+ async def retrieve(
+ self,
+ *,
+ if_none_match: str | Omit = omit,
+ x_client_request_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncBinaryAPIResponse:
+ """
+ Returns the effective Policy Bundle for the user identified by the zone-issued
+ resource-scoped token. When no user-scope binding exists, one will be generated
+ from the default set.
+
+ The response body is a binary archive in the codec selected via the `Accept`
+ header. The only codec supported today is
+ `application/vnd.keycard.policy-bundle.v1+tar+gzip`. Clients SHOULD send an
+ explicit `Accept` header; absent one, the server defaults to the tar+gzip codec.
+
+ Supports conditional fetch via `If-None-Match`: when the supplied ETag matches
+ the current bundle, the server responds `304 Not Modified` with no body.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
+ extra_headers = {
+ **strip_not_given(
+ {
+ "If-None-Match": if_none_match,
+ "X-Client-Request-ID": x_client_request_id,
+ }
+ ),
+ **(extra_headers or {}),
+ }
+ return await self._get(
+ "/policy/bundle",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ security={"bearer_auth": True},
+ ),
+ cast_to=AsyncBinaryAPIResponse,
+ )
+
+ async def update(
+ self,
+ body: FileContent | AsyncBinaryTypes,
+ *,
+ if_match: str | Omit = omit,
+ x_client_request_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> AsyncBinaryAPIResponse:
+ """
+ Accepts an edited Policy Bundle archive and applies it as the active user-scope
+ PolicySetVersion for the calling user.
+
+ The user's policy set is seeded from the system-default policies on first
+ access, forked into customer-owned policies; a user bundle therefore contains
+ only customer-owned policies. Applying an edit creates a new version of the
+ affected policy, and a `new_policy` entry adds a further customer-owned policy.
+ Platform-owned catalog policies are never edited in place by this operation.
+
+ The request body codec is determined from `Content-Type`. The only codec
+ supported today is `application/vnd.keycard.policy-bundle.v1+tar+gzip`.
+
+ Supports optimistic concurrency via `If-Match`: when supplied, the server
+ applies the bundle only if the supplied ETag matches the current bundle ETag;
+ otherwise responds `412 Precondition Failed`.
+
+ On success the server returns the materialized bundle (in the same codec) and
+ its new `ETag`.
+
+ Args:
+ body: tar+gzip Policy Bundle archive. `manifest.json` is **required** (see
+ `PolicyBundleManifest`); `schema.cedarschema` is **optional and ignored** — the
+ server validates against its attested schema for `manifest.schema.version`. The
+ manifest's `policies[]` list is authoritative for the resulting set: each entry
+ must have a matching `policies/.cedar` (or, for a `new_policy` entry,
+ `policies/.cedar`) member, and a member with no manifest entry is
+ dropped. Only the `sha` fields are advisory and recomputed server-side.
+ Duplicate or unrecognized entries are rejected with `bundle_invalid`. See the
+ **PolicyBundle** tag for the layout.
+
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})}
+ extra_headers = {
+ **strip_not_given(
+ {
+ "If-Match": if_match,
+ "X-Client-Request-ID": x_client_request_id,
+ }
+ ),
+ **(extra_headers or {}),
+ }
+ extra_headers = {"Content-Type": "application/octet-stream", **(extra_headers or {})}
+ return await self._put(
+ "/policy/bundle",
+ content=await async_read_file_content(body) if isinstance(body, os.PathLike) else body,
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ security={"bearer_auth": True},
+ ),
+ cast_to=AsyncBinaryAPIResponse,
+ )
+
+ async def reset(
+ self,
+ *,
+ x_client_request_id: str | Omit = omit,
+ # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
+ # The extra values given here take precedence over values defined on the client or passed to this method.
+ extra_headers: Headers | None = None,
+ extra_query: Query | None = None,
+ extra_body: Body | None = None,
+ timeout: float | httpx.Timeout | None | NotGiven = not_given,
+ ) -> None:
+ """
+ Archives the PolicySet for the calling user (if any), causing subsequent
+ `GET /policy/bundle` requests to fall back to the default user policies.
+ Idempotent: returns `204 No Content` even when no user-scope binding exists.
+
+ Args:
+ extra_headers: Send extra headers
+
+ extra_query: Add additional query parameters to the request
+
+ extra_body: Add additional JSON properties to the request
+
+ timeout: Override the client-level default timeout for this request, in seconds
+ """
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ extra_headers = {**strip_not_given({"X-Client-Request-ID": x_client_request_id}), **(extra_headers or {})}
+ return await self._delete(
+ "/policy/bundle",
+ options=make_request_options(
+ extra_headers=extra_headers,
+ extra_query=extra_query,
+ extra_body=extra_body,
+ timeout=timeout,
+ security={"bearer_auth": True},
+ ),
+ cast_to=NoneType,
+ )
+
+
+class PolicyBundleResourceWithRawResponse:
+ def __init__(self, policy_bundle: PolicyBundleResource) -> None:
+ self._policy_bundle = policy_bundle
+
+ self.retrieve = to_custom_raw_response_wrapper(
+ policy_bundle.retrieve,
+ BinaryAPIResponse,
+ )
+ self.update = to_custom_raw_response_wrapper(
+ policy_bundle.update,
+ BinaryAPIResponse,
+ )
+ self.reset = to_raw_response_wrapper(
+ policy_bundle.reset,
+ )
+
+
+class AsyncPolicyBundleResourceWithRawResponse:
+ def __init__(self, policy_bundle: AsyncPolicyBundleResource) -> None:
+ self._policy_bundle = policy_bundle
+
+ self.retrieve = async_to_custom_raw_response_wrapper(
+ policy_bundle.retrieve,
+ AsyncBinaryAPIResponse,
+ )
+ self.update = async_to_custom_raw_response_wrapper(
+ policy_bundle.update,
+ AsyncBinaryAPIResponse,
+ )
+ self.reset = async_to_raw_response_wrapper(
+ policy_bundle.reset,
+ )
+
+
+class PolicyBundleResourceWithStreamingResponse:
+ def __init__(self, policy_bundle: PolicyBundleResource) -> None:
+ self._policy_bundle = policy_bundle
+
+ self.retrieve = to_custom_streamed_response_wrapper(
+ policy_bundle.retrieve,
+ StreamedBinaryAPIResponse,
+ )
+ self.update = to_custom_streamed_response_wrapper(
+ policy_bundle.update,
+ StreamedBinaryAPIResponse,
+ )
+ self.reset = to_streamed_response_wrapper(
+ policy_bundle.reset,
+ )
+
+
+class AsyncPolicyBundleResourceWithStreamingResponse:
+ def __init__(self, policy_bundle: AsyncPolicyBundleResource) -> None:
+ self._policy_bundle = policy_bundle
+
+ self.retrieve = async_to_custom_streamed_response_wrapper(
+ policy_bundle.retrieve,
+ AsyncStreamedBinaryAPIResponse,
+ )
+ self.update = async_to_custom_streamed_response_wrapper(
+ policy_bundle.update,
+ AsyncStreamedBinaryAPIResponse,
+ )
+ self.reset = async_to_streamed_response_wrapper(
+ policy_bundle.reset,
+ )
diff --git a/src/keycardai_api/resources/zones/policy_schemas.py b/src/keycardai_api/resources/zones/policy_schemas.py
index eb5b9fd..a5a4f5b 100644
--- a/src/keycardai_api/resources/zones/policy_schemas.py
+++ b/src/keycardai_api/resources/zones/policy_schemas.py
@@ -36,7 +36,7 @@ class PolicySchemasResource(SyncAPIResource):
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -284,7 +284,7 @@ class AsyncPolicySchemasResource(AsyncAPIResource):
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
diff --git a/src/keycardai_api/resources/zones/resources.py b/src/keycardai_api/resources/zones/resources.py
index 2941cfe..5b19dad 100644
--- a/src/keycardai_api/resources/zones/resources.py
+++ b/src/keycardai_api/resources/zones/resources.py
@@ -271,6 +271,7 @@ def list(
before: str | Omit = omit,
credential_provider_id: str | Omit = omit,
expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit,
+ filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit,
identifier: str | Omit = omit,
limit: int | Omit = omit,
slug: str | Omit = omit,
@@ -281,8 +282,12 @@ def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ResourceListResponse:
- """
- Returns a list of resources in the specified zone
+ """Returns a list of resources in the specified zone.
+
+ Filter by exact identifier
+ via `filter[identifier]` (repeatable, OR'd). Matching is exact: identifiers are
+ unique per zone, so a filter returns at most one resource per value and never
+ performs URL prefix resolution.
Args:
after: Cursor for forward pagination
@@ -291,6 +296,8 @@ def list(
credential_provider_id: Filter resources by credential provider ID
+ filter_identifier: Filter by exact resource identifier
+
identifier: Filter resources by identifier
limit: Maximum number of items to return
@@ -318,6 +325,7 @@ def list(
"before": before,
"credential_provider_id": credential_provider_id,
"expand": expand,
+ "filter_identifier": filter_identifier,
"identifier": identifier,
"limit": limit,
"slug": slug,
@@ -606,6 +614,7 @@ async def list(
before: str | Omit = omit,
credential_provider_id: str | Omit = omit,
expand: Union[Literal["total_count"], List[Literal["total_count"]]] | Omit = omit,
+ filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit,
identifier: str | Omit = omit,
limit: int | Omit = omit,
slug: str | Omit = omit,
@@ -616,8 +625,12 @@ async def list(
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
) -> ResourceListResponse:
- """
- Returns a list of resources in the specified zone
+ """Returns a list of resources in the specified zone.
+
+ Filter by exact identifier
+ via `filter[identifier]` (repeatable, OR'd). Matching is exact: identifiers are
+ unique per zone, so a filter returns at most one resource per value and never
+ performs URL prefix resolution.
Args:
after: Cursor for forward pagination
@@ -626,6 +639,8 @@ async def list(
credential_provider_id: Filter resources by credential provider ID
+ filter_identifier: Filter by exact resource identifier
+
identifier: Filter resources by identifier
limit: Maximum number of items to return
@@ -653,6 +668,7 @@ async def list(
"before": before,
"credential_provider_id": credential_provider_id,
"expand": expand,
+ "filter_identifier": filter_identifier,
"identifier": identifier,
"limit": limit,
"slug": slug,
diff --git a/src/keycardai_api/resources/zones/users.py b/src/keycardai_api/resources/zones/users.py
index 81cd5a1..52b71d8 100644
--- a/src/keycardai_api/resources/zones/users.py
+++ b/src/keycardai_api/resources/zones/users.py
@@ -88,12 +88,24 @@ def list(
after: str | Omit = omit,
before: str | Omit = omit,
expand: Union[
- Literal["total_count", "session_count", "grant_count", "role-assignments"],
- List[Literal["total_count", "session_count", "grant_count", "role-assignments"]],
+ Literal[
+ "total_count", "session_count", "grant_count", "role-assignments", "credentials", "credentials.provider"
+ ],
+ List[
+ Literal[
+ "total_count",
+ "session_count",
+ "grant_count",
+ "role-assignments",
+ "credentials",
+ "credentials.provider",
+ ]
+ ],
]
| Omit = omit,
filter_email: Union[str, SequenceNotStr[str]] | Omit = omit,
filter_id: Union[str, SequenceNotStr[str]] | Omit = omit,
+ filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit,
limit: int | Omit = omit,
query: Union[str, SequenceNotStr[str]] | Omit = omit,
query_email: Union[str, SequenceNotStr[str]] | Omit = omit,
@@ -109,27 +121,29 @@ def list(
"""
Returns a list of users in the specified zone.
- **Rollout note:** the paginated/searchable/sortable behavior described below is
- gated behind the `user-pagination` feature flag and is currently disabled for
- most zones. While the flag is off, the response returns every user in the zone
- (capped at 100) in `items` and a fixed pagination envelope where `after_cursor`
- and `before_cursor` are `null` and `total_count` is `0`. The query parameters
- below are accepted but ignored. The flag is rolled out per-zone in Datadog and
- will become the default once Console adopts the paginated contract.
+ Note: cursor pagination, search, and sort are not yet enabled for all zones.
+ Where they are not enabled, the response returns all users in the zone (capped
+ at 100) in `items`, with `after_cursor` and `before_cursor` set to `null` and
+ `total_count` of `0`; `filter[email]` and `filter[identifier]` are still
+ applied, while the pagination, search, and sort parameters below are accepted
+ but ignored.
Use cursor pagination via `after`/`before`. Sort: comma-separated field list;
prefix with `-` for descending. Use `expand[]=total_count` to include the
matching row count, `expand[]=session_count` to include per-user session counts,
- `expand[]=grant_count` to include per-user delegated-grant counts, and
- `expand[]=role-assignments` to include each user's structured role grants.
- Filter by exact email via `filter[email]`; search via `query[email]` /
- `query[subject]` / `query[]` (substring match, OR'd across repeated values).
- `query[]` matches against email and federation credential subject. Pass
- `filter[id]` (repeatable, max 100) to restrict results to a known set of users —
- mutually exclusive with `after`/`before` (returns 400 if combined). When
- `filter[id]` is set, `limit` is ignored and the response contains every
- requested user that exists in the zone, in a single page. IDs not in the zone
- are silently omitted.
+ `expand[]=grant_count` to include per-user delegated-grant counts,
+ `expand[]=role-assignments` to include each user's structured role grants,
+ `expand[]=credentials` to include each user's authentication credentials (each
+ with its `provider_id`), and `expand[]=credentials.provider` to additionally
+ inline the full identity provider on each federation credential. Filter by exact
+ email via `filter[email]` and by exact identifier via `filter[identifier]`;
+ search via `query[email]` / `query[subject]` / `query[]` (substring match, OR'd
+ across repeated values). `query[]` matches against email and federation
+ credential subject. Pass `filter[id]` (repeatable, max 100) to restrict results
+ to a known set of users — mutually exclusive with `after`/`before` (returns 400
+ if combined). When `filter[id]` is set, `limit` is ignored and the response
+ contains every requested user that exists in the zone, in a single page. IDs not
+ in the zone are silently omitted.
Args:
after: Cursor for forward pagination
@@ -141,6 +155,8 @@ def list(
filter_id: Restrict results to users with this publicId. Repeatable, max 100. Mutually
exclusive with after/before.
+ filter_identifier: Filter by exact user identifier
+
limit: Maximum number of items to return
query: Search across email and credential subject (substring match)
@@ -176,6 +192,7 @@ def list(
"expand": expand,
"filter_email": filter_email,
"filter_id": filter_id,
+ "filter_identifier": filter_identifier,
"limit": limit,
"query": query,
"query_email": query_email,
@@ -252,12 +269,24 @@ async def list(
after: str | Omit = omit,
before: str | Omit = omit,
expand: Union[
- Literal["total_count", "session_count", "grant_count", "role-assignments"],
- List[Literal["total_count", "session_count", "grant_count", "role-assignments"]],
+ Literal[
+ "total_count", "session_count", "grant_count", "role-assignments", "credentials", "credentials.provider"
+ ],
+ List[
+ Literal[
+ "total_count",
+ "session_count",
+ "grant_count",
+ "role-assignments",
+ "credentials",
+ "credentials.provider",
+ ]
+ ],
]
| Omit = omit,
filter_email: Union[str, SequenceNotStr[str]] | Omit = omit,
filter_id: Union[str, SequenceNotStr[str]] | Omit = omit,
+ filter_identifier: Union[str, SequenceNotStr[str]] | Omit = omit,
limit: int | Omit = omit,
query: Union[str, SequenceNotStr[str]] | Omit = omit,
query_email: Union[str, SequenceNotStr[str]] | Omit = omit,
@@ -273,27 +302,29 @@ async def list(
"""
Returns a list of users in the specified zone.
- **Rollout note:** the paginated/searchable/sortable behavior described below is
- gated behind the `user-pagination` feature flag and is currently disabled for
- most zones. While the flag is off, the response returns every user in the zone
- (capped at 100) in `items` and a fixed pagination envelope where `after_cursor`
- and `before_cursor` are `null` and `total_count` is `0`. The query parameters
- below are accepted but ignored. The flag is rolled out per-zone in Datadog and
- will become the default once Console adopts the paginated contract.
+ Note: cursor pagination, search, and sort are not yet enabled for all zones.
+ Where they are not enabled, the response returns all users in the zone (capped
+ at 100) in `items`, with `after_cursor` and `before_cursor` set to `null` and
+ `total_count` of `0`; `filter[email]` and `filter[identifier]` are still
+ applied, while the pagination, search, and sort parameters below are accepted
+ but ignored.
Use cursor pagination via `after`/`before`. Sort: comma-separated field list;
prefix with `-` for descending. Use `expand[]=total_count` to include the
matching row count, `expand[]=session_count` to include per-user session counts,
- `expand[]=grant_count` to include per-user delegated-grant counts, and
- `expand[]=role-assignments` to include each user's structured role grants.
- Filter by exact email via `filter[email]`; search via `query[email]` /
- `query[subject]` / `query[]` (substring match, OR'd across repeated values).
- `query[]` matches against email and federation credential subject. Pass
- `filter[id]` (repeatable, max 100) to restrict results to a known set of users —
- mutually exclusive with `after`/`before` (returns 400 if combined). When
- `filter[id]` is set, `limit` is ignored and the response contains every
- requested user that exists in the zone, in a single page. IDs not in the zone
- are silently omitted.
+ `expand[]=grant_count` to include per-user delegated-grant counts,
+ `expand[]=role-assignments` to include each user's structured role grants,
+ `expand[]=credentials` to include each user's authentication credentials (each
+ with its `provider_id`), and `expand[]=credentials.provider` to additionally
+ inline the full identity provider on each federation credential. Filter by exact
+ email via `filter[email]` and by exact identifier via `filter[identifier]`;
+ search via `query[email]` / `query[subject]` / `query[]` (substring match, OR'd
+ across repeated values). `query[]` matches against email and federation
+ credential subject. Pass `filter[id]` (repeatable, max 100) to restrict results
+ to a known set of users — mutually exclusive with `after`/`before` (returns 400
+ if combined). When `filter[id]` is set, `limit` is ignored and the response
+ contains every requested user that exists in the zone, in a single page. IDs not
+ in the zone are silently omitted.
Args:
after: Cursor for forward pagination
@@ -305,6 +336,8 @@ async def list(
filter_id: Restrict results to users with this publicId. Repeatable, max 100. Mutually
exclusive with after/before.
+ filter_identifier: Filter by exact user identifier
+
limit: Maximum number of items to return
query: Search across email and credential subject (substring match)
@@ -340,6 +373,7 @@ async def list(
"expand": expand,
"filter_email": filter_email,
"filter_id": filter_id,
+ "filter_identifier": filter_identifier,
"limit": limit,
"query": query,
"query_email": query_email,
diff --git a/src/keycardai_api/resources/zones/zones.py b/src/keycardai_api/resources/zones/zones.py
index fcbdb8a..aef2478 100644
--- a/src/keycardai_api/resources/zones/zones.py
+++ b/src/keycardai_api/resources/zones/zones.py
@@ -188,7 +188,7 @@ def policy_schemas(self) -> PolicySchemasResource:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -556,7 +556,7 @@ def policy_schemas(self) -> AsyncPolicySchemasResource:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -943,7 +943,7 @@ def policy_schemas(self) -> PolicySchemasResourceWithRawResponse:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -1034,7 +1034,7 @@ def policy_schemas(self) -> AsyncPolicySchemasResourceWithRawResponse:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -1125,7 +1125,7 @@ def policy_schemas(self) -> PolicySchemasResourceWithStreamingResponse:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
@@ -1216,7 +1216,7 @@ def policy_schemas(self) -> AsyncPolicySchemasResourceWithStreamingResponse:
- **Keycard::RegistrationMethod** — enum entity: `"managed"`, `"dcr"`
- **Keycard::CredentialType** — enum entity: `"token"`, `"password"`, `"public-key"`, `"url"`, `"public"`
- **Keycard::Resource** — `id` (String), `name` (String), `scopes` (Set of String)
- - **Keycard::Claims** — `email` (String), `groups` (Set of String), plus arbitrary additional fields
+ - **Keycard::Claims** — `email` (String), `groups` (Set of String), `issuer_claims` (issuer-specific claims record), plus arbitrary additional fields
Enum-like attributes use Cedar enum entity types (schema version `2026-03-16`+).
In policies, reference values as `RegistrationMethod::"managed"` or `CredentialType::"token"`.
diff --git a/src/keycardai_api/types/__init__.py b/src/keycardai_api/types/__init__.py
index d89e8f9..b64078b 100644
--- a/src/keycardai_api/types/__init__.py
+++ b/src/keycardai_api/types/__init__.py
@@ -18,6 +18,7 @@
from .organization_create_params import OrganizationCreateParams as OrganizationCreateParams
from .organization_list_response import OrganizationListResponse as OrganizationListResponse
from .organization_update_params import OrganizationUpdateParams as OrganizationUpdateParams
+from .policy_bundle_update_params import PolicyBundleUpdateParams as PolicyBundleUpdateParams
from .invitation_retrieve_response import InvitationRetrieveResponse as InvitationRetrieveResponse
from .organization_retrieve_params import OrganizationRetrieveParams as OrganizationRetrieveParams
from .encryption_key_aws_kms_config import EncryptionKeyAwsKmsConfig as EncryptionKeyAwsKmsConfig
diff --git a/src/keycardai_api/types/organizations/sso_connection_protocol.py b/src/keycardai_api/types/organizations/sso_connection_protocol.py
index 5767f80..42ae9b7 100644
--- a/src/keycardai_api/types/organizations/sso_connection_protocol.py
+++ b/src/keycardai_api/types/organizations/sso_connection_protocol.py
@@ -1,6 +1,6 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Optional
+from typing import Dict, List, Optional
from ..._models import BaseModel
@@ -13,6 +13,13 @@ class Oauth2(BaseModel):
authorization_endpoint: Optional[str] = None
"""OAuth 2.0 authorization endpoint"""
+ authorization_parameters: Optional[Dict[str, str]] = None
+ """Custom query parameters appended to authorization redirect URLs.
+
+ Use for non-standard providers (e.g. Google prompt=consent,
+ access_type=offline).
+ """
+
code_challenge_methods_supported: Optional[List[str]] = None
"""Supported PKCE code challenge methods"""
@@ -32,6 +39,18 @@ class Oauth2(BaseModel):
class Openid(BaseModel):
"""OpenID Connect protocol configuration for SSO connection"""
+ scopes: Optional[List[str]] = None
+ """Additional OIDC scopes to request from this provider during authentication (e.g.
+
+ "groups"). Merged with the default scopes (openid, profile, email).
+ """
+
+ user_identifier_claim: Optional[str] = None
+ """
+ Name of a top-level string claim in the provider's ID Token to use as the user
+ identifier on user creation. When not set, the user's Keycard ID is used.
+ """
+
userinfo_endpoint: Optional[str] = None
"""OpenID Connect UserInfo endpoint"""
diff --git a/src/keycardai_api/types/organizations/sso_connection_protocol_param.py b/src/keycardai_api/types/organizations/sso_connection_protocol_param.py
index 84d6074..2183279 100644
--- a/src/keycardai_api/types/organizations/sso_connection_protocol_param.py
+++ b/src/keycardai_api/types/organizations/sso_connection_protocol_param.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import Optional
+from typing import Dict, Optional
from typing_extensions import TypedDict
from ..._types import SequenceNotStr
@@ -16,6 +16,13 @@ class Oauth2(TypedDict, total=False):
authorization_endpoint: Optional[str]
"""OAuth 2.0 authorization endpoint"""
+ authorization_parameters: Optional[Dict[str, str]]
+ """Custom query parameters appended to authorization redirect URLs.
+
+ Use for non-standard providers (e.g. Google prompt=consent,
+ access_type=offline).
+ """
+
code_challenge_methods_supported: Optional[SequenceNotStr[str]]
"""Supported PKCE code challenge methods"""
@@ -35,6 +42,18 @@ class Oauth2(TypedDict, total=False):
class Openid(TypedDict, total=False):
"""OpenID Connect protocol configuration for SSO connection"""
+ scopes: Optional[SequenceNotStr[str]]
+ """Additional OIDC scopes to request from this provider during authentication (e.g.
+
+ "groups"). Merged with the default scopes (openid, profile, email).
+ """
+
+ user_identifier_claim: Optional[str]
+ """
+ Name of a top-level string claim in the provider's ID Token to use as the user
+ identifier on user creation. When not set, the user's Keycard ID is used.
+ """
+
userinfo_endpoint: Optional[str]
"""OpenID Connect UserInfo endpoint"""
diff --git a/src/keycardai_api/types/organizations/sso_connection_update_params.py b/src/keycardai_api/types/organizations/sso_connection_update_params.py
index e8d852e..93c8454 100644
--- a/src/keycardai_api/types/organizations/sso_connection_update_params.py
+++ b/src/keycardai_api/types/organizations/sso_connection_update_params.py
@@ -2,13 +2,13 @@
from __future__ import annotations
-from typing import Optional
+from typing import Dict, Optional
from typing_extensions import Annotated, TypedDict
+from ..._types import SequenceNotStr
from ..._utils import PropertyInfo
-from .sso_connection_protocol_param import SSOConnectionProtocolParam
-__all__ = ["SSOConnectionUpdateParams"]
+__all__ = ["SSOConnectionUpdateParams", "Protocols", "ProtocolsOauth2", "ProtocolsOpenid"]
class SSOConnectionUpdateParams(TypedDict, total=False):
@@ -21,7 +21,86 @@ class SSOConnectionUpdateParams(TypedDict, total=False):
identifier: str
"""SSO provider identifier (e.g., issuer URL)"""
- protocols: Optional[SSOConnectionProtocolParam]
- """Protocol configuration for SSO connection"""
+ protocols: Optional[Protocols]
+ """Protocol configuration for an SSO connection update.
+
+ Omit a protocol to leave it unchanged.
+ """
x_client_request_id: Annotated[str, PropertyInfo(alias="X-Client-Request-ID")]
+
+
+class ProtocolsOauth2(TypedDict, total=False):
+ """OAuth 2.0 protocol configuration for an SSO connection update.
+
+ Each field is tri-state, omit to leave unchanged, send null to clear, send a value to set.
+ """
+
+ authorization_endpoint: Optional[str]
+ """OAuth 2.0 authorization endpoint. Set to null to clear."""
+
+ authorization_parameters: Optional[Dict[str, str]]
+ """Custom query parameters appended to authorization redirect URLs.
+
+ Use for non-standard providers (e.g. Google prompt=consent,
+ access_type=offline). Set to null to clear.
+ """
+
+ code_challenge_methods_supported: Optional[SequenceNotStr[str]]
+ """Supported PKCE code challenge methods. Set to null to clear."""
+
+ jwks_uri: Optional[str]
+ """JSON Web Key Set endpoint. Set to null to clear."""
+
+ registration_endpoint: Optional[str]
+ """OAuth 2.0 registration endpoint. Set to null to clear."""
+
+ scopes_supported: Optional[SequenceNotStr[str]]
+ """Supported OAuth 2.0 scopes. Set to null to clear."""
+
+ token_endpoint: Optional[str]
+ """OAuth 2.0 token endpoint. Set to null to clear."""
+
+
+class ProtocolsOpenid(TypedDict, total=False):
+ """OpenID Connect protocol configuration for an SSO connection update.
+
+ Each field is tri-state, omit to leave unchanged, send null to clear, send a value to set.
+ """
+
+ scopes: Optional[SequenceNotStr[str]]
+ """Additional OIDC scopes to request from this provider during authentication (e.g.
+
+ "groups"). Merged with the default scopes (openid, profile, email). Set to null
+ to clear.
+ """
+
+ user_identifier_claim: Optional[str]
+ """
+ Name of a top-level string claim in the provider's ID Token to use as the user
+ identifier on user creation. Set to null to clear.
+ """
+
+ userinfo_endpoint: Optional[str]
+ """OpenID Connect UserInfo endpoint. Set to null to clear."""
+
+
+class Protocols(TypedDict, total=False):
+ """Protocol configuration for an SSO connection update.
+
+ Omit a protocol to leave it unchanged.
+ """
+
+ oauth2: Optional[ProtocolsOauth2]
+ """OAuth 2.0 protocol configuration for an SSO connection update.
+
+ Each field is tri-state, omit to leave unchanged, send null to clear, send a
+ value to set.
+ """
+
+ openid: Optional[ProtocolsOpenid]
+ """OpenID Connect protocol configuration for an SSO connection update.
+
+ Each field is tri-state, omit to leave unchanged, send null to clear, send a
+ value to set.
+ """
diff --git a/src/keycardai_api/types/policy_bundle_update_params.py b/src/keycardai_api/types/policy_bundle_update_params.py
new file mode 100644
index 0000000..86dd8e2
--- /dev/null
+++ b/src/keycardai_api/types/policy_bundle_update_params.py
@@ -0,0 +1,15 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing_extensions import Annotated, TypedDict
+
+from .._utils import PropertyInfo
+
+__all__ = ["PolicyBundleUpdateParams"]
+
+
+class PolicyBundleUpdateParams(TypedDict, total=False):
+ if_match: Annotated[str, PropertyInfo(alias="If-Match")]
+
+ x_client_request_id: Annotated[str, PropertyInfo(alias="X-Client-Request-ID")]
diff --git a/src/keycardai_api/types/zones/__init__.py b/src/keycardai_api/types/zones/__init__.py
index 03942fa..283dad0 100644
--- a/src/keycardai_api/types/zones/__init__.py
+++ b/src/keycardai_api/types/zones/__init__.py
@@ -20,12 +20,9 @@
from .user_agent import UserAgent as UserAgent
from .application import Application as Application
from .base_fields import BaseFields as BaseFields
-from .task_status import TaskStatus as TaskStatus
from .zone_member import ZoneMember as ZoneMember
-from .install_status import InstallStatus as InstallStatus
from .metadata_param import MetadataParam as MetadataParam
from .schema_version import SchemaVersion as SchemaVersion
-from .task_operation import TaskOperation as TaskOperation
from .user_list_params import UserListParams as UserListParams
from .application_trait import ApplicationTrait as ApplicationTrait
from .member_add_params import MemberAddParams as MemberAddParams
diff --git a/src/keycardai_api/types/zones/application_trait.py b/src/keycardai_api/types/zones/application_trait.py
index 293bb0f..c266be7 100644
--- a/src/keycardai_api/types/zones/application_trait.py
+++ b/src/keycardai_api/types/zones/application_trait.py
@@ -4,4 +4,4 @@
__all__ = ["ApplicationTrait"]
-ApplicationTrait: TypeAlias = Literal["gateway", "mcp-provider"]
+ApplicationTrait: TypeAlias = Literal["gateway", "mcp-provider", "unified-gateway", "mcp-server"]
diff --git a/src/keycardai_api/types/zones/install_status.py b/src/keycardai_api/types/zones/install_status.py
deleted file mode 100644
index 835b898..0000000
--- a/src/keycardai_api/types/zones/install_status.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing_extensions import Literal, TypeAlias
-
-__all__ = ["InstallStatus"]
-
-InstallStatus: TypeAlias = Literal["pending", "active", "deleting", "failed", "deleted"]
diff --git a/src/keycardai_api/types/zones/packages/__init__.py b/src/keycardai_api/types/zones/packages/__init__.py
deleted file mode 100644
index f8ee8b1..0000000
--- a/src/keycardai_api/types/zones/packages/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
diff --git a/src/keycardai_api/types/zones/policies/policy_version.py b/src/keycardai_api/types/zones/policies/policy_version.py
index f9982be..d8683af 100644
--- a/src/keycardai_api/types/zones/policies/policy_version.py
+++ b/src/keycardai_api/types/zones/policies/policy_version.py
@@ -43,7 +43,9 @@ class PolicyVersion(BaseModel):
"""Cedar policy in JSON representation.
Populated by default and when `format=json` is passed; null when `format=cedar`
- narrows the response to the text representation only.
+ narrows the response to the text representation only. Serialized verbatim from
+ the stored Cedar so the order of `staticPolicies` matches the source policy
+ order (ACC-613).
"""
cedar_raw: Optional[str] = None
diff --git a/src/keycardai_api/types/zones/policy_sets/policy_set_version.py b/src/keycardai_api/types/zones/policy_sets/policy_set_version.py
index 5b89efc..996c5e2 100644
--- a/src/keycardai_api/types/zones/policy_sets/policy_set_version.py
+++ b/src/keycardai_api/types/zones/policy_sets/policy_set_version.py
@@ -41,11 +41,23 @@ class PolicySetVersion(BaseModel):
version: int
active: Optional[bool] = None
- """Whether this policy set version is currently bound with mode='active'"""
+ """Whether this policy set version is currently bound with mode='active'.
+
+ Always populated in responses; clients must treat absence as unknown rather than
+ inferring 'not bound'.
+ """
archived_at: Optional[datetime] = None
+ """Timestamp when the version was archived.
+
+ Non-null only for archived versions; null or absent means not archived.
+ """
archived_by: Optional[str] = None
+ """Identifier of the actor that archived the version.
+
+ Null or absent means not archived.
+ """
attestation: Optional[AttestationStatement] = None
"""Decoded content of an Attestation JWS payload.
diff --git a/src/keycardai_api/types/zones/resource_list_params.py b/src/keycardai_api/types/zones/resource_list_params.py
index 05e841a..04dacee 100644
--- a/src/keycardai_api/types/zones/resource_list_params.py
+++ b/src/keycardai_api/types/zones/resource_list_params.py
@@ -5,6 +5,7 @@
from typing import List, Union
from typing_extensions import Literal, Annotated, TypedDict
+from ..._types import SequenceNotStr
from ..._utils import PropertyInfo
__all__ = ["ResourceListParams"]
@@ -22,6 +23,9 @@ class ResourceListParams(TypedDict, total=False):
expand: Annotated[Union[Literal["total_count"], List[Literal["total_count"]]], PropertyInfo(alias="expand[]")]
+ filter_identifier: Annotated[Union[str, SequenceNotStr[str]], PropertyInfo(alias="filter[identifier]")]
+ """Filter by exact resource identifier"""
+
identifier: str
"""Filter resources by identifier"""
diff --git a/src/keycardai_api/types/zones/task_operation.py b/src/keycardai_api/types/zones/task_operation.py
deleted file mode 100644
index d7278a8..0000000
--- a/src/keycardai_api/types/zones/task_operation.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing_extensions import Literal, TypeAlias
-
-__all__ = ["TaskOperation"]
-
-TaskOperation: TypeAlias = Literal["create", "delete"]
diff --git a/src/keycardai_api/types/zones/task_status.py b/src/keycardai_api/types/zones/task_status.py
deleted file mode 100644
index de09b18..0000000
--- a/src/keycardai_api/types/zones/task_status.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from typing_extensions import Literal, TypeAlias
-
-__all__ = ["TaskStatus"]
-
-TaskStatus: TypeAlias = Literal["pending", "running", "completed", "failed"]
diff --git a/src/keycardai_api/types/zones/user.py b/src/keycardai_api/types/zones/user.py
index a9f39ac..77590e5 100644
--- a/src/keycardai_api/types/zones/user.py
+++ b/src/keycardai_api/types/zones/user.py
@@ -1,12 +1,68 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-from typing import List, Optional
+from typing import List, Union, Optional
from datetime import datetime
-from typing_extensions import Literal
+from typing_extensions import Literal, TypeAlias
+from .provider import Provider
from ..._models import BaseModel
-__all__ = ["User", "RoleAssignment", "RoleAssignmentScope"]
+__all__ = [
+ "User",
+ "Credential",
+ "CredentialIamUserCredentialFederation",
+ "CredentialIamUserCredentialPassword",
+ "RoleAssignment",
+ "RoleAssignmentScope",
+]
+
+
+class CredentialIamUserCredentialFederation(BaseModel):
+ """Federation credential: the user authenticates through an identity provider."""
+
+ created_at: datetime
+ """Entity creation timestamp"""
+
+ provider_id: Optional[str] = None
+ """ID of the identity provider backing this credential.
+
+ `null` when the source provider has been deleted.
+ """
+
+ type: Literal["federation"]
+
+ updated_at: datetime
+ """Entity update timestamp"""
+
+ issuer: Optional[str] = None
+ """Issuer identifier of the identity provider."""
+
+ provider: Optional[Provider] = None
+ """
+ A Provider is a system that supplies access to Resources and allows actors
+ (Users or Applications) to authenticate.
+ """
+
+ subject: Optional[str] = None
+ """Subject identifier from the identity provider."""
+
+
+class CredentialIamUserCredentialPassword(BaseModel):
+ """Password credential: the user authenticates with email and password.
+
+ The email lives on the user.
+ """
+
+ created_at: datetime
+ """Entity creation timestamp"""
+
+ type: Literal["password"]
+
+ updated_at: datetime
+ """Entity update timestamp"""
+
+
+Credential: TypeAlias = Union[CredentialIamUserCredentialFederation, CredentialIamUserCredentialPassword]
class RoleAssignmentScope(BaseModel):
@@ -28,9 +84,17 @@ class RoleAssignment(BaseModel):
"""ID of the assigned role"""
role_identifier: str
- """Opaque role identifier.
+ """
+ Role identifier: a lowercase slug (letters and digits separated by single
+ hyphens or underscores), unique per owner type within a zone. Role identifiers
+ surface in policy evaluation, so the slug restriction keeps them unambiguous in
+ policy text.
+ """
- Treated as an opaque identifier by the API and unique within a zone.
+ role_owner_type: Literal["platform", "customer"]
+ """Owner type of the granted role.
+
+ Disambiguates roles that share an identifier across owner types.
"""
scope: Optional[RoleAssignmentScope] = None
@@ -77,6 +141,13 @@ class User(BaseModel):
authenticated_at: Optional[str] = None
"""Date when the user was last authenticated"""
+ credentials: Optional[List[Credential]] = None
+ """
+ Authentication credentials for this user, each carrying its identity provider
+ for federation credentials. Populated only when `expand[]=credentials` is set on
+ the listing endpoint.
+ """
+
grant_count: Optional[int] = None
"""Delegated-grant count for this user.
diff --git a/src/keycardai_api/types/zones/user_list_params.py b/src/keycardai_api/types/zones/user_list_params.py
index dfcc914..99dbba6 100644
--- a/src/keycardai_api/types/zones/user_list_params.py
+++ b/src/keycardai_api/types/zones/user_list_params.py
@@ -20,8 +20,19 @@ class UserListParams(TypedDict, total=False):
expand: Annotated[
Union[
- Literal["total_count", "session_count", "grant_count", "role-assignments"],
- List[Literal["total_count", "session_count", "grant_count", "role-assignments"]],
+ Literal[
+ "total_count", "session_count", "grant_count", "role-assignments", "credentials", "credentials.provider"
+ ],
+ List[
+ Literal[
+ "total_count",
+ "session_count",
+ "grant_count",
+ "role-assignments",
+ "credentials",
+ "credentials.provider",
+ ]
+ ],
],
PropertyInfo(alias="expand[]"),
]
@@ -35,6 +46,9 @@ class UserListParams(TypedDict, total=False):
Repeatable, max 100. Mutually exclusive with after/before.
"""
+ filter_identifier: Annotated[Union[str, SequenceNotStr[str]], PropertyInfo(alias="filter[identifier]")]
+ """Filter by exact user identifier"""
+
limit: int
"""Maximum number of items to return"""
diff --git a/tests/api_resources/organizations/test_sso_connection.py b/tests/api_resources/organizations/test_sso_connection.py
index 8be8143..dee8785 100644
--- a/tests/api_resources/organizations/test_sso_connection.py
+++ b/tests/api_resources/organizations/test_sso_connection.py
@@ -90,13 +90,18 @@ def test_method_update_with_all_params(self, client: KeycardAPI) -> None:
protocols={
"oauth2": {
"authorization_endpoint": "https://example.com",
+ "authorization_parameters": {"foo": "string"},
"code_challenge_methods_supported": ["string"],
"jwks_uri": "https://example.com",
"registration_endpoint": "https://example.com",
"scopes_supported": ["string"],
"token_endpoint": "https://example.com",
},
- "openid": {"userinfo_endpoint": "https://example.com"},
+ "openid": {
+ "scopes": ["string"],
+ "user_identifier_claim": "user_identifier_claim",
+ "userinfo_endpoint": "https://example.com",
+ },
},
x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
)
@@ -208,13 +213,18 @@ def test_method_enable_with_all_params(self, client: KeycardAPI) -> None:
protocols={
"oauth2": {
"authorization_endpoint": "https://example.com",
+ "authorization_parameters": {"foo": "string"},
"code_challenge_methods_supported": ["string"],
"jwks_uri": "https://example.com",
"registration_endpoint": "https://example.com",
"scopes_supported": ["string"],
"token_endpoint": "https://example.com",
},
- "openid": {"userinfo_endpoint": "https://example.com"},
+ "openid": {
+ "scopes": ["string"],
+ "user_identifier_claim": "user_identifier_claim",
+ "userinfo_endpoint": "https://example.com",
+ },
},
x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
)
@@ -337,13 +347,18 @@ async def test_method_update_with_all_params(self, async_client: AsyncKeycardAPI
protocols={
"oauth2": {
"authorization_endpoint": "https://example.com",
+ "authorization_parameters": {"foo": "string"},
"code_challenge_methods_supported": ["string"],
"jwks_uri": "https://example.com",
"registration_endpoint": "https://example.com",
"scopes_supported": ["string"],
"token_endpoint": "https://example.com",
},
- "openid": {"userinfo_endpoint": "https://example.com"},
+ "openid": {
+ "scopes": ["string"],
+ "user_identifier_claim": "user_identifier_claim",
+ "userinfo_endpoint": "https://example.com",
+ },
},
x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
)
@@ -455,13 +470,18 @@ async def test_method_enable_with_all_params(self, async_client: AsyncKeycardAPI
protocols={
"oauth2": {
"authorization_endpoint": "https://example.com",
+ "authorization_parameters": {"foo": "string"},
"code_challenge_methods_supported": ["string"],
"jwks_uri": "https://example.com",
"registration_endpoint": "https://example.com",
"scopes_supported": ["string"],
"token_endpoint": "https://example.com",
},
- "openid": {"userinfo_endpoint": "https://example.com"},
+ "openid": {
+ "scopes": ["string"],
+ "user_identifier_claim": "user_identifier_claim",
+ "userinfo_endpoint": "https://example.com",
+ },
},
x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
)
diff --git a/tests/api_resources/test_policy_bundle.py b/tests/api_resources/test_policy_bundle.py
new file mode 100644
index 0000000..5342e3f
--- /dev/null
+++ b/tests/api_resources/test_policy_bundle.py
@@ -0,0 +1,312 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+import os
+from typing import Any, cast
+
+import httpx
+import pytest
+from respx import MockRouter
+
+from keycardai_api import KeycardAPI, AsyncKeycardAPI
+from keycardai_api._response import (
+ BinaryAPIResponse,
+ AsyncBinaryAPIResponse,
+ StreamedBinaryAPIResponse,
+ AsyncStreamedBinaryAPIResponse,
+)
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestPolicyBundle:
+ parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_method_retrieve(self, client: KeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.get("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ policy_bundle = client.policy_bundle.retrieve()
+ assert policy_bundle.is_closed
+ assert policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_method_retrieve_with_all_params(self, client: KeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.get("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ policy_bundle = client.policy_bundle.retrieve(
+ if_none_match="If-None-Match",
+ x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+ assert policy_bundle.is_closed
+ assert policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_raw_response_retrieve(self, client: KeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.get("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+
+ policy_bundle = client.policy_bundle.with_raw_response.retrieve()
+
+ assert policy_bundle.is_closed is True
+ assert policy_bundle.http_request.headers.get("X-Stainless-Lang") == "python"
+ assert policy_bundle.json() == {"foo": "bar"}
+ assert isinstance(policy_bundle, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_streaming_response_retrieve(self, client: KeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.get("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ with client.policy_bundle.with_streaming_response.retrieve() as policy_bundle:
+ assert not policy_bundle.is_closed
+ assert policy_bundle.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ assert policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, StreamedBinaryAPIResponse)
+
+ assert cast(Any, policy_bundle.is_closed) is True
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_method_update(self, client: KeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.put("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ policy_bundle = client.policy_bundle.update(
+ body=b"Example data",
+ )
+ assert policy_bundle.is_closed
+ assert policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_method_update_with_all_params(self, client: KeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.put("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ policy_bundle = client.policy_bundle.update(
+ body=b"Example data",
+ if_match="If-Match",
+ x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+ assert policy_bundle.is_closed
+ assert policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_raw_response_update(self, client: KeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.put("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+
+ policy_bundle = client.policy_bundle.with_raw_response.update(
+ body=b"Example data",
+ )
+
+ assert policy_bundle.is_closed is True
+ assert policy_bundle.http_request.headers.get("X-Stainless-Lang") == "python"
+ assert policy_bundle.json() == {"foo": "bar"}
+ assert isinstance(policy_bundle, BinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ def test_streaming_response_update(self, client: KeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.put("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ with client.policy_bundle.with_streaming_response.update(
+ body=b"Example data",
+ ) as policy_bundle:
+ assert not policy_bundle.is_closed
+ assert policy_bundle.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ assert policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, StreamedBinaryAPIResponse)
+
+ assert cast(Any, policy_bundle.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_reset(self, client: KeycardAPI) -> None:
+ policy_bundle = client.policy_bundle.reset()
+ assert policy_bundle is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_reset_with_all_params(self, client: KeycardAPI) -> None:
+ policy_bundle = client.policy_bundle.reset(
+ x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+ assert policy_bundle is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_reset(self, client: KeycardAPI) -> None:
+ response = client.policy_bundle.with_raw_response.reset()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ policy_bundle = response.parse()
+ assert policy_bundle is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_reset(self, client: KeycardAPI) -> None:
+ with client.policy_bundle.with_streaming_response.reset() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ policy_bundle = response.parse()
+ assert policy_bundle is None
+
+ assert cast(Any, response.is_closed) is True
+
+
+class TestAsyncPolicyBundle:
+ parametrize = pytest.mark.parametrize(
+ "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
+ )
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_method_retrieve(self, async_client: AsyncKeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.get("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ policy_bundle = await async_client.policy_bundle.retrieve()
+ assert policy_bundle.is_closed
+ assert await policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_method_retrieve_with_all_params(self, async_client: AsyncKeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.get("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ policy_bundle = await async_client.policy_bundle.retrieve(
+ if_none_match="If-None-Match",
+ x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+ assert policy_bundle.is_closed
+ assert await policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_raw_response_retrieve(self, async_client: AsyncKeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.get("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+
+ policy_bundle = await async_client.policy_bundle.with_raw_response.retrieve()
+
+ assert policy_bundle.is_closed is True
+ assert policy_bundle.http_request.headers.get("X-Stainless-Lang") == "python"
+ assert await policy_bundle.json() == {"foo": "bar"}
+ assert isinstance(policy_bundle, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_streaming_response_retrieve(self, async_client: AsyncKeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.get("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ async with async_client.policy_bundle.with_streaming_response.retrieve() as policy_bundle:
+ assert not policy_bundle.is_closed
+ assert policy_bundle.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ assert await policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, AsyncStreamedBinaryAPIResponse)
+
+ assert cast(Any, policy_bundle.is_closed) is True
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_method_update(self, async_client: AsyncKeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.put("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ policy_bundle = await async_client.policy_bundle.update(
+ body=b"Example data",
+ )
+ assert policy_bundle.is_closed
+ assert await policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_method_update_with_all_params(self, async_client: AsyncKeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.put("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ policy_bundle = await async_client.policy_bundle.update(
+ body=b"Example data",
+ if_match="If-Match",
+ x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+ assert policy_bundle.is_closed
+ assert await policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_raw_response_update(self, async_client: AsyncKeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.put("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+
+ policy_bundle = await async_client.policy_bundle.with_raw_response.update(
+ body=b"Example data",
+ )
+
+ assert policy_bundle.is_closed is True
+ assert policy_bundle.http_request.headers.get("X-Stainless-Lang") == "python"
+ assert await policy_bundle.json() == {"foo": "bar"}
+ assert isinstance(policy_bundle, AsyncBinaryAPIResponse)
+
+ @parametrize
+ @pytest.mark.respx(base_url=base_url)
+ async def test_streaming_response_update(self, async_client: AsyncKeycardAPI, respx_mock: MockRouter) -> None:
+ respx_mock.put("/policy/bundle").mock(return_value=httpx.Response(200, json={"foo": "bar"}))
+ async with async_client.policy_bundle.with_streaming_response.update(
+ body=b"Example data",
+ ) as policy_bundle:
+ assert not policy_bundle.is_closed
+ assert policy_bundle.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ assert await policy_bundle.json() == {"foo": "bar"}
+ assert cast(Any, policy_bundle.is_closed) is True
+ assert isinstance(policy_bundle, AsyncStreamedBinaryAPIResponse)
+
+ assert cast(Any, policy_bundle.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_reset(self, async_client: AsyncKeycardAPI) -> None:
+ policy_bundle = await async_client.policy_bundle.reset()
+ assert policy_bundle is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_reset_with_all_params(self, async_client: AsyncKeycardAPI) -> None:
+ policy_bundle = await async_client.policy_bundle.reset(
+ x_client_request_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e",
+ )
+ assert policy_bundle is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_reset(self, async_client: AsyncKeycardAPI) -> None:
+ response = await async_client.policy_bundle.with_raw_response.reset()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ policy_bundle = await response.parse()
+ assert policy_bundle is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_reset(self, async_client: AsyncKeycardAPI) -> None:
+ async with async_client.policy_bundle.with_streaming_response.reset() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ policy_bundle = await response.parse()
+ assert policy_bundle is None
+
+ assert cast(Any, response.is_closed) is True
diff --git a/tests/api_resources/zones/test_resources.py b/tests/api_resources/zones/test_resources.py
index da273d4..4e820a7 100644
--- a/tests/api_resources/zones/test_resources.py
+++ b/tests/api_resources/zones/test_resources.py
@@ -234,6 +234,7 @@ def test_method_list_with_all_params(self, client: KeycardAPI) -> None:
before="x",
credential_provider_id="credentialProviderId",
expand="total_count",
+ filter_identifier="string",
identifier="identifier",
limit=1,
slug="slug",
@@ -546,6 +547,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKeycardAPI)
before="x",
credential_provider_id="credentialProviderId",
expand="total_count",
+ filter_identifier="string",
identifier="identifier",
limit=1,
slug="slug",
diff --git a/tests/api_resources/zones/test_users.py b/tests/api_resources/zones/test_users.py
index 1a256ef..ffc908a 100644
--- a/tests/api_resources/zones/test_users.py
+++ b/tests/api_resources/zones/test_users.py
@@ -87,6 +87,7 @@ def test_method_list_with_all_params(self, client: KeycardAPI) -> None:
expand="total_count",
filter_email="dev@stainless.com",
filter_id="string",
+ filter_identifier="string",
limit=1,
query="x",
query_email="x",
@@ -205,6 +206,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncKeycardAPI)
expand="total_count",
filter_email="dev@stainless.com",
filter_id="string",
+ filter_identifier="string",
limit=1,
query="x",
query_email="x",
diff --git a/tests/conftest.py b/tests/conftest.py
index 850730c..e57ccc4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -46,6 +46,7 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None:
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+api_key = "My API Key"
client_id = "My Client ID"
client_secret = "My Client Secret"
@@ -57,7 +58,11 @@ def client(request: FixtureRequest) -> Iterator[KeycardAPI]:
raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}")
with KeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=strict
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=strict,
) as client:
yield client
@@ -84,6 +89,7 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncKeycardAPI
async with AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=strict,
diff --git a/tests/test_client.py b/tests/test_client.py
index a174064..a570787 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -39,6 +39,7 @@
T = TypeVar("T")
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+api_key = "My API Key"
client_id = "My Client ID"
client_secret = "My Client Secret"
@@ -137,6 +138,10 @@ def test_copy(self, client: KeycardAPI) -> None:
copied = client.copy()
assert id(copied) != id(client)
+ copied = client.copy(api_key="another My API Key")
+ assert copied.api_key == "another My API Key"
+ assert client.api_key == "My API Key"
+
copied = client.copy(client_id="another My Client ID")
assert copied.client_id == "another My Client ID"
assert client.client_id == "My Client ID"
@@ -164,6 +169,7 @@ def test_copy_default_options(self, client: KeycardAPI) -> None:
def test_copy_default_headers(self) -> None:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -203,6 +209,7 @@ def test_copy_default_headers(self) -> None:
def test_copy_default_query(self) -> None:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -333,6 +340,7 @@ def test_request_timeout(self, client: KeycardAPI) -> None:
def test_client_timeout_option(self) -> None:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -350,6 +358,7 @@ def test_http_client_timeout_option(self) -> None:
with httpx.Client(timeout=None) as http_client:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -366,6 +375,7 @@ def test_http_client_timeout_option(self) -> None:
with httpx.Client() as http_client:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -382,6 +392,7 @@ def test_http_client_timeout_option(self) -> None:
with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -399,6 +410,7 @@ async def test_invalid_http_client(self) -> None:
async with httpx.AsyncClient() as http_client:
KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -408,6 +420,7 @@ async def test_invalid_http_client(self) -> None:
def test_default_headers_option(self) -> None:
test_client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -419,6 +432,7 @@ def test_default_headers_option(self) -> None:
test_client2 = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -437,6 +451,7 @@ def test_default_headers_option(self) -> None:
def test_default_query_option(self) -> None:
client = KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -636,6 +651,7 @@ def mock_handler(request: httpx.Request) -> httpx.Response:
with KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -733,6 +749,7 @@ class Model(BaseModel):
def test_base_url_setter(self) -> None:
client = KeycardAPI(
base_url="https://example.com/from_init",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -747,7 +764,9 @@ def test_base_url_setter(self) -> None:
def test_base_url_env(self) -> None:
with update_env(KEYCARD_API_BASE_URL="http://localhost:5000/from/env"):
- client = KeycardAPI(client_id=client_id, client_secret=client_secret, _strict_response_validation=True)
+ client = KeycardAPI(
+ api_key=api_key, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ )
assert client.base_url == "http://localhost:5000/from/env/"
@pytest.mark.parametrize(
@@ -755,12 +774,14 @@ def test_base_url_env(self) -> None:
[
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -785,12 +806,14 @@ def test_base_url_trailing_slash(self, client: KeycardAPI) -> None:
[
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -815,12 +838,14 @@ def test_base_url_no_trailing_slash(self, client: KeycardAPI) -> None:
[
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
KeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -842,7 +867,11 @@ def test_absolute_request_url(self, client: KeycardAPI) -> None:
def test_copied_client_does_not_close_http(self) -> None:
test_client = KeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
assert not test_client.is_closed()
@@ -855,7 +884,11 @@ def test_copied_client_does_not_close_http(self) -> None:
def test_client_context_manager(self) -> None:
test_client = KeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
with test_client as c2:
assert c2 is test_client
@@ -879,6 +912,7 @@ def test_client_max_retries_validation(self) -> None:
with pytest.raises(TypeError, match=r"max_retries cannot be None"):
KeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -893,14 +927,22 @@ class Model(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
strict_client = KeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
with pytest.raises(APIResponseValidationError):
strict_client.get("/foo", cast_to=Model)
non_strict_client = KeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=False
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=False,
)
response = non_strict_client.get("/foo", cast_to=Model)
@@ -1116,6 +1158,10 @@ def test_copy(self, async_client: AsyncKeycardAPI) -> None:
copied = async_client.copy()
assert id(copied) != id(async_client)
+ copied = async_client.copy(api_key="another My API Key")
+ assert copied.api_key == "another My API Key"
+ assert async_client.api_key == "My API Key"
+
copied = async_client.copy(client_id="another My Client ID")
assert copied.client_id == "another My Client ID"
assert async_client.client_id == "My Client ID"
@@ -1143,6 +1189,7 @@ def test_copy_default_options(self, async_client: AsyncKeycardAPI) -> None:
async def test_copy_default_headers(self) -> None:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1182,6 +1229,7 @@ async def test_copy_default_headers(self) -> None:
async def test_copy_default_query(self) -> None:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1314,6 +1362,7 @@ async def test_request_timeout(self, async_client: AsyncKeycardAPI) -> None:
async def test_client_timeout_option(self) -> None:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1331,6 +1380,7 @@ async def test_http_client_timeout_option(self) -> None:
async with httpx.AsyncClient(timeout=None) as http_client:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1347,6 +1397,7 @@ async def test_http_client_timeout_option(self) -> None:
async with httpx.AsyncClient() as http_client:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1363,6 +1414,7 @@ async def test_http_client_timeout_option(self) -> None:
async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1380,6 +1432,7 @@ def test_invalid_http_client(self) -> None:
with httpx.Client() as http_client:
AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1389,6 +1442,7 @@ def test_invalid_http_client(self) -> None:
async def test_default_headers_option(self) -> None:
test_client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1400,6 +1454,7 @@ async def test_default_headers_option(self) -> None:
test_client2 = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1418,6 +1473,7 @@ async def test_default_headers_option(self) -> None:
async def test_default_query_option(self) -> None:
client = AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1617,6 +1673,7 @@ async def mock_handler(request: httpx.Request) -> httpx.Response:
async with AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1718,6 +1775,7 @@ class Model(BaseModel):
async def test_base_url_setter(self) -> None:
client = AsyncKeycardAPI(
base_url="https://example.com/from_init",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1732,7 +1790,9 @@ async def test_base_url_setter(self) -> None:
async def test_base_url_env(self) -> None:
with update_env(KEYCARD_API_BASE_URL="http://localhost:5000/from/env"):
- client = AsyncKeycardAPI(client_id=client_id, client_secret=client_secret, _strict_response_validation=True)
+ client = AsyncKeycardAPI(
+ api_key=api_key, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ )
assert client.base_url == "http://localhost:5000/from/env/"
@pytest.mark.parametrize(
@@ -1740,12 +1800,14 @@ async def test_base_url_env(self) -> None:
[
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1770,12 +1832,14 @@ async def test_base_url_trailing_slash(self, client: AsyncKeycardAPI) -> None:
[
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1800,12 +1864,14 @@ async def test_base_url_no_trailing_slash(self, client: AsyncKeycardAPI) -> None
[
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
),
AsyncKeycardAPI(
base_url="http://localhost:5000/custom/path/",
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1827,7 +1893,11 @@ async def test_absolute_request_url(self, client: AsyncKeycardAPI) -> None:
async def test_copied_client_does_not_close_http(self) -> None:
test_client = AsyncKeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
assert not test_client.is_closed()
@@ -1841,7 +1911,11 @@ async def test_copied_client_does_not_close_http(self) -> None:
async def test_client_context_manager(self) -> None:
test_client = AsyncKeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
async with test_client as c2:
assert c2 is test_client
@@ -1867,6 +1941,7 @@ async def test_client_max_retries_validation(self) -> None:
with pytest.raises(TypeError, match=r"max_retries cannot be None"):
AsyncKeycardAPI(
base_url=base_url,
+ api_key=api_key,
client_id=client_id,
client_secret=client_secret,
_strict_response_validation=True,
@@ -1881,14 +1956,22 @@ class Model(BaseModel):
respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format"))
strict_client = AsyncKeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=True
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=True,
)
with pytest.raises(APIResponseValidationError):
await strict_client.get("/foo", cast_to=Model)
non_strict_client = AsyncKeycardAPI(
- base_url=base_url, client_id=client_id, client_secret=client_secret, _strict_response_validation=False
+ base_url=base_url,
+ api_key=api_key,
+ client_id=client_id,
+ client_secret=client_secret,
+ _strict_response_validation=False,
)
response = await non_strict_client.get("/foo", cast_to=Model)