From e6234483bc3e80382e52f9f17d01a06bf48777ce Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Fri, 20 Feb 2026 05:34:19 -1000 Subject: [PATCH 01/10] FGA implementation pr1 --- src/workos/authorization.py | 196 ++++++++++++++++++++- src/workos/types/authorization/resource.py | 5 +- tests/test_authorization_resource_crud.py | 189 ++++++++++++++++++++ 3 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 tests/test_authorization_resource_crud.py diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 6e12f035..eac54b3f 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -1,6 +1,7 @@ -from typing import Any, Dict, Optional, Protocol, Sequence +from typing import Any, Dict, Optional, Protocol, Sequence, Union from pydantic import TypeAdapter +from typing_extensions import TypedDict from workos.types.authorization.environment_role import ( EnvironmentRole, @@ -8,6 +9,7 @@ ) from workos.types.authorization.organization_role import OrganizationRole from workos.types.authorization.permission import Permission +from workos.types.authorization.resource import Resource from workos.types.authorization.role import Role, RoleList from workos.types.list_resource import ( ListArgs, @@ -28,6 +30,24 @@ ) AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions" +AUTHORIZATION_RESOURCES_PATH = "authorization/resources" + + +class ParentResourceById(TypedDict): + """Identify a parent resource by its WorkOS resource ID.""" + + resource_id: str + + +class ParentResourceByExternalId(TypedDict): + """Identify a parent resource by organization, type, and external ID.""" + + organization_id: str + resource_type: str + external_id: str + + +ParentResource = Union[ParentResourceById, ParentResourceByExternalId] _role_adapter: TypeAdapter[Role] = TypeAdapter(Role) @@ -161,6 +181,34 @@ def add_environment_role_permission( permission_slug: str, ) -> SyncOrAsync[EnvironmentRole]: ... + # Resources + + def get_resource(self, resource_id: str) -> SyncOrAsync[Resource]: ... + + def create_resource( + self, + *, + resource_type: str, + organization_id: str, + external_id: Optional[str] = None, + meta: Optional[Dict[str, Any]] = None, + parent: Optional[ParentResource] = None, + ) -> SyncOrAsync[Resource]: ... + + def update_resource( + self, + resource_id: str, + *, + meta: Optional[Dict[str, Any]] = None, + ) -> SyncOrAsync[Resource]: ... + + def delete_resource( + self, + resource_id: str, + *, + cascade_delete: Optional[bool] = None, + ) -> SyncOrAsync[None]: ... + class Authorization(AuthorizationModule): _http_client: SyncHTTPClient @@ -437,6 +485,79 @@ def add_environment_role_permission( return EnvironmentRole.model_validate(response) + # Resources + + def get_resource(self, resource_id: str) -> Resource: + response = self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_GET, + ) + + return Resource.model_validate(response) + + def create_resource( + self, + *, + resource_type: str, + organization_id: str, + external_id: Optional[str] = None, + meta: Optional[Dict[str, Any]] = None, + parent: Optional[ParentResource] = None, + ) -> Resource: + json: Dict[str, Any] = { + "resource_type": resource_type, + "organization_id": organization_id, + } + if external_id is not None: + json["external_id"] = external_id + if meta is not None: + json["meta"] = meta + if parent is not None: + json["parent"] = parent + + response = self._http_client.request( + AUTHORIZATION_RESOURCES_PATH, + method=REQUEST_METHOD_POST, + json=json, + ) + + return Resource.model_validate(response) + + def update_resource( + self, + resource_id: str, + *, + meta: Optional[Dict[str, Any]] = None, + ) -> Resource: + json: Dict[str, Any] = {} + if meta is not None: + json["meta"] = meta + + response = self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_PATCH, + json=json, + ) + + return Resource.model_validate(response) + + def delete_resource( + self, + resource_id: str, + *, + cascade_delete: Optional[bool] = None, + ) -> None: + if cascade_delete is not None: + self._http_client.delete_with_body( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + json={"cascade_delete": cascade_delete}, + ) + else: + self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_DELETE, + ) + class AsyncAuthorization(AuthorizationModule): _http_client: AsyncHTTPClient @@ -712,3 +833,76 @@ async def add_environment_role_permission( ) return EnvironmentRole.model_validate(response) + + # Resources + + async def get_resource(self, resource_id: str) -> Resource: + response = await self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_GET, + ) + + return Resource.model_validate(response) + + async def create_resource( + self, + *, + resource_type: str, + organization_id: str, + external_id: Optional[str] = None, + meta: Optional[Dict[str, Any]] = None, + parent: Optional[ParentResource] = None, + ) -> Resource: + json: Dict[str, Any] = { + "resource_type": resource_type, + "organization_id": organization_id, + } + if external_id is not None: + json["external_id"] = external_id + if meta is not None: + json["meta"] = meta + if parent is not None: + json["parent"] = parent + + response = await self._http_client.request( + AUTHORIZATION_RESOURCES_PATH, + method=REQUEST_METHOD_POST, + json=json, + ) + + return Resource.model_validate(response) + + async def update_resource( + self, + resource_id: str, + *, + meta: Optional[Dict[str, Any]] = None, + ) -> Resource: + json: Dict[str, Any] = {} + if meta is not None: + json["meta"] = meta + + response = await self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_PATCH, + json=json, + ) + + return Resource.model_validate(response) + + async def delete_resource( + self, + resource_id: str, + *, + cascade_delete: Optional[bool] = None, + ) -> None: + if cascade_delete is not None: + await self._http_client.delete_with_body( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + json={"cascade_delete": cascade_delete}, + ) + else: + await self._http_client.request( + f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", + method=REQUEST_METHOD_DELETE, + ) diff --git a/src/workos/types/authorization/resource.py b/src/workos/types/authorization/resource.py index 917673c4..5b29778b 100644 --- a/src/workos/types/authorization/resource.py +++ b/src/workos/types/authorization/resource.py @@ -1,4 +1,4 @@ -from typing import Literal, Optional +from typing import Any, Literal, Mapping, Optional from workos.types.workos_model import WorkOSModel @@ -14,5 +14,8 @@ class Resource(WorkOSModel): resource_type_slug: str organization_id: str parent_resource_id: Optional[str] = None + # The API returns meta when set via create_resource / update_resource. + # Without this field the model would silently discard that data. + meta: Optional[Mapping[str, Any]] = None created_at: str updated_at: str diff --git a/tests/test_authorization_resource_crud.py b/tests/test_authorization_resource_crud.py new file mode 100644 index 00000000..6cd9ea68 --- /dev/null +++ b/tests/test_authorization_resource_crud.py @@ -0,0 +1,189 @@ +from typing import Union + +import pytest +from tests.utils.fixtures.mock_resource import MockResource +from tests.utils.syncify import syncify +from workos.authorization import AsyncAuthorization, Authorization + + +@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) +class TestAuthorizationResourceCRUD: + @pytest.fixture(autouse=True) + def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): + self.http_client = module_instance._http_client + self.authorization = module_instance + + @pytest.fixture + def mock_resource(self): + return MockResource(id="res_01ABC").dict() + + # --- get_resource --- + + def test_get_resource(self, mock_resource, capture_and_mock_http_client_request): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + resource = syncify(self.authorization.get_resource("res_01ABC")) + + assert resource.id == "res_01ABC" + assert resource.object == "authorization_resource" + assert request_kwargs["method"] == "get" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + + # --- create_resource --- + + def test_create_resource_required_fields_only( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + resource = syncify( + self.authorization.create_resource( + resource_type="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + ) + ) + + assert resource.id == "res_01ABC" + assert request_kwargs["method"] == "post" + assert request_kwargs["url"].endswith("/authorization/resources") + assert request_kwargs["json"] == { + "resource_type": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + } + + def test_create_resource_with_all_optional_fields( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + syncify( + self.authorization.create_resource( + resource_type="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + external_id="ext_123", + meta={"key": "value"}, + parent={"resource_id": "res_01PARENT"}, + ) + ) + + assert request_kwargs["json"] == { + "resource_type": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "external_id": "ext_123", + "meta": {"key": "value"}, + "parent": {"resource_id": "res_01PARENT"}, + } + + def test_create_resource_with_parent_by_id( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + syncify( + self.authorization.create_resource( + resource_type="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + parent={"resource_id": "res_01PARENT"}, + ) + ) + + assert request_kwargs["json"]["parent"] == {"resource_id": "res_01PARENT"} + + def test_create_resource_with_parent_by_external_id( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + syncify( + self.authorization.create_resource( + resource_type="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + parent={ + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "resource_type": "folder", + "external_id": "ext_parent_456", + }, + ) + ) + + assert request_kwargs["json"]["parent"] == { + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "resource_type": "folder", + "external_id": "ext_parent_456", + } + + # --- update_resource --- + + def test_update_resource_with_meta( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + resource = syncify( + self.authorization.update_resource( + "res_01ABC", + meta={"updated_key": "updated_value"}, + ) + ) + + assert resource.id == "res_01ABC" + assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + assert request_kwargs["json"] == {"meta": {"updated_key": "updated_value"}} + + def test_update_resource_without_meta( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + syncify(self.authorization.update_resource("res_01ABC")) + + assert request_kwargs["method"] == "patch" + assert request_kwargs["json"] == {} + + # --- delete_resource --- + + def test_delete_resource_without_cascade( + self, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, + status_code=202, + headers={"content-type": "text/plain; charset=utf-8"}, + ) + + response = syncify(self.authorization.delete_resource("res_01ABC")) + + assert response is None + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + + def test_delete_resource_with_cascade(self, capture_and_mock_http_client_request): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, + status_code=202, + headers={"content-type": "text/plain; charset=utf-8"}, + ) + + response = syncify( + self.authorization.delete_resource("res_01ABC", cascade_delete=True) + ) + + assert response is None + assert request_kwargs["method"] == "delete" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + assert request_kwargs["json"] == {"cascade_delete": True} From 99d54a56a247449e71402764a7116356b1c0c3df Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Fri, 20 Feb 2026 06:52:02 -1000 Subject: [PATCH 02/10] cleanup --- src/workos/authorization.py | 89 +++++++++++----------- src/workos/types/authorization/resource.py | 3 - 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/workos/authorization.py b/src/workos/authorization.py index eac54b3f..eeea21bb 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -34,17 +34,12 @@ class ParentResourceById(TypedDict): - """Identify a parent resource by its WorkOS resource ID.""" - - resource_id: str + parent_resource_id: str class ParentResourceByExternalId(TypedDict): - """Identify a parent resource by organization, type, and external ID.""" - - organization_id: str - resource_type: str - external_id: str + parent_resource_external_id: str + parent_resource_type_slug: str ParentResource = Union[ParentResourceById, ParentResourceByExternalId] @@ -188,18 +183,20 @@ def get_resource(self, resource_id: str) -> SyncOrAsync[Resource]: ... def create_resource( self, *, - resource_type: str, + resource_type_slug: str, organization_id: str, - external_id: Optional[str] = None, - meta: Optional[Dict[str, Any]] = None, - parent: Optional[ParentResource] = None, + external_id: str, + name: str, + parent: ParentResource, + description: Optional[str] = None, ) -> SyncOrAsync[Resource]: ... def update_resource( self, resource_id: str, *, - meta: Optional[Dict[str, Any]] = None, + name: Optional[str] = None, + description: Optional[str] = None, ) -> SyncOrAsync[Resource]: ... def delete_resource( @@ -498,22 +495,22 @@ def get_resource(self, resource_id: str) -> Resource: def create_resource( self, *, - resource_type: str, + resource_type_slug: str, organization_id: str, - external_id: Optional[str] = None, - meta: Optional[Dict[str, Any]] = None, - parent: Optional[ParentResource] = None, + external_id: str, + name: str, + parent: ParentResource, + description: Optional[str] = None, ) -> Resource: json: Dict[str, Any] = { - "resource_type": resource_type, + "resource_type_slug": resource_type_slug, "organization_id": organization_id, + "external_id": external_id, + "name": name, + **parent, } - if external_id is not None: - json["external_id"] = external_id - if meta is not None: - json["meta"] = meta - if parent is not None: - json["parent"] = parent + if description is not None: + json["description"] = description response = self._http_client.request( AUTHORIZATION_RESOURCES_PATH, @@ -525,13 +522,16 @@ def create_resource( def update_resource( self, - resource_id: str, *, - meta: Optional[Dict[str, Any]] = None, + resource_id: str, + name: Optional[str] = None, + description: Optional[str] = None, ) -> Resource: json: Dict[str, Any] = {} - if meta is not None: - json["meta"] = meta + if name is not None: + json["name"] = name + if description is not None: + json["description"] = description response = self._http_client.request( f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", @@ -543,8 +543,8 @@ def update_resource( def delete_resource( self, - resource_id: str, *, + resource_id: str, cascade_delete: Optional[bool] = None, ) -> None: if cascade_delete is not None: @@ -847,22 +847,22 @@ async def get_resource(self, resource_id: str) -> Resource: async def create_resource( self, *, - resource_type: str, + resource_type_slug: str, organization_id: str, - external_id: Optional[str] = None, - meta: Optional[Dict[str, Any]] = None, - parent: Optional[ParentResource] = None, + external_id: str, + name: str, + parent: ParentResource, + description: Optional[str] = None, ) -> Resource: json: Dict[str, Any] = { - "resource_type": resource_type, + "resource_type_slug": resource_type_slug, "organization_id": organization_id, + "external_id": external_id, + "name": name, + **parent, } - if external_id is not None: - json["external_id"] = external_id - if meta is not None: - json["meta"] = meta - if parent is not None: - json["parent"] = parent + if description is not None: + json["description"] = description response = await self._http_client.request( AUTHORIZATION_RESOURCES_PATH, @@ -876,11 +876,14 @@ async def update_resource( self, resource_id: str, *, - meta: Optional[Dict[str, Any]] = None, + name: Optional[str] = None, + description: Optional[str] = None, ) -> Resource: json: Dict[str, Any] = {} - if meta is not None: - json["meta"] = meta + if name is not None: + json["name"] = name + if description is not None: + json["description"] = description response = await self._http_client.request( f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", diff --git a/src/workos/types/authorization/resource.py b/src/workos/types/authorization/resource.py index 5b29778b..e699292b 100644 --- a/src/workos/types/authorization/resource.py +++ b/src/workos/types/authorization/resource.py @@ -14,8 +14,5 @@ class Resource(WorkOSModel): resource_type_slug: str organization_id: str parent_resource_id: Optional[str] = None - # The API returns meta when set via create_resource / update_resource. - # Without this field the model would silently discard that data. - meta: Optional[Mapping[str, Any]] = None created_at: str updated_at: str From 989e7058124e175ee69bd990495b1d75d950c59f Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Fri, 20 Feb 2026 06:56:36 -1000 Subject: [PATCH 03/10] lint --- src/workos/types/authorization/resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workos/types/authorization/resource.py b/src/workos/types/authorization/resource.py index e699292b..917673c4 100644 --- a/src/workos/types/authorization/resource.py +++ b/src/workos/types/authorization/resource.py @@ -1,4 +1,4 @@ -from typing import Any, Literal, Mapping, Optional +from typing import Literal, Optional from workos.types.workos_model import WorkOSModel From ed9d761cdbf26079a6565a4c3e5d168a910b7d24 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Fri, 20 Feb 2026 07:08:48 -1000 Subject: [PATCH 04/10] lint --- src/workos/authorization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/workos/authorization.py b/src/workos/authorization.py index eeea21bb..3f587917 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -522,8 +522,8 @@ def create_resource( def update_resource( self, - *, resource_id: str, + *, name: Optional[str] = None, description: Optional[str] = None, ) -> Resource: @@ -543,8 +543,8 @@ def update_resource( def delete_resource( self, - *, resource_id: str, + *, cascade_delete: Optional[bool] = None, ) -> None: if cascade_delete is not None: From 48cefa93ecf769839df938140ae12b72501c0916 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Fri, 20 Feb 2026 07:21:06 -1000 Subject: [PATCH 05/10] lint --- tests/test_authorization_resource_crud.py | 56 ++++++++++++++--------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/tests/test_authorization_resource_crud.py b/tests/test_authorization_resource_crud.py index 6cd9ea68..01e7353e 100644 --- a/tests/test_authorization_resource_crud.py +++ b/tests/test_authorization_resource_crud.py @@ -42,8 +42,11 @@ def test_create_resource_required_fields_only( resource = syncify( self.authorization.create_resource( - resource_type="document", + resource_type_slug="document", organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + external_id="ext_123", + name="Test Resource", + parent={"parent_resource_id": "res_01PARENT"}, ) ) @@ -51,8 +54,11 @@ def test_create_resource_required_fields_only( assert request_kwargs["method"] == "post" assert request_kwargs["url"].endswith("/authorization/resources") assert request_kwargs["json"] == { - "resource_type": "document", + "resource_type_slug": "document", "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "external_id": "ext_123", + "name": "Test Resource", + "parent_resource_id": "res_01PARENT", } def test_create_resource_with_all_optional_fields( @@ -64,20 +70,22 @@ def test_create_resource_with_all_optional_fields( syncify( self.authorization.create_resource( - resource_type="document", + resource_type_slug="document", organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", external_id="ext_123", - meta={"key": "value"}, - parent={"resource_id": "res_01PARENT"}, + name="Test Resource", + parent={"parent_resource_id": "res_01PARENT"}, + description="A test document", ) ) assert request_kwargs["json"] == { - "resource_type": "document", + "resource_type_slug": "document", "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", "external_id": "ext_123", - "meta": {"key": "value"}, - "parent": {"resource_id": "res_01PARENT"}, + "name": "Test Resource", + "parent_resource_id": "res_01PARENT", + "description": "A test document", } def test_create_resource_with_parent_by_id( @@ -89,13 +97,15 @@ def test_create_resource_with_parent_by_id( syncify( self.authorization.create_resource( - resource_type="document", + resource_type_slug="document", organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - parent={"resource_id": "res_01PARENT"}, + external_id="ext_123", + name="Test Resource", + parent={"parent_resource_id": "res_01PARENT"}, ) ) - assert request_kwargs["json"]["parent"] == {"resource_id": "res_01PARENT"} + assert request_kwargs["json"]["parent_resource_id"] == "res_01PARENT" def test_create_resource_with_parent_by_external_id( self, mock_resource, capture_and_mock_http_client_request @@ -106,21 +116,19 @@ def test_create_resource_with_parent_by_external_id( syncify( self.authorization.create_resource( - resource_type="document", + resource_type_slug="document", organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + external_id="ext_123", + name="Test Resource", parent={ - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "resource_type": "folder", - "external_id": "ext_parent_456", + "parent_resource_external_id": "ext_parent_456", + "parent_resource_type_slug": "folder", }, ) ) - assert request_kwargs["json"]["parent"] == { - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "resource_type": "folder", - "external_id": "ext_parent_456", - } + assert request_kwargs["json"]["parent_resource_external_id"] == "ext_parent_456" + assert request_kwargs["json"]["parent_resource_type_slug"] == "folder" # --- update_resource --- @@ -134,14 +142,18 @@ def test_update_resource_with_meta( resource = syncify( self.authorization.update_resource( "res_01ABC", - meta={"updated_key": "updated_value"}, + name="Updated Name", + description="Updated description", ) ) assert resource.id == "res_01ABC" assert request_kwargs["method"] == "patch" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["json"] == {"meta": {"updated_key": "updated_value"}} + assert request_kwargs["json"] == { + "name": "Updated Name", + "description": "Updated description", + } def test_update_resource_without_meta( self, mock_resource, capture_and_mock_http_client_request From 9dc8d6f5953daa4820b0c61519dc407ac0dba2cb Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Tue, 24 Feb 2026 08:09:42 -1000 Subject: [PATCH 06/10] testing --- .claude/settings.local.json | 7 +++++ src/workos/authorization.py | 32 ++++++++++++++------ src/workos/utils/_base_http_client.py | 3 +- src/workos/utils/http_client.py | 6 ++++ tests/test_authorization_resource_crud.py | 37 +++++++++++++++++++++++ 5 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..489c3d39 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(uv run pytest:*)" + ] + } +} diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 3f587917..cbe9ad20 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -1,8 +1,18 @@ +from enum import Enum from typing import Any, Dict, Optional, Protocol, Sequence, Union from pydantic import TypeAdapter from typing_extensions import TypedDict + +class _Unset(Enum): + """Sentinel to distinguish 'not provided' from an explicit None.""" + + TOKEN = 0 + + +UNSET: _Unset = _Unset.TOKEN + from workos.types.authorization.environment_role import ( EnvironmentRole, EnvironmentRoleList, @@ -187,7 +197,7 @@ def create_resource( organization_id: str, external_id: str, name: str, - parent: ParentResource, + parent: Optional[ParentResource] = None, description: Optional[str] = None, ) -> SyncOrAsync[Resource]: ... @@ -196,7 +206,7 @@ def update_resource( resource_id: str, *, name: Optional[str] = None, - description: Optional[str] = None, + description: Union[str, None, _Unset] = UNSET, ) -> SyncOrAsync[Resource]: ... def delete_resource( @@ -499,7 +509,7 @@ def create_resource( organization_id: str, external_id: str, name: str, - parent: ParentResource, + parent: Optional[ParentResource] = None, description: Optional[str] = None, ) -> Resource: json: Dict[str, Any] = { @@ -507,7 +517,7 @@ def create_resource( "organization_id": organization_id, "external_id": external_id, "name": name, - **parent, + **(parent or {}), } if description is not None: json["description"] = description @@ -525,18 +535,19 @@ def update_resource( resource_id: str, *, name: Optional[str] = None, - description: Optional[str] = None, + description: Union[str, None, _Unset] = UNSET, ) -> Resource: json: Dict[str, Any] = {} if name is not None: json["name"] = name - if description is not None: + if not isinstance(description, _Unset): json["description"] = description response = self._http_client.request( f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", method=REQUEST_METHOD_PATCH, json=json, + exclude_none=False, ) return Resource.model_validate(response) @@ -851,7 +862,7 @@ async def create_resource( organization_id: str, external_id: str, name: str, - parent: ParentResource, + parent: Optional[ParentResource] = None, description: Optional[str] = None, ) -> Resource: json: Dict[str, Any] = { @@ -859,7 +870,7 @@ async def create_resource( "organization_id": organization_id, "external_id": external_id, "name": name, - **parent, + **(parent or {}), } if description is not None: json["description"] = description @@ -877,18 +888,19 @@ async def update_resource( resource_id: str, *, name: Optional[str] = None, - description: Optional[str] = None, + description: Union[str, None, _Unset] = UNSET, ) -> Resource: json: Dict[str, Any] = {} if name is not None: json["name"] = name - if description is not None: + if not isinstance(description, _Unset): json["description"] = description response = await self._http_client.request( f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", method=REQUEST_METHOD_PATCH, json=json, + exclude_none=False, ) return Resource.model_validate(response) diff --git a/src/workos/utils/_base_http_client.py b/src/workos/utils/_base_http_client.py index ad5ebaa5..402d71c2 100644 --- a/src/workos/utils/_base_http_client.py +++ b/src/workos/utils/_base_http_client.py @@ -124,6 +124,7 @@ def _prepare_request( headers: HeadersType = None, exclude_default_auth_headers: bool = False, force_include_body: bool = False, + exclude_none: bool = True, ) -> PreparedRequest: """Executes a request against the WorkOS API. @@ -159,7 +160,7 @@ def _prepare_request( params = {k: v for k, v in params.items() if v is not None} # Remove any body values that are None - if json is not None and isinstance(json, Mapping): + if exclude_none and json is not None and isinstance(json, Mapping): json = {k: v for k, v in json.items() if v is not None} # We'll spread these return values onto the HTTP client request method diff --git a/src/workos/utils/http_client.py b/src/workos/utils/http_client.py index 9a2d7a57..5c7deac5 100644 --- a/src/workos/utils/http_client.py +++ b/src/workos/utils/http_client.py @@ -88,6 +88,7 @@ def request( json: JsonType = None, headers: HeadersType = None, exclude_default_auth_headers: bool = False, + exclude_none: bool = True, ) -> ResponseJson: """Executes a request against the WorkOS API. @@ -98,6 +99,7 @@ def request( method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants params (ParamsType): Query params to be added to the request json (JsonType): Body payload to be added to the request + exclude_none (bool): If True, removes None values from the JSON body Returns: ResponseJson: Response from WorkOS @@ -109,6 +111,7 @@ def request( json=json, headers=headers, exclude_default_auth_headers=exclude_default_auth_headers, + exclude_none=exclude_none, ) response = self._client.request(**prepared_request_parameters) return self._handle_response(response) @@ -206,6 +209,7 @@ async def request( json: JsonType = None, headers: HeadersType = None, exclude_default_auth_headers: bool = False, + exclude_none: bool = True, ) -> ResponseJson: """Executes a request against the WorkOS API. @@ -216,6 +220,7 @@ async def request( method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants params (ParamsType): Query params to be added to the request json (JsonType): Body payload to be added to the request + exclude_none (bool): If True, removes None values from the JSON body Returns: ResponseJson: Response from WorkOS @@ -227,6 +232,7 @@ async def request( json=json, headers=headers, exclude_default_auth_headers=exclude_default_auth_headers, + exclude_none=exclude_none, ) response = await self._client.request(**prepared_request_parameters) return self._handle_response(response) diff --git a/tests/test_authorization_resource_crud.py b/tests/test_authorization_resource_crud.py index 01e7353e..1921ed92 100644 --- a/tests/test_authorization_resource_crud.py +++ b/tests/test_authorization_resource_crud.py @@ -61,6 +61,31 @@ def test_create_resource_required_fields_only( "parent_resource_id": "res_01PARENT", } + def test_create_resource_without_parent( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 201 + ) + + resource = syncify( + self.authorization.create_resource( + resource_type_slug="document", + organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", + external_id="ext_123", + name="Test Resource", + ) + ) + + assert resource.id == "res_01ABC" + assert request_kwargs["method"] == "post" + assert request_kwargs["json"] == { + "resource_type_slug": "document", + "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", + "external_id": "ext_123", + "name": "Test Resource", + } + def test_create_resource_with_all_optional_fields( self, mock_resource, capture_and_mock_http_client_request ): @@ -155,6 +180,18 @@ def test_update_resource_with_meta( "description": "Updated description", } + def test_update_resource_clear_description( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + syncify(self.authorization.update_resource("res_01ABC", description=None)) + + assert request_kwargs["method"] == "patch" + assert request_kwargs["json"] == {"description": None} + def test_update_resource_without_meta( self, mock_resource, capture_and_mock_http_client_request ): From e0ed7466d6561f99bfd2df4e98b100c822bd66fe Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Tue, 24 Feb 2026 08:18:26 -1000 Subject: [PATCH 07/10] more --- src/workos/authorization.py | 16 +++++++--------- tests/test_authorization_resource_crud.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/workos/authorization.py b/src/workos/authorization.py index cbe9ad20..0232f24d 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -4,15 +4,6 @@ from pydantic import TypeAdapter from typing_extensions import TypedDict - -class _Unset(Enum): - """Sentinel to distinguish 'not provided' from an explicit None.""" - - TOKEN = 0 - - -UNSET: _Unset = _Unset.TOKEN - from workos.types.authorization.environment_role import ( EnvironmentRole, EnvironmentRoleList, @@ -39,6 +30,13 @@ class _Unset(Enum): REQUEST_METHOD_PUT, ) +class _Unset(Enum): + + TOKEN = 0 + + +UNSET: _Unset = _Unset.TOKEN + AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions" AUTHORIZATION_RESOURCES_PATH = "authorization/resources" diff --git a/tests/test_authorization_resource_crud.py b/tests/test_authorization_resource_crud.py index 1921ed92..65cfe329 100644 --- a/tests/test_authorization_resource_crud.py +++ b/tests/test_authorization_resource_crud.py @@ -204,6 +204,28 @@ def test_update_resource_without_meta( assert request_kwargs["method"] == "patch" assert request_kwargs["json"] == {} + + def test_update_resource_without_desc( + self, mock_resource, capture_and_mock_http_client_request + ): + request_kwargs = capture_and_mock_http_client_request( + self.http_client, mock_resource, 200 + ) + + resource = syncify( + self.authorization.update_resource( + "res_01ABC", + name="Updated Name", + ) + ) + + assert resource.id == "res_01ABC" + assert request_kwargs["method"] == "patch" + assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") + assert request_kwargs["json"] == { + "name": "Updated Name" + } + # --- delete_resource --- def test_delete_resource_without_cascade( From cbc1c12a3df6b102cd253c8e22e168e05b6ce011 Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Tue, 24 Feb 2026 08:19:21 -1000 Subject: [PATCH 08/10] moar --- src/workos/authorization.py | 2 +- tests/test_authorization_resource_crud.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 0232f24d..16566f8c 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -30,8 +30,8 @@ REQUEST_METHOD_PUT, ) -class _Unset(Enum): +class _Unset(Enum): TOKEN = 0 diff --git a/tests/test_authorization_resource_crud.py b/tests/test_authorization_resource_crud.py index 65cfe329..3ff79a2d 100644 --- a/tests/test_authorization_resource_crud.py +++ b/tests/test_authorization_resource_crud.py @@ -204,7 +204,6 @@ def test_update_resource_without_meta( assert request_kwargs["method"] == "patch" assert request_kwargs["json"] == {} - def test_update_resource_without_desc( self, mock_resource, capture_and_mock_http_client_request ): @@ -222,9 +221,7 @@ def test_update_resource_without_desc( assert resource.id == "res_01ABC" assert request_kwargs["method"] == "patch" assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["json"] == { - "name": "Updated Name" - } + assert request_kwargs["json"] == {"name": "Updated Name"} # --- delete_resource --- From 0af2dd1f703fa4b463c40b140b748ee77f74ae3b Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Tue, 24 Feb 2026 08:19:46 -1000 Subject: [PATCH 09/10] moar --- .claude/settings.local.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 489c3d39..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(uv run pytest:*)" - ] - } -} From 3713990f5c7b8a6aee69bd1e137a862bc9325d5d Mon Sep 17 00:00:00 2001 From: swaroopakkineni Date: Tue, 24 Feb 2026 08:25:54 -1000 Subject: [PATCH 10/10] moar --- src/workos/authorization.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/workos/authorization.py b/src/workos/authorization.py index 16566f8c..1d3ed1b9 100644 --- a/src/workos/authorization.py +++ b/src/workos/authorization.py @@ -515,8 +515,9 @@ def create_resource( "organization_id": organization_id, "external_id": external_id, "name": name, - **(parent or {}), } + if parent is not None: + json.update(parent) if description is not None: json["description"] = description @@ -868,8 +869,9 @@ async def create_resource( "organization_id": organization_id, "external_id": external_id, "name": name, - **(parent or {}), } + if parent is not None: + json.update(parent) if description is not None: json["description"] = description