scalekit-sdk-python 1.0.3__tar.gz → 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {scalekit_sdk_python-1.0.3/scalekit_sdk_python.egg-info → scalekit_sdk_python-1.0.4}/PKG-INFO +11 -11
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/__init__.py +2 -1
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/client.py +127 -9
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/core.py +2 -0
- scalekit_sdk_python-1.0.4/scalekit/directory.py +243 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/organization.py +20 -1
- scalekit_sdk_python-1.0.4/scalekit/utils/directory.py +81 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/clients/clients_pb2.py +27 -25
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/clients/clients_pb2.pyi +8 -4
- scalekit_sdk_python-1.0.4/scalekit/v1/commons/commons_pb2.py +62 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/commons/commons_pb2.pyi +83 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/connections/connections_pb2.py +450 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/connections/connections_pb2.pyi +70 -8
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/connections/connections_pb2_grpc.py +33 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/directories/directories_pb2.py +342 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/directories/directories_pb2.pyi +414 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/directories/directories_pb2_grpc.py +429 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/environments/environments_pb2.py +65 -23
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/environments/environments_pb2.pyi +61 -1
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/environments/environments_pb2_grpc.py +165 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/events/events_pb2.py +105 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/events/events_pb2.pyi +279 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/events/events_pb2_grpc.py +66 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/login_box/login_box_pb2.py +69 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/login_box/login_box_pb2.pyi +74 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/login_box/login_box_pb2_grpc.py +132 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/members/members_pb2.py +108 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/members/members_pb2.pyi +11 -4
- scalekit_sdk_python-1.0.4/scalekit/v1/migrations/migrations_pb2.py +48 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/migrations/migrations_pb2.pyi +37 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/migrations/migrations_pb2_grpc.py +133 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/organizations/__init__.py +0 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/organizations/organizations_pb2.py +217 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/organizations/organizations_pb2.pyi +41 -8
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/organizations/organizations_pb2_grpc.py +33 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/roles/__init__.py +0 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/roles/roles_pb2.py +109 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/roles/roles_pb2.pyi +116 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/roles/roles_pb2_grpc.py +199 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/user_attributes/__init__.py +0 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/user_attributes/user_attributes_pb2.py +95 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/user_attributes/user_attributes_pb2.pyi +111 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/user_attributes/user_attributes_pb2_grpc.py +298 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/users/__init__.py +0 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/users/users_pb2.py +104 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/users/users_pb2.pyi +49 -18
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/users/users_pb2_grpc.py +33 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/webhooks/__init__.py +0 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/webhooks/webhooks_pb2.py +43 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/webhooks/webhooks_pb2.pyi +31 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/webhooks/webhooks_pb2_grpc.py +100 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/workspaces/__init__.py +0 -0
- scalekit_sdk_python-1.0.4/scalekit/v1/workspaces/workspaces_pb2.py +130 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/workspaces/workspaces_pb2.pyi +74 -1
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/workspaces/workspaces_pb2_grpc.py +133 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4/scalekit_sdk_python.egg-info}/PKG-INFO +11 -11
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit_sdk_python.egg-info/SOURCES.txt +27 -4
- scalekit_sdk_python-1.0.4/scalekit_sdk_python.egg-info/requires.txt +10 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/setup.py +11 -11
- scalekit_sdk_python-1.0.3/scalekit/v1/commons/commons_pb2.py +0 -36
- scalekit_sdk_python-1.0.3/scalekit/v1/commons/commons_pb2.pyi +0 -30
- scalekit_sdk_python-1.0.3/scalekit/v1/connections/connections_pb2.py +0 -409
- scalekit_sdk_python-1.0.3/scalekit/v1/events/events_pb2.py +0 -39
- scalekit_sdk_python-1.0.3/scalekit/v1/events/events_pb2.pyi +0 -62
- scalekit_sdk_python-1.0.3/scalekit/v1/events/events_pb2_grpc.py +0 -4
- scalekit_sdk_python-1.0.3/scalekit/v1/members/members_pb2.py +0 -105
- scalekit_sdk_python-1.0.3/scalekit/v1/organizations/organizations_pb2.py +0 -188
- scalekit_sdk_python-1.0.3/scalekit/v1/user_profile_attributes/user_profile_attributes_pb2.py +0 -83
- scalekit_sdk_python-1.0.3/scalekit/v1/user_profile_attributes/user_profile_attributes_pb2.pyi +0 -99
- scalekit_sdk_python-1.0.3/scalekit/v1/user_profile_attributes/user_profile_attributes_pb2_grpc.py +0 -174
- scalekit_sdk_python-1.0.3/scalekit/v1/users/users_pb2.py +0 -97
- scalekit_sdk_python-1.0.3/scalekit/v1/workspaces/workspaces_pb2.py +0 -90
- scalekit_sdk_python-1.0.3/scalekit_sdk_python.egg-info/requires.txt +0 -10
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/LICENSE +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/README.md +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/validate/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/validate/expression_pb2.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/validate/expression_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/validate/expression_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/validate/priv/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/validate/priv/private_pb2.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/validate/priv/private_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/validate/priv/private_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/validate/validate_pb2.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/validate/validate_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/buf/validate/validate_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/common/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/common/scalekit.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/common/user.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/connection.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/constants/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/constants/user.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/domain.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1 → scalekit_sdk_python-1.0.4/scalekit/utils}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/clients → scalekit_sdk_python-1.0.4/scalekit/v1}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/commons → scalekit_sdk_python-1.0.4/scalekit/v1/clients}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/clients/clients_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/connections → scalekit_sdk_python-1.0.4/scalekit/v1/commons}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/commons/commons_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/domains → scalekit_sdk_python-1.0.4/scalekit/v1/connections}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/environments → scalekit_sdk_python-1.0.4/scalekit/v1/directories}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/errdetails → scalekit_sdk_python-1.0.4/scalekit/v1/domains}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/domains/domains_pb2.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/domains/domains_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/domains/domains_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/events → scalekit_sdk_python-1.0.4/scalekit/v1/environments}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/members → scalekit_sdk_python-1.0.4/scalekit/v1/errdetails}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/errdetails/errdetails_pb2.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/errdetails/errdetails_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/errdetails/errdetails_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/options → scalekit_sdk_python-1.0.4/scalekit/v1/events}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/organizations → scalekit_sdk_python-1.0.4/scalekit/v1/login_box}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/user_profile_attributes → scalekit_sdk_python-1.0.4/scalekit/v1/members}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/members/members_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/users → scalekit_sdk_python-1.0.4/scalekit/v1/migrations}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3/scalekit/v1/workspaces → scalekit_sdk_python-1.0.4/scalekit/v1/options}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/options/options_pb2.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/options/options_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit/v1/options/options_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit_sdk_python.egg-info/dependency_links.txt +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/scalekit_sdk_python.egg-info/top_level.txt +0 -0
- {scalekit_sdk_python-1.0.3 → scalekit_sdk_python-1.0.4}/setup.cfg +0 -0
{scalekit_sdk_python-1.0.3/scalekit_sdk_python.egg-info → scalekit_sdk_python-1.0.4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: scalekit-sdk-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Summary: Scalekit official Python SDK
|
|
5
5
|
Home-page: https://github.com/scalekit-inc/scalekit-sdk-python
|
|
6
6
|
Author: Team Scalekit
|
|
@@ -11,16 +11,16 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: grpcio
|
|
15
|
-
Requires-Dist: protobuf
|
|
16
|
-
Requires-Dist: google
|
|
17
|
-
Requires-Dist: requests
|
|
18
|
-
Requires-Dist: PyJWT
|
|
19
|
-
Requires-Dist: cryptography
|
|
20
|
-
Requires-Dist: setuptools
|
|
21
|
-
Requires-Dist: grpcio-status
|
|
22
|
-
Requires-Dist: protoc-gen-openapiv2
|
|
23
|
-
Requires-Dist: googleapis-common-protos
|
|
14
|
+
Requires-Dist: grpcio>=1.64.1
|
|
15
|
+
Requires-Dist: protobuf>=5.27.0
|
|
16
|
+
Requires-Dist: google>=3.0.0
|
|
17
|
+
Requires-Dist: requests>=2.32.3
|
|
18
|
+
Requires-Dist: PyJWT>=2.8.0
|
|
19
|
+
Requires-Dist: cryptography>=43.0.3
|
|
20
|
+
Requires-Dist: setuptools>=70.3.0
|
|
21
|
+
Requires-Dist: grpcio-status>=1.64.0
|
|
22
|
+
Requires-Dist: protoc-gen-openapiv2>=0.0.1
|
|
23
|
+
Requires-Dist: googleapis-common-protos>=1.56.1
|
|
24
24
|
|
|
25
25
|
<p align="left">
|
|
26
26
|
<a href="https://scalekit.com" target="_blank" rel="noopener noreferrer">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
|
|
1
2
|
from scalekit.client import ScalekitClient
|
|
2
|
-
from scalekit.common.scalekit import
|
|
3
|
+
from scalekit.common.scalekit import CodeAuthenticationOptions, AuthorizationUrlOptions
|
|
3
4
|
|
|
4
5
|
__all__ = ['ScalekitClient', 'AuthorizationUrlOptions', 'CodeAuthenticationOptions']
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
import json
|
|
3
|
+
from math import floor
|
|
4
|
+
from typing import Any, Optional, Dict
|
|
3
5
|
from urllib.parse import urlencode
|
|
4
6
|
|
|
5
7
|
import jwt
|
|
8
|
+
import hmac
|
|
9
|
+
import hashlib
|
|
10
|
+
import base64
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
6
12
|
from scalekit.core import CoreClient
|
|
7
13
|
from scalekit.domain import DomainClient
|
|
8
14
|
from scalekit.connection import ConnectionClient
|
|
9
15
|
from scalekit.organization import OrganizationClient
|
|
16
|
+
from scalekit.directory import DirectoryClient
|
|
10
17
|
from scalekit.common.scalekit import (
|
|
11
18
|
AuthorizationUrlOptions,
|
|
12
19
|
CodeAuthenticationOptions,
|
|
@@ -16,10 +23,16 @@ from scalekit.common.scalekit import (
|
|
|
16
23
|
from scalekit.constants.user import id_token_claim_to_user_map
|
|
17
24
|
|
|
18
25
|
AUTHORIZE_ENDPOINT = "oauth/authorize"
|
|
26
|
+
webhook_tolerance_in_seconds = timedelta(minutes=5)
|
|
27
|
+
webhook_signature_version = "v1"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WebhookVerificationError(Exception):
|
|
31
|
+
pass
|
|
19
32
|
|
|
20
33
|
|
|
21
34
|
class ScalekitClient:
|
|
22
|
-
""" """
|
|
35
|
+
""" Class definition for scalekit client """
|
|
23
36
|
|
|
24
37
|
def __init__(self, env_url: str, client_id: str, client_secret: str):
|
|
25
38
|
"""
|
|
@@ -31,7 +44,8 @@ class ScalekitClient:
|
|
|
31
44
|
:type : ``` str ```
|
|
32
45
|
:param client_secret : Client Secret
|
|
33
46
|
:type : ``` str ```
|
|
34
|
-
|
|
47
|
+
|
|
48
|
+
:returns:
|
|
35
49
|
None
|
|
36
50
|
"""
|
|
37
51
|
try:
|
|
@@ -41,6 +55,7 @@ class ScalekitClient:
|
|
|
41
55
|
self.domain = DomainClient(self.core_client)
|
|
42
56
|
self.connection = ConnectionClient(self.core_client)
|
|
43
57
|
self.organization = OrganizationClient(self.core_client)
|
|
58
|
+
self.directory = DirectoryClient(self.core_client)
|
|
44
59
|
except Exception as exp:
|
|
45
60
|
raise exp
|
|
46
61
|
|
|
@@ -52,9 +67,10 @@ class ScalekitClient:
|
|
|
52
67
|
|
|
53
68
|
:param redirect_uri : Redirect URI for SAML SSO
|
|
54
69
|
:type : ``` str ```
|
|
55
|
-
:param options :
|
|
70
|
+
:param options : Auth URL options object
|
|
56
71
|
:type : ``` obj ```
|
|
57
|
-
|
|
72
|
+
|
|
73
|
+
:returns:
|
|
58
74
|
Authorization URL
|
|
59
75
|
"""
|
|
60
76
|
try:
|
|
@@ -92,8 +108,9 @@ class ScalekitClient:
|
|
|
92
108
|
:type : ``` str ```
|
|
93
109
|
:param redirect_uri : Redirect URI
|
|
94
110
|
:type : ``` str ```
|
|
95
|
-
:param options :
|
|
111
|
+
:param options : CodeAuthenticationOptions Object
|
|
96
112
|
:type : ``` obj ```
|
|
113
|
+
|
|
97
114
|
:returns:
|
|
98
115
|
dict with user, id token & access token
|
|
99
116
|
"""
|
|
@@ -131,7 +148,8 @@ class ScalekitClient:
|
|
|
131
148
|
|
|
132
149
|
:param token : access token
|
|
133
150
|
:type : ``` str ```
|
|
134
|
-
|
|
151
|
+
|
|
152
|
+
:returns:
|
|
135
153
|
bool
|
|
136
154
|
"""
|
|
137
155
|
try:
|
|
@@ -146,7 +164,8 @@ class ScalekitClient:
|
|
|
146
164
|
|
|
147
165
|
:param idp_initiated_login_token : IDP initiated login token
|
|
148
166
|
:type : ``` str ```
|
|
149
|
-
|
|
167
|
+
|
|
168
|
+
:returns:
|
|
150
169
|
``` IdpInitiatedLoginClaims ```
|
|
151
170
|
"""
|
|
152
171
|
try:
|
|
@@ -163,7 +182,8 @@ class ScalekitClient:
|
|
|
163
182
|
|
|
164
183
|
:param token : token
|
|
165
184
|
:type : ``` str ```
|
|
166
|
-
|
|
185
|
+
|
|
186
|
+
:returns:
|
|
167
187
|
payload
|
|
168
188
|
"""
|
|
169
189
|
self.core_client.get_jwks()
|
|
@@ -171,3 +191,101 @@ class ScalekitClient:
|
|
|
171
191
|
key = self.core_client.keys[kid]
|
|
172
192
|
|
|
173
193
|
return jwt.decode(token, key=key, algorithms="RS256", options=options)
|
|
194
|
+
|
|
195
|
+
def verify_webhook_payload(self, secret: str, headers: Dict[str, str], payload: [str | bytes]) -> bool:
|
|
196
|
+
"""
|
|
197
|
+
Method to verify webhook payload
|
|
198
|
+
|
|
199
|
+
:param secret : Secret for webhook verification
|
|
200
|
+
:type : ``` str ```
|
|
201
|
+
:param headers : Webhook request headers
|
|
202
|
+
:type : ``` dict[str, str] ```
|
|
203
|
+
:param payload : Webhook payload in str or bytes
|
|
204
|
+
:type : ``` str | bytes ```
|
|
205
|
+
|
|
206
|
+
:returns:
|
|
207
|
+
bool
|
|
208
|
+
"""
|
|
209
|
+
payload = payload if isinstance(payload, str) else payload.decode()
|
|
210
|
+
webhook_id = headers.get("webhook-id")
|
|
211
|
+
webhook_timestamp = headers.get("webhook-timestamp")
|
|
212
|
+
webhook_signature = headers.get("webhook-signature")
|
|
213
|
+
|
|
214
|
+
if not all([webhook_id, webhook_timestamp, webhook_signature]):
|
|
215
|
+
raise WebhookVerificationError("Missing required headers")
|
|
216
|
+
|
|
217
|
+
secret_parts = secret.split("_")
|
|
218
|
+
if len(secret_parts) < 2:
|
|
219
|
+
raise WebhookVerificationError("Invalid secret")
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
secret_bytes = base64.b64decode(secret_parts[1])
|
|
223
|
+
except Exception as exp:
|
|
224
|
+
raise exp
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
timestamp = self.__verify_timestamp(webhook_timestamp)
|
|
228
|
+
except Exception as exp:
|
|
229
|
+
raise exp
|
|
230
|
+
|
|
231
|
+
timestamp_str = str(floor(timestamp.replace(tzinfo=timezone.utc).timestamp()))
|
|
232
|
+
data = f"{webhook_id}.{timestamp_str}.{payload}"
|
|
233
|
+
computed_signature = base64.b64decode(self.__compute_signature(secret_bytes, data).split(',')[1])
|
|
234
|
+
|
|
235
|
+
received_signatures = webhook_signature.split(" ")
|
|
236
|
+
for versioned_signature in received_signatures:
|
|
237
|
+
signature_parts = versioned_signature.split(",")
|
|
238
|
+
if len(signature_parts) < 2:
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
version = signature_parts[0]
|
|
242
|
+
signature = base64.b64decode(signature_parts[1])
|
|
243
|
+
|
|
244
|
+
if version != webhook_signature_version:
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
if hmac.compare_digest(signature, computed_signature):
|
|
248
|
+
return True
|
|
249
|
+
|
|
250
|
+
raise WebhookVerificationError("Invalid signature")
|
|
251
|
+
|
|
252
|
+
@staticmethod
|
|
253
|
+
def __verify_timestamp(timestamp_str: str):
|
|
254
|
+
"""
|
|
255
|
+
Method to verify time stamp
|
|
256
|
+
|
|
257
|
+
:param timestamp_str : Timestamp for verification
|
|
258
|
+
:type : ``` str ```
|
|
259
|
+
|
|
260
|
+
:returns:
|
|
261
|
+
None
|
|
262
|
+
"""
|
|
263
|
+
now = datetime.now(tz=timezone.utc)
|
|
264
|
+
try:
|
|
265
|
+
timestamp = datetime.fromtimestamp(float(timestamp_str), tz=timezone.utc)
|
|
266
|
+
except Exception:
|
|
267
|
+
raise WebhookVerificationError("Invalid Signature Headers")
|
|
268
|
+
|
|
269
|
+
if timestamp < (now - webhook_tolerance_in_seconds):
|
|
270
|
+
raise Exception("Message timestamp too old")
|
|
271
|
+
|
|
272
|
+
if timestamp > (now + webhook_tolerance_in_seconds):
|
|
273
|
+
raise Exception("Message timestamp too new")
|
|
274
|
+
|
|
275
|
+
return timestamp
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def __compute_signature(secret: bytes, data: str) -> str:
|
|
279
|
+
"""
|
|
280
|
+
Method to compute signature
|
|
281
|
+
|
|
282
|
+
:param secret : secret for signature
|
|
283
|
+
:type : ``` bytes ```
|
|
284
|
+
:param data : data for signature
|
|
285
|
+
:type : ``` str ```
|
|
286
|
+
|
|
287
|
+
:returns:
|
|
288
|
+
None
|
|
289
|
+
"""
|
|
290
|
+
signature = hmac.new(secret, data.encode(), hashlib.sha256).digest()
|
|
291
|
+
return f"v1, {base64.b64encode(signature).decode('utf-8')}"
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
from google.protobuf.json_format import MessageToJson
|
|
3
|
+
|
|
4
|
+
from scalekit.core import CoreClient
|
|
5
|
+
from scalekit.utils.directory import DirUser, ListDirUsersResponse, DirGroup, ListDirGroupsResponse
|
|
6
|
+
|
|
7
|
+
from scalekit.v1.directories.directories_pb2 import *
|
|
8
|
+
from scalekit.v1.directories.directories_pb2_grpc import DirectoryServiceStub
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DirectoryClient:
|
|
12
|
+
""" Class definition for Directory Client """
|
|
13
|
+
|
|
14
|
+
def __init__(self, core_client: CoreClient):
|
|
15
|
+
"""
|
|
16
|
+
Initializer for Directory Client
|
|
17
|
+
|
|
18
|
+
:param core_client : CoreClient Object
|
|
19
|
+
:type : ``` obj ```
|
|
20
|
+
|
|
21
|
+
:returns
|
|
22
|
+
None
|
|
23
|
+
"""
|
|
24
|
+
self.core_client = core_client
|
|
25
|
+
self.directory_service = DirectoryServiceStub(
|
|
26
|
+
self.core_client.grpc_secure_channel
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def get_directory(
|
|
30
|
+
self,
|
|
31
|
+
organization_id: str,
|
|
32
|
+
directory_id: str,
|
|
33
|
+
) -> GetDirectoryResponse:
|
|
34
|
+
"""
|
|
35
|
+
Method to get directory based on given organization and directory id
|
|
36
|
+
|
|
37
|
+
:param organization_id : Organization id
|
|
38
|
+
:type : ``` str ```
|
|
39
|
+
:param directory_id : directory id
|
|
40
|
+
:type : ``` str ```
|
|
41
|
+
|
|
42
|
+
:returns:
|
|
43
|
+
Get Directory Response
|
|
44
|
+
"""
|
|
45
|
+
return self.core_client.grpc_exec(
|
|
46
|
+
self.directory_service.GetDirectory.with_call,
|
|
47
|
+
GetDirectoryRequest(
|
|
48
|
+
id=directory_id,
|
|
49
|
+
organization_id=organization_id,
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def list_directories(self, organization_id) -> ListDirectoriesResponse:
|
|
54
|
+
"""
|
|
55
|
+
Method to list directories for given organization id
|
|
56
|
+
|
|
57
|
+
:param organization_id : org id to fetch directory list
|
|
58
|
+
:type : ``` str ```
|
|
59
|
+
|
|
60
|
+
:returns:
|
|
61
|
+
list of directories
|
|
62
|
+
"""
|
|
63
|
+
return self.core_client.grpc_exec(
|
|
64
|
+
self.directory_service.ListDirectories.with_call,
|
|
65
|
+
ListDirectoriesRequest(organization_id=organization_id),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def list_directory_users(
|
|
69
|
+
self,
|
|
70
|
+
organization_id: str,
|
|
71
|
+
directory_id: str,
|
|
72
|
+
page_size: Optional[int] = None,
|
|
73
|
+
page_token: Optional[str] = None,
|
|
74
|
+
include_detail: Optional[bool] = None,
|
|
75
|
+
updated_after: Optional[str] = None
|
|
76
|
+
) -> tuple[ListDirUsersResponse, Any]:
|
|
77
|
+
"""
|
|
78
|
+
Method to fetch list of directory users based on given organization and directory id
|
|
79
|
+
|
|
80
|
+
:param organization_id : Organization id
|
|
81
|
+
:type : ``` str ```
|
|
82
|
+
:param directory_id : directory id
|
|
83
|
+
:type : ``` str ```
|
|
84
|
+
:param page_size : page size for org list fetch
|
|
85
|
+
:type : ``` int ```
|
|
86
|
+
:param page_token : page token for org list fetch
|
|
87
|
+
:type : ``` str ```
|
|
88
|
+
:param include_detail : param to include detailed data
|
|
89
|
+
:type : ``` bool ```
|
|
90
|
+
:param updated_after : param to get updated after detail
|
|
91
|
+
:type : ``` str ```
|
|
92
|
+
|
|
93
|
+
:returns:
|
|
94
|
+
list of directory users
|
|
95
|
+
"""
|
|
96
|
+
response = self.core_client.grpc_exec(
|
|
97
|
+
self.directory_service.ListDirectoryUsers.with_call,
|
|
98
|
+
ListDirectoryUsersRequest(
|
|
99
|
+
organization_id=organization_id,
|
|
100
|
+
directory_id=directory_id,
|
|
101
|
+
page_size=page_size,
|
|
102
|
+
page_token=page_token,
|
|
103
|
+
include_detail=include_detail,
|
|
104
|
+
updated_after=updated_after
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
user_response = (ListDirUsersResponse(), response[1])
|
|
109
|
+
for user in response[0].users:
|
|
110
|
+
dir_user = DirUser()
|
|
111
|
+
dir_user.id = user.id
|
|
112
|
+
dir_user.email = user.email
|
|
113
|
+
dir_user.preferred_username = user.preferred_username
|
|
114
|
+
dir_user.given_name = user.given_name
|
|
115
|
+
dir_user.family_name = user.family_name
|
|
116
|
+
dir_user.updated_at = user.updated_at
|
|
117
|
+
dir_user.emails = user.emails
|
|
118
|
+
dir_user.groups = user.groups
|
|
119
|
+
dir_user.user_detail = MessageToJson(user.user_detail)
|
|
120
|
+
|
|
121
|
+
if not hasattr(user_response[0], 'users'):
|
|
122
|
+
user_response[0].users = [dir_user]
|
|
123
|
+
else:
|
|
124
|
+
user_response[0].users.append(dir_user)
|
|
125
|
+
user_response[0].total_size = response[0].total_size
|
|
126
|
+
user_response[0].next_page_token = response[0].next_page_token
|
|
127
|
+
user_response[0].prev_page_token = response[0].prev_page_token
|
|
128
|
+
|
|
129
|
+
return user_response
|
|
130
|
+
|
|
131
|
+
def list_directory_groups(
|
|
132
|
+
self,
|
|
133
|
+
organization_id: str,
|
|
134
|
+
directory_id: str,
|
|
135
|
+
page_size: Optional[int] = None,
|
|
136
|
+
page_token: Optional[str] = None,
|
|
137
|
+
include_detail: Optional[bool] = None,
|
|
138
|
+
updated_after: Optional[str] = None
|
|
139
|
+
) -> tuple[ListDirGroupsResponse, Any]:
|
|
140
|
+
"""
|
|
141
|
+
Method to fetch list of directory groups based on given organization and directory id
|
|
142
|
+
|
|
143
|
+
:param organization_id : Organization id
|
|
144
|
+
:type : ``` str ```
|
|
145
|
+
:param directory_id : directory id
|
|
146
|
+
:type : ``` str ```
|
|
147
|
+
:param page_size : page size for org list fetch
|
|
148
|
+
:type : ``` int ```
|
|
149
|
+
:param page_token : page token for org list fetch
|
|
150
|
+
:type : ``` str ```
|
|
151
|
+
:param include_detail : param to include detailed data
|
|
152
|
+
:type : ``` bool ```
|
|
153
|
+
:param updated_after : param to get updated after detail
|
|
154
|
+
:type : ``` str ```
|
|
155
|
+
|
|
156
|
+
:returns:
|
|
157
|
+
list of directory users
|
|
158
|
+
"""
|
|
159
|
+
response = self.core_client.grpc_exec(
|
|
160
|
+
self.directory_service.ListDirectoryGroups.with_call,
|
|
161
|
+
ListDirectoryGroupsRequest(
|
|
162
|
+
organization_id=organization_id,
|
|
163
|
+
directory_id=directory_id,
|
|
164
|
+
page_size=page_size,
|
|
165
|
+
page_token=page_token,
|
|
166
|
+
include_detail=include_detail,
|
|
167
|
+
updated_after=updated_after
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
group_response = (ListDirGroupsResponse(), response[1])
|
|
172
|
+
for group in response[0].groups:
|
|
173
|
+
dir_group = DirGroup()
|
|
174
|
+
dir_group.id = group.id
|
|
175
|
+
dir_group.display_name = group.display_name
|
|
176
|
+
dir_group.total_users = group.total_users
|
|
177
|
+
dir_group.updated_at = group.updated_at
|
|
178
|
+
dir_group.group_detail = MessageToJson(group.group_detail)
|
|
179
|
+
|
|
180
|
+
if not hasattr(group_response, 'users'):
|
|
181
|
+
group_response[0].groups = [dir_group]
|
|
182
|
+
else:
|
|
183
|
+
group_response[0].groups.append(dir_group)
|
|
184
|
+
|
|
185
|
+
group_response[0].total_size = response[0].total_size
|
|
186
|
+
group_response[0].next_page_token = response[0].next_page_token
|
|
187
|
+
group_response[0].prev_page_token = response[0].prev_page_token
|
|
188
|
+
|
|
189
|
+
return group_response
|
|
190
|
+
|
|
191
|
+
def enable_directory(self, organization_id: str, directory_id: str) -> ToggleDirectoryResponse:
|
|
192
|
+
"""
|
|
193
|
+
Method to enable directory based on given organization and directory id
|
|
194
|
+
|
|
195
|
+
:param organization_id : Organization id
|
|
196
|
+
:type : ``` str ```
|
|
197
|
+
:param directory_id : directory id
|
|
198
|
+
:type : ``` str ```
|
|
199
|
+
|
|
200
|
+
:returns:
|
|
201
|
+
Toggle Directory Response
|
|
202
|
+
"""
|
|
203
|
+
return self.core_client.grpc_exec(
|
|
204
|
+
self.directory_service.EnableDirectory.with_call,
|
|
205
|
+
ToggleDirectoryRequest(organization_id=organization_id, id=directory_id),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def disable_directory(self, organization_id: str, directory_id: str) -> ToggleDirectoryResponse:
|
|
209
|
+
"""
|
|
210
|
+
Method to disable directory based on given organization and directory id
|
|
211
|
+
|
|
212
|
+
:param organization_id : Organization id
|
|
213
|
+
:type : ``` str ```
|
|
214
|
+
:param directory_id : directory id
|
|
215
|
+
:type : ``` str ```
|
|
216
|
+
|
|
217
|
+
:returns:
|
|
218
|
+
Toggle Directory Response
|
|
219
|
+
"""
|
|
220
|
+
return self.core_client.grpc_exec(
|
|
221
|
+
self.directory_service.DisableDirectory.with_call,
|
|
222
|
+
ToggleDirectoryRequest(organization_id=organization_id, id=directory_id),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def get_primary_directory_by_organization_id(self, organization_id: str):
|
|
226
|
+
"""
|
|
227
|
+
Method to get primary directory based on given organization id
|
|
228
|
+
|
|
229
|
+
:param organization_id : Organization id
|
|
230
|
+
:type : ``` str ```
|
|
231
|
+
|
|
232
|
+
:returns:
|
|
233
|
+
Primary directory
|
|
234
|
+
"""
|
|
235
|
+
response = self.core_client.grpc_exec(
|
|
236
|
+
self.directory_service.ListDirectories.with_call,
|
|
237
|
+
ListDirectoriesRequest(organization_id=organization_id),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if response[0].directories:
|
|
241
|
+
return response[0].directories[0]
|
|
242
|
+
else:
|
|
243
|
+
raise Exception("Directory does not exist for given Organization Id.")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Optional, List, Dict
|
|
2
2
|
|
|
3
3
|
from scalekit.core import CoreClient
|
|
4
4
|
from scalekit.v1.organizations.organizations_pb2 import *
|
|
@@ -172,6 +172,7 @@ class OrganizationClient:
|
|
|
172
172
|
|
|
173
173
|
:param organization_id : Organization id to delete portal link for
|
|
174
174
|
:type : ``` str ```
|
|
175
|
+
|
|
175
176
|
:returns:
|
|
176
177
|
None
|
|
177
178
|
"""
|
|
@@ -179,3 +180,21 @@ class OrganizationClient:
|
|
|
179
180
|
self.organization_service.DeletePortalLink.with_call,
|
|
180
181
|
DeletePortalLinkRequest(id=organization_id),
|
|
181
182
|
)
|
|
183
|
+
|
|
184
|
+
def update_organization_settings(self, organization_id: str, settings: List[Dict[str, bool]]):
|
|
185
|
+
"""
|
|
186
|
+
Method to update organization settings
|
|
187
|
+
|
|
188
|
+
:param organization_id : Organization id for org update
|
|
189
|
+
:type : ``` str ```
|
|
190
|
+
:param settings : Organization settings
|
|
191
|
+
:type : ``` list[dict[str, bool]] ```
|
|
192
|
+
|
|
193
|
+
:returns:
|
|
194
|
+
None
|
|
195
|
+
"""
|
|
196
|
+
self.core_client.grpc_exec(
|
|
197
|
+
self.organization_service.UpdateOrganizationSettings.with_call,
|
|
198
|
+
UpdateOrganizationSettingsRequest(
|
|
199
|
+
id=organization_id, settings={'features': settings})
|
|
200
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
|
|
2
|
+
from google.protobuf import message as _message
|
|
3
|
+
from google.protobuf import timestamp_pb2 as _timestamp_pb2
|
|
4
|
+
from google.protobuf.internal import containers as _containers
|
|
5
|
+
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, \
|
|
6
|
+
Union as _Union
|
|
7
|
+
|
|
8
|
+
from scalekit.v1.directories.directories_pb2 import DirectoryGroup
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DirUser(_message.Message):
|
|
12
|
+
""" """
|
|
13
|
+
__slots__ = ("id", "email", "preferred_username", "given_name", "family_name", "updated_at", "emails", "groups", "user_detail")
|
|
14
|
+
ID_FIELD_NUMBER: _ClassVar[int]
|
|
15
|
+
EMAIL_FIELD_NUMBER: _ClassVar[int]
|
|
16
|
+
PREFERRED_USERNAME_FIELD_NUMBER: _ClassVar[int]
|
|
17
|
+
GIVEN_NAME_FIELD_NUMBER: _ClassVar[int]
|
|
18
|
+
FAMILY_NAME_FIELD_NUMBER: _ClassVar[int]
|
|
19
|
+
UPDATED_AT_FIELD_NUMBER: _ClassVar[int]
|
|
20
|
+
EMAILS_FIELD_NUMBER: _ClassVar[int]
|
|
21
|
+
GROUPS_FIELD_NUMBER: _ClassVar[int]
|
|
22
|
+
USER_DETAIL_FIELD_NUMBER: _ClassVar[int]
|
|
23
|
+
id: str | None
|
|
24
|
+
email: str | None
|
|
25
|
+
preferred_username: str | None
|
|
26
|
+
given_name: str | None
|
|
27
|
+
family_name: str | None
|
|
28
|
+
updated_at: _timestamp_pb2.Timestamp | None
|
|
29
|
+
emails: _containers.RepeatedScalarFieldContainer[str] | None
|
|
30
|
+
groups: _containers.RepeatedCompositeFieldContainer[DirectoryGroup] | None
|
|
31
|
+
user_detail: str | None
|
|
32
|
+
|
|
33
|
+
def __init__(self): ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DirGroup(_message.Message):
|
|
37
|
+
""" """
|
|
38
|
+
__slots__ = ("id", "display_name", "total_users", "updated_at", "group_detail")
|
|
39
|
+
ID_FIELD_NUMBER: _ClassVar[int]
|
|
40
|
+
DISPLAY_NAME_FIELD_NUMBER: _ClassVar[int]
|
|
41
|
+
TOTAL_USERS_FIELD_NUMBER: _ClassVar[int]
|
|
42
|
+
UPDATED_AT_FIELD_NUMBER: _ClassVar[int]
|
|
43
|
+
GROUP_DETAIL_FIELD_NUMBER: _ClassVar[int]
|
|
44
|
+
id: str | None
|
|
45
|
+
display_name: str | None
|
|
46
|
+
total_users: int | None
|
|
47
|
+
updated_at: _timestamp_pb2.Timestamp | None
|
|
48
|
+
group_detail: str | None
|
|
49
|
+
def __init__(self): ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ListDirUsersResponse(_message.Message):
|
|
53
|
+
__slots__ = ("users", "total_size", "next_page_token", "prev_page_token")
|
|
54
|
+
USERS_FIELD_NUMBER: _ClassVar[int]
|
|
55
|
+
TOTAL_SIZE_FIELD_NUMBER: _ClassVar[int]
|
|
56
|
+
NEXT_PAGE_TOKEN_FIELD_NUMBER: _ClassVar[int]
|
|
57
|
+
PREV_PAGE_TOKEN_FIELD_NUMBER: _ClassVar[int]
|
|
58
|
+
users: _containers.RepeatedCompositeFieldContainer[DirUser]
|
|
59
|
+
total_size: int
|
|
60
|
+
next_page_token: str
|
|
61
|
+
prev_page_token: str
|
|
62
|
+
|
|
63
|
+
def __init__(self, users: _Optional[_Iterable[_Union[DirUser, _Mapping]]] = ...,
|
|
64
|
+
total_size: _Optional[int] = ..., next_page_token: _Optional[str] = ...,
|
|
65
|
+
prev_page_token: _Optional[str] = ...) -> None: ...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ListDirGroupsResponse(_message.Message):
|
|
69
|
+
__slots__ = ("groups", "total_size", "next_page_token", "prev_page_token")
|
|
70
|
+
GROUPS_FIELD_NUMBER: _ClassVar[int]
|
|
71
|
+
TOTAL_SIZE_FIELD_NUMBER: _ClassVar[int]
|
|
72
|
+
NEXT_PAGE_TOKEN_FIELD_NUMBER: _ClassVar[int]
|
|
73
|
+
PREV_PAGE_TOKEN_FIELD_NUMBER: _ClassVar[int]
|
|
74
|
+
groups: _containers.RepeatedCompositeFieldContainer[DirGroup]
|
|
75
|
+
total_size: int
|
|
76
|
+
next_page_token: str
|
|
77
|
+
prev_page_token: str
|
|
78
|
+
|
|
79
|
+
def __init__(self, groups: _Optional[_Iterable[_Union[DirGroup, _Mapping]]] = ...,
|
|
80
|
+
total_size: _Optional[int] = ..., next_page_token: _Optional[str] = ...,
|
|
81
|
+
prev_page_token: _Optional[str] = ...) -> None: ...
|