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..6d52be0 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 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/keycard/keycard-api-9033c94c3389ccc8501a1403313165fc2985801e98b166f21766cc601e33bcea.yml +openapi_spec_hash: 85d79369d2b5eb95a49751607792b987 +config_hash: e1aa3a147cff2ff69e1f0bf2c7d0e6bb diff --git a/CHANGELOG.md b/CHANGELOG.md index cefd0e4..53d78eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 0.17.0 (2026-07-22) + +Full Changelog: [v0.16.0...v0.17.0](https://github.com/keycardai/keycard-python/compare/v0.16.0...v0.17.0) + +### Features + +* **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)) +* **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)) + + +### 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..de50220 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: 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..4aa915e 100644 --- a/src/keycardai_api/_client.py +++ b/src/keycardai_api/_client.py @@ -160,12 +160,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: @@ -390,12 +405,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: 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/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/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/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/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..f99d086 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): @@ -77,6 +133,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/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)