Skip to content

Commit 33fe64d

Browse files
author
Asaf Cohen
authored
Merge pull request #56 from permitio/asaf/cto-248-expose-bulk-operations-api-in-python-sdk-bulk-create-users
Expose bulk operations api in python sdk bulk create users
2 parents 9e67f42 + a870455 commit 33fe64d

9 files changed

Lines changed: 552 additions & 5 deletions

File tree

permit/api/models.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2078,6 +2078,34 @@ class Config:
20782078
)
20792079

20802080

2081+
class ResourceInstanceCreateBulkOperation(BaseModel):
2082+
class Config:
2083+
extra = Extra.allow
2084+
2085+
operations: List[ResourceInstanceCreate] = Field(..., title="Operations")
2086+
2087+
2088+
class ResourceInstanceCreateBulkOperationResult(BaseModel):
2089+
pass
2090+
2091+
class Config:
2092+
extra = Extra.allow
2093+
2094+
2095+
class ResourceInstanceDeleteBulkOperation(BaseModel):
2096+
class Config:
2097+
extra = Extra.allow
2098+
2099+
idents: List[str] = Field(..., title="Idents")
2100+
2101+
2102+
class ResourceInstanceDeleteBulkOperationResult(BaseModel):
2103+
pass
2104+
2105+
class Config:
2106+
extra = Extra.allow
2107+
2108+
20812109
class ResourceInstanceRead(BaseModel):
20822110
class Config:
20832111
extra = Extra.allow

permit/api/resource_instances.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,15 @@
1515
required_permissions,
1616
)
1717
from .context import ApiContextLevel, ApiKeyAccessLevel
18-
from .models import ResourceInstanceCreate, ResourceInstanceRead, ResourceInstanceUpdate
18+
from .models import (
19+
ResourceInstanceCreate,
20+
ResourceInstanceCreateBulkOperation,
21+
ResourceInstanceCreateBulkOperationResult,
22+
ResourceInstanceDeleteBulkOperation,
23+
ResourceInstanceDeleteBulkOperationResult,
24+
ResourceInstanceRead,
25+
ResourceInstanceUpdate,
26+
)
1927

2028

2129
class ResourceInstancesApi(BasePermitApi):
@@ -28,6 +36,15 @@ def __resource_instances(self) -> SimpleHttpClient:
2836
)
2937
)
3038

