Skip to content

Commit 8dd434f

Browse files
FGA_1: create/delete/get/update resource (#563)
1 parent a5823c6 commit 8dd434f

File tree

4 files changed

+474
-2
lines changed

4 files changed

+474
-2
lines changed

src/workos/authorization.py

Lines changed: 209 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
from typing import Any, Dict, Optional, Protocol, Sequence
1+
from enum import Enum
2+
from typing import Any, Dict, Optional, Protocol, Sequence, Union
23

34
from pydantic import TypeAdapter
5+
from typing_extensions import TypedDict
46

57
from workos.types.authorization.environment_role import (
68
EnvironmentRole,
79
EnvironmentRoleList,
810
)
911
from workos.types.authorization.organization_role import OrganizationRole
1012
from workos.types.authorization.permission import Permission
13+
from workos.types.authorization.resource import Resource
1114
from workos.types.authorization.role import Role, RoleList
1215
from workos.types.list_resource import (
1316
ListArgs,
@@ -27,9 +30,28 @@
2730
REQUEST_METHOD_PUT,
2831
)
2932

33+
34+
class _Unset(Enum):
35+
TOKEN = 0
36+
37+
38+
UNSET: _Unset = _Unset.TOKEN
39+
3040
AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions"
3141
AUTHORIZATION_RESOURCES_PATH = "authorization/resources"
3242

43+
44+
class ParentResourceById(TypedDict):
45+
parent_resource_id: str
46+
47+
48+
class ParentResourceByExternalId(TypedDict):
49+
parent_resource_external_id: str
50+
parent_resource_type_slug: str
51+
52+
53+
ParentResource = Union[ParentResourceById, ParentResourceByExternalId]
54+
3355
_role_adapter: TypeAdapter[Role] = TypeAdapter(Role)
3456

3557

@@ -162,6 +184,36 @@ def add_environment_role_permission(
162184
permission_slug: str,
163185
) -> SyncOrAsync[EnvironmentRole]: ...
164186

187+
# Resources
188+
189+
def get_resource(self, resource_id: str) -> SyncOrAsync[Resource]: ...
190+
191+
def create_resource(
192+
self,
193+
*,
194+
resource_type_slug: str,
195+
organization_id: str,
196+
external_id: str,
197+
name: str,
198+
parent: Optional[ParentResource] = None,
199+
description: Optional[str] = None,
200+
) -> SyncOrAsync[Resource]: ...
201+
202+
def update_resource(
203+
self,
204+
resource_id: str,
205+
*,
206+
name: Optional[str] = None,
207+
description: Union[str, None, _Unset] = UNSET,
208+
) -> SyncOrAsync[Resource]: ...
209+
210+
def delete_resource(
211+
self,
212+
resource_id: str,
213+
*,
214+
cascade_delete: Optional[bool] = None,
215+
) -> SyncOrAsync[None]: ...
216+
165217

166218
class Authorization(AuthorizationModule):
167219
_http_client: SyncHTTPClient
@@ -438,6 +490,84 @@ def add_environment_role_permission(
438490

439491
return EnvironmentRole.model_validate(response)
440492

493+
# Resources
494+
495+
def get_resource(self, resource_id: str) -> Resource:
496+
response = self._http_client.request(
497+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
498+
method=REQUEST_METHOD_GET,
499+
)
500+
501+
return Resource.model_validate(response)
502+
503+
def create_resource(
504+
self,
505+
*,
506+
resource_type_slug: str,
507+
organization_id: str,
508+
external_id: str,
509+
name: str,
510+
parent: Optional[ParentResource] = None,
511+
description: Optional[str] = None,
512+
) -> Resource:
513+
json: Dict[str, Any] = {
514+
"resource_type_slug": resource_type_slug,
515+
"organization_id": organization_id,
516+
"external_id": external_id,
517+
"name": name,
518+
}
519+
if parent is not None:
520+
json.update(parent)
521+
if description is not None:
522+
json["description"] = description
523+
524+
response = self._http_client.request(
525+
AUTHORIZATION_RESOURCES_PATH,
526+
method=REQUEST_METHOD_POST,
527+
json=json,
528+
)
529+
530+
return Resource.model_validate(response)
531+
532+
def update_resource(
533+
self,
534+
resource_id: str,
535+
*,
536+
name: Optional[str] = None,
537+
description: Union[str, None, _Unset] = UNSET,
538+
) -> Resource:
539+
json: Dict[str, Any] = {}
540+
if name is not None:
541+
json["name"] = name
542+
if not isinstance(description, _Unset):
543+
json["description"] = description
544+
545+
response = self._http_client.request(
546+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
547+
method=REQUEST_METHOD_PATCH,
548+
json=json,
549+
exclude_none=False,
550+
)
551+
552+
return Resource.model_validate(response)
553+
554+
def delete_resource(
555+
self,
556+
resource_id: str,
557+
*,
558+
cascade_delete: Optional[bool] = None,
559+
) -> None:
560+
if cascade_delete is not None:
561+
self._http_client.delete_with_body(
562+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
563+
json={"cascade_delete": cascade_delete},
564+
)
565+
else:
566+
self._http_client.request(
567+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
568+
method=REQUEST_METHOD_DELETE,
569+
)
570+
441571

442572
class AsyncAuthorization(AuthorizationModule):
443573
_http_client: AsyncHTTPClient
@@ -713,3 +843,81 @@ async def add_environment_role_permission(
713843
)
714844

715845
return EnvironmentRole.model_validate(response)
846+
847+
# Resources
848+
849+
async def get_resource(self, resource_id: str) -> Resource:
850+
response = await self._http_client.request(
851+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
852+
method=REQUEST_METHOD_GET,
853+
)
854+
855+
return Resource.model_validate(response)
856+
857+
async def create_resource(
858+
self,
859+
*,
860+
resource_type_slug: str,
861+
organization_id: str,
862+
external_id: str,
863+
name: str,
864+
parent: Optional[ParentResource] = None,
865+
description: Optional[str] = None,
866+
) -> Resource:
867+
json: Dict[str, Any] = {
868+
"resource_type_slug": resource_type_slug,
869+
"organization_id": organization_id,
870+
"external_id": external_id,
871+
"name": name,
872+
}
873+
if parent is not None:
874+
json.update(parent)
875+
if description is not None:
876+
json["description"] = description
877+
878+
response = await self._http_client.request(
879+
AUTHORIZATION_RESOURCES_PATH,
880+
method=REQUEST_METHOD_POST,
881+
json=json,
882+
)
883+
884+
return Resource.model_validate(response)
885+
886+
async def update_resource(
887+
self,
888+
resource_id: str,
889+
*,
890+
name: Optional[str] = None,
891+
description: Union[str, None, _Unset] = UNSET,
892+
) -> Resource:
893+
json: Dict[str, Any] = {}
894+
if name is not None:
895+
json["name"] = name
896+
if not isinstance(description, _Unset):
897+
json["description"] = description
898+
899+
response = await self._http_client.request(
900+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
901+
method=REQUEST_METHOD_PATCH,
902+
json=json,
903+
exclude_none=False,
904+
)
905+
906+
return Resource.model_validate(response)
907+
908+
async def delete_resource(
909+
self,
910+
resource_id: str,
911+
*,
912+
cascade_delete: Optional[bool] = None,
913+
) -> None:
914+
if cascade_delete is not None:
915+
await self._http_client.delete_with_body(
916+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
917+
json={"cascade_delete": cascade_delete},
918+
)
919+
else:
920+
await self._http_client.request(
921+
f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}",
922+
method=REQUEST_METHOD_DELETE,
923+
)

src/workos/utils/_base_http_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def _prepare_request(
124124
headers: HeadersType = None,
125125
exclude_default_auth_headers: bool = False,
126126
force_include_body: bool = False,
127+
exclude_none: bool = True,
127128
) -> PreparedRequest:
128129
"""Executes a request against the WorkOS API.
129130
@@ -159,7 +160,7 @@ def _prepare_request(
159160
params = {k: v for k, v in params.items() if v is not None}
160161

161162
# Remove any body values that are None
162-
if json is not None and isinstance(json, Mapping):
163+
if exclude_none and json is not None and isinstance(json, Mapping):
163164
json = {k: v for k, v in json.items() if v is not None}
164165

165166
# We'll spread these return values onto the HTTP client request method

src/workos/utils/http_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def request(
8888
json: JsonType = None,
8989
headers: HeadersType = None,
9090
exclude_default_auth_headers: bool = False,
91+
exclude_none: bool = True,
9192
) -> ResponseJson:
9293
"""Executes a request against the WorkOS API.
9394
@@ -98,6 +99,7 @@ def request(
9899
method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants
99100
params (ParamsType): Query params to be added to the request
100101
json (JsonType): Body payload to be added to the request
102+
exclude_none (bool): If True, removes None values from the JSON body
101103
102104
Returns:
103105
ResponseJson: Response from WorkOS
@@ -109,6 +111,7 @@ def request(
109111
json=json,
110112
headers=headers,
111113
exclude_default_auth_headers=exclude_default_auth_headers,
114+
exclude_none=exclude_none,
112115
)
113116
response = self._client.request(**prepared_request_parameters)
114117
return self._handle_response(response)
@@ -206,6 +209,7 @@ async def request(
206209
json: JsonType = None,
207210
headers: HeadersType = None,
208211
exclude_default_auth_headers: bool = False,
212+
exclude_none: bool = True,
209213
) -> ResponseJson:
210214
"""Executes a request against the WorkOS API.
211215
@@ -216,6 +220,7 @@ async def request(
216220
method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants
217221
params (ParamsType): Query params to be added to the request
218222
json (JsonType): Body payload to be added to the request
223+
exclude_none (bool): If True, removes None values from the JSON body
219224
220225
Returns:
221226
ResponseJson: Response from WorkOS
@@ -227,6 +232,7 @@ async def request(
227232
json=json,
228233
headers=headers,
229234
exclude_default_auth_headers=exclude_default_auth_headers,
235+
exclude_none=exclude_none,
230236
)
231237
response = await self._client.request(**prepared_request_parameters)
232238
return self._handle_response(response)

0 commit comments

Comments
 (0)