39+
@property
40+
def __bulk_operations(self) -> SimpleHttpClient:
41+
return self._build_http_client(
42+
"/v2/facts/{proj_id}/{env_id}/bulk/resource_instances".format(
43+
proj_id=self.config.api_context.project,
44+
env_id=self.config.api_context.environment,
45+
)
46+
)
47+
3148
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
3249
@required_context(ApiContextLevel.ENVIRONMENT)
3350
@validate_arguments
@@ -201,3 +218,57 @@ async def delete(self, instance_key: str) -> None:
201218
PermitContextError: If the configured ApiContext does not match the required endpoint context.
202219
"""
203220
return await self.__resource_instances.delete(f"/{instance_key}")
221+
222+
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
223+
@required_context(ApiContextLevel.ENVIRONMENT)
224+
@validate_arguments
225+
async def bulk_replace(
226+
self, resource_instances: List[ResourceInstanceCreate]
227+
) -> ResourceInstanceCreateBulkOperationResult:
228+
"""
229+
Creates (and if need replaces) resource instances in bulk.
230+
231+
If the resource instance exists - replaces it.
232+
Otherwise creates previously non-existing resource instances.
233+
234+
Args:
235+
resource_instances: The resource instances to create/replace.
236+
237+
Returns:
238+
the bulk replace report.
239+
240+
Raises:
241+
PermitApiError: If the API returns an error HTTP status code.
242+
PermitContextError: If the configured ApiContext does not match the required endpoint context.
243+
"""
244+
return await self.__bulk_operations.put(
245+
"",
246+
model=ResourceInstanceCreateBulkOperationResult,
247+
json=ResourceInstanceCreateBulkOperation(operations=resource_instances),
248+
)
249+
250+
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
251+
@required_context(ApiContextLevel.ENVIRONMENT)
252+
@validate_arguments
253+
async def bulk_delete(
254+
self, resource_instances: List[str]
255+
) -> ResourceInstanceDeleteBulkOperationResult:
256+
"""
257+
Deletes resource instances in bulk.
258+
259+
Args:
260+
resource_instances: The resource instance identities to delete.
261+
Each identity can be either `resource_type:instance_key` (like Repository:react) or the resource instance uuid.
262+
263+
Returns:
264+
the bulk delete report.
265+
266+
Raises:
267+
PermitApiError: If the API returns an error HTTP status code.
268+
PermitContextError: If the configured ApiContext does not match the required endpoint context.
269+
"""
270+
return await self.__bulk_operations.delete(
271+
"",
272+
model=ResourceInstanceDeleteBulkOperationResult,
273+
json=ResourceInstanceDeleteBulkOperation(idents=resource_instances),
274+
)

permit/api/tenants.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@
1515
required_permissions,
1616
)
1717
from .context import ApiContextLevel, ApiKeyAccessLevel
18-
from .models import PaginatedResultUserRead, TenantCreate, TenantRead, TenantUpdate
18+
from .models import (
19+
PaginatedResultUserRead,
20+
TenantCreate,
21+
TenantCreateBulkOperation,
22+
TenantCreateBulkOperationResult,
23+
TenantDeleteBulkOperation,
24+
TenantDeleteBulkOperationResult,
25+
TenantRead,
26+
TenantUpdate,
27+
)
1928

2029

2130
class TenantsApi(BasePermitApi):
@@ -28,6 +37,15 @@ def __tenants(self) -> SimpleHttpClient:
2837
)
2938
)
3039

40+
@property
41+
def __bulk_operations(self) -> SimpleHttpClient:
42+
return self._build_http_client(
43+
"/v2/facts/{proj_id}/{env_id}/bulk/tenants".format(
44+
proj_id=self.config.api_context.project,
45+
env_id=self.config.api_context.environment,
46+
)
47+
)
48+
3149
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
3250
@required_context(ApiContextLevel.ENVIRONMENT)
3351
@validate_arguments
@@ -215,3 +233,53 @@ async def delete_tenant_user(self, tenant_key: str, user_key: str) -> None:
215233
PermitContextError: If the configured ApiContext does not match the required endpoint context.
216234
"""
217235
return await self.__tenants.delete(f"/{tenant_key}/users/{user_key}")
236+
237+
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
238+
@required_context(ApiContextLevel.ENVIRONMENT)
239+
@validate_arguments
240+
async def bulk_create(
241+
self, tenants: List[TenantCreate]
242+
) -> TenantCreateBulkOperationResult:
243+
"""
244+
Creates tenants in bulk.
245+
246+
Args:
247+
tenants: The tenants to create
248+
249+
Returns:
250+
the bulk creation report.
251+
252+
Raises:
253+
PermitApiError: If the API returns an error HTTP status code.
254+
PermitContextError: If the configured ApiContext does not match the required endpoint context.
255+
"""
256+
return await self.__bulk_operations.post(
257+
"",
258+
model=TenantCreateBulkOperationResult,
259+
json=TenantCreateBulkOperation(operations=tenants),
260+
)
261+
262+
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
263+
@required_context(ApiContextLevel.ENVIRONMENT)
264+
@validate_arguments
265+
async def bulk_delete(self, tenants: List[str]) -> TenantDeleteBulkOperationResult:
266+
"""
267+
Deletes tenants in bulk.
268+
269+
If the tenant exists - replaces it. Otherwise creates a non-existing tenant.
270+
271+
Args:
272+
tenants: The tenants identities to delete. Each identity can be either the tenant key or the tenant id.
273+
274+
Returns:
275+
the bulk delete report.
276+
277+
Raises:
278+
PermitApiError: If the API returns an error HTTP status code.
279+
PermitContextError: If the configured ApiContext does not match the required endpoint context.
280+
"""
281+
return await self.__bulk_operations.delete(
282+
"",
283+
model=TenantDeleteBulkOperationResult,
284+
json=TenantDeleteBulkOperation(idents=tenants),
285+
)

permit/api/users.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@
2121
RoleAssignmentRead,
2222
RoleAssignmentRemove,
2323
UserCreate,
24+
UserCreateBulkOperation,
25+
UserCreateBulkOperationResult,
26+
UserDeleteBulkOperation,
27+
UserDeleteBulkOperationResult,
2428
UserRead,
29+
UserReplaceBulkOperation,
30+
UserReplaceBulkOperationResult,
2531
UserUpdate,
2632
)
2733

@@ -45,6 +51,15 @@ def __role_assignments(self) -> SimpleHttpClient:
4551
)
4652
)
4753

54+
@property
55+
def __bulk_operations(self) -> SimpleHttpClient:
56+
return self._build_http_client(
57+
"/v2/facts/{proj_id}/{env_id}/bulk/users".format(
58+
proj_id=self.config.api_context.project,
59+
env_id=self.config.api_context.environment,
60+
)
61+
)
62+
4863
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
4964
@required_context(ApiContextLevel.ENVIRONMENT)
5065
@validate_arguments
@@ -211,6 +226,82 @@ async def delete(self, user_key: str) -> None:
211226
"""
212227
return await self.__users.delete(f"/{user_key}")
213228

229+
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
230+
@required_context(ApiContextLevel.ENVIRONMENT)
231+
@validate_arguments
232+
async def bulk_create(
233+
self, users: List[UserCreate]
234+
) -> UserCreateBulkOperationResult:
235+
"""
236+
Creates users in bulk.
237+
238+
Args:
239+
users: The users to create
240+
241+
Returns:
242+
the bulk creation report.
243+
244+
Raises:
245+
PermitApiError: If the API returns an error HTTP status code.
246+
PermitContextError: If the configured ApiContext does not match the required endpoint context.
247+
"""
248+
return await self.__bulk_operations.post(
249+
"",
250+
model=UserCreateBulkOperationResult,
251+
json=UserCreateBulkOperation(operations=users),
252+
)
253+
254+
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
255+
@required_context(ApiContextLevel.ENVIRONMENT)
256+
@validate_arguments
257+
async def bulk_replace(
258+
self, users: List[UserCreate]
259+
) -> UserReplaceBulkOperationResult:
260+
"""
261+
Replaces users in bulk.
262+
263+
If the user exists - replaces it.
264+
Otherwise creates previously non-existing users.
265+
266+
Args:
267+
users: The users to replace.
268+
269+
Returns:
270+
the bulk replace report.
271+
272+
Raises:
273+
PermitApiError: If the API returns an error HTTP status code.
274+
PermitContextError: If the configured ApiContext does not match the required endpoint context.
275+
"""
276+
return await self.__bulk_operations.put(
277+
"",
278+
model=UserReplaceBulkOperationResult,
279+
json=UserReplaceBulkOperation(operations=users),
280+
)
281+
282+
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
283+
@required_context(ApiContextLevel.ENVIRONMENT)
284+
@validate_arguments
285+
async def bulk_delete(self, users: List[str]) -> UserDeleteBulkOperationResult:
286+
"""
287+
Deletes users in bulk.
288+
289+
Args:
290+
users: The users identities to delete. Each identity can be either the user key or the user id.
291+
292+
Returns:
293+
the bulk delete report.
294+
295+
Raises:
296+
PermitApiError: If the API returns an error HTTP status code.
297+
PermitContextError: If the configured ApiContext does not match the required endpoint context.
298+
"""
299+
return await self.__bulk_operations.delete(
300+
"",
301+
model=UserDeleteBulkOperationResult,
302+
json=UserDeleteBulkOperation(idents=users),
303+
)
304+
214305
@required_permissions(ApiKeyAccessLevel.ENVIRONMENT_LEVEL_API_KEY)
215306
@required_context(ApiContextLevel.ENVIRONMENT)
216307
@validate_arguments

permit/exceptions.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,32 @@ def status_code(self) -> int:
9595
"""
9696
return self._response.status
9797

98+
@property
99+
def content_type(self) -> str:
100+
"""
101+
Get the HTTP content type header of the error response.
102+
103+
Returns:
104+
The value of the HTTP Response Content-type header, or None
105+
"""
106+
return self._response.headers.get("content-type")
107+
98108

99109
async def handle_api_error(response: aiohttp.ClientResponse):
100110
if response.status < 200 or response.status >= 400:
111+
# handle non-json errors (can be returned by load balancer)
112+
content_type = response.headers.get("content-type")
113+
if content_type is not None and content_type.lower() != "application/json":
114+
error_string = await response.text()
115+
raise PermitApiError(
116+
f"{response.status} API Error",
117+
response,
118+
json={"status_code": response.status, "error": error_string},
119+
)
120+
121+
# fallback to handle json errors
101122
json = await response.json()
102-
raise PermitApiError("API error", response, json)
123+
raise PermitApiError(f"{response.status} API error", response, json)
103124

104125

105126
def handle_client_error(func):

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def get_requirements(env=""):
1010

1111
setup(
1212
name="permit",
13-
version="2.2.2",
13+
version="2.3.0",
1414
packages=find_packages(),
1515
author="Asaf Cohen",
1616
author_email="asaf@permit.io",

0 commit comments

Comments
 (0)