scalekit-sdk-python 1.0.2__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.2/scalekit_sdk_python.egg-info → scalekit_sdk_python-1.0.4}/PKG-INFO +12 -12
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/__init__.py +2 -1
- scalekit_sdk_python-1.0.4/scalekit/client.py +291 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/common/scalekit.py +10 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/core.py +3 -1
- scalekit_sdk_python-1.0.4/scalekit/directory.py +243 -0
- {scalekit_sdk_python-1.0.2 → 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.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/clients/clients_pb2.py +27 -25
- {scalekit_sdk_python-1.0.2 → 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.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/connections/connections_pb2.pyi +70 -8
- {scalekit_sdk_python-1.0.2 → 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.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/environments/environments_pb2.py +65 -23
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/environments/environments_pb2.pyi +61 -1
- {scalekit_sdk_python-1.0.2 → 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.2 → 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.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/organizations/organizations_pb2.pyi +41 -8
- {scalekit_sdk_python-1.0.2 → 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.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/users/users_pb2.pyi +49 -18
- {scalekit_sdk_python-1.0.2 → 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.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/workspaces/workspaces_pb2.pyi +74 -1
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/workspaces/workspaces_pb2_grpc.py +133 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4/scalekit_sdk_python.egg-info}/PKG-INFO +12 -12
- {scalekit_sdk_python-1.0.2 → 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.2 → scalekit_sdk_python-1.0.4}/setup.py +12 -12
- scalekit_sdk_python-1.0.2/scalekit/client.py +0 -145
- scalekit_sdk_python-1.0.2/scalekit/v1/commons/commons_pb2.py +0 -36
- scalekit_sdk_python-1.0.2/scalekit/v1/commons/commons_pb2.pyi +0 -30
- scalekit_sdk_python-1.0.2/scalekit/v1/connections/connections_pb2.py +0 -409
- scalekit_sdk_python-1.0.2/scalekit/v1/events/events_pb2.py +0 -39
- scalekit_sdk_python-1.0.2/scalekit/v1/events/events_pb2.pyi +0 -62
- scalekit_sdk_python-1.0.2/scalekit/v1/events/events_pb2_grpc.py +0 -4
- scalekit_sdk_python-1.0.2/scalekit/v1/members/members_pb2.py +0 -105
- scalekit_sdk_python-1.0.2/scalekit/v1/organizations/organizations_pb2.py +0 -188
- scalekit_sdk_python-1.0.2/scalekit/v1/user_profile_attributes/user_profile_attributes_pb2.py +0 -83
- scalekit_sdk_python-1.0.2/scalekit/v1/user_profile_attributes/user_profile_attributes_pb2.pyi +0 -99
- scalekit_sdk_python-1.0.2/scalekit/v1/user_profile_attributes/user_profile_attributes_pb2_grpc.py +0 -174
- scalekit_sdk_python-1.0.2/scalekit/v1/users/users_pb2.py +0 -97
- scalekit_sdk_python-1.0.2/scalekit/v1/workspaces/workspaces_pb2.py +0 -90
- scalekit_sdk_python-1.0.2/scalekit_sdk_python.egg-info/requires.txt +0 -10
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/LICENSE +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/README.md +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/validate/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/validate/expression_pb2.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/validate/expression_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/validate/expression_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/validate/priv/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/validate/priv/private_pb2.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/validate/priv/private_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/validate/priv/private_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/validate/validate_pb2.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/validate/validate_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/buf/validate/validate_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/common/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/common/user.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/connection.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/constants/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/constants/user.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/domain.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1 → scalekit_sdk_python-1.0.4/scalekit/utils}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/clients → scalekit_sdk_python-1.0.4/scalekit/v1}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/commons → scalekit_sdk_python-1.0.4/scalekit/v1/clients}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/clients/clients_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/connections → scalekit_sdk_python-1.0.4/scalekit/v1/commons}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/commons/commons_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/domains → scalekit_sdk_python-1.0.4/scalekit/v1/connections}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/environments → scalekit_sdk_python-1.0.4/scalekit/v1/directories}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/errdetails → scalekit_sdk_python-1.0.4/scalekit/v1/domains}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/domains/domains_pb2.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/domains/domains_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/domains/domains_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/events → scalekit_sdk_python-1.0.4/scalekit/v1/environments}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/members → scalekit_sdk_python-1.0.4/scalekit/v1/errdetails}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/errdetails/errdetails_pb2.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/errdetails/errdetails_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/errdetails/errdetails_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/options → scalekit_sdk_python-1.0.4/scalekit/v1/events}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/organizations → scalekit_sdk_python-1.0.4/scalekit/v1/login_box}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/user_profile_attributes → scalekit_sdk_python-1.0.4/scalekit/v1/members}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/members/members_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/users → scalekit_sdk_python-1.0.4/scalekit/v1/migrations}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2/scalekit/v1/workspaces → scalekit_sdk_python-1.0.4/scalekit/v1/options}/__init__.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/options/options_pb2.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/options/options_pb2.pyi +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit/v1/options/options_pb2_grpc.py +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit_sdk_python.egg-info/dependency_links.txt +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/scalekit_sdk_python.egg-info/top_level.txt +0 -0
- {scalekit_sdk_python-1.0.2 → scalekit_sdk_python-1.0.4}/setup.cfg +0 -0
{scalekit_sdk_python-1.0.2/scalekit_sdk_python.egg-info → scalekit_sdk_python-1.0.4}/PKG-INFO
RENAMED
|
@@ -1,26 +1,26 @@
|
|
|
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
|
|
7
|
-
Author-email: support@
|
|
7
|
+
Author-email: support@scalekit.com
|
|
8
8
|
License: MIT
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
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']
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
from math import floor
|
|
4
|
+
from typing import Any, Optional, Dict
|
|
5
|
+
from urllib.parse import urlencode
|
|
6
|
+
|
|
7
|
+
import jwt
|
|
8
|
+
import hmac
|
|
9
|
+
import hashlib
|
|
10
|
+
import base64
|
|
11
|
+
from datetime import datetime, timedelta, timezone
|
|
12
|
+
from scalekit.core import CoreClient
|
|
13
|
+
from scalekit.domain import DomainClient
|
|
14
|
+
from scalekit.connection import ConnectionClient
|
|
15
|
+
from scalekit.organization import OrganizationClient
|
|
16
|
+
from scalekit.directory import DirectoryClient
|
|
17
|
+
from scalekit.common.scalekit import (
|
|
18
|
+
AuthorizationUrlOptions,
|
|
19
|
+
CodeAuthenticationOptions,
|
|
20
|
+
GrantType,
|
|
21
|
+
IdpInitiatedLoginClaims,
|
|
22
|
+
)
|
|
23
|
+
from scalekit.constants.user import id_token_claim_to_user_map
|
|
24
|
+
|
|
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
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ScalekitClient:
|
|
35
|
+
""" Class definition for scalekit client """
|
|
36
|
+
|
|
37
|
+
def __init__(self, env_url: str, client_id: str, client_secret: str):
|
|
38
|
+
"""
|
|
39
|
+
Initializer for Scalekit base class
|
|
40
|
+
|
|
41
|
+
:param env_url : Environment URL
|
|
42
|
+
:type : ``` str ```
|
|
43
|
+
:param client_id : Client ID
|
|
44
|
+
:type : ``` str ```
|
|
45
|
+
:param client_secret : Client Secret
|
|
46
|
+
:type : ``` str ```
|
|
47
|
+
|
|
48
|
+
:returns:
|
|
49
|
+
None
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
self.core_client = CoreClient(
|
|
53
|
+
env_url=env_url, client_id=client_id, client_secret=client_secret
|
|
54
|
+
)
|
|
55
|
+
self.domain = DomainClient(self.core_client)
|
|
56
|
+
self.connection = ConnectionClient(self.core_client)
|
|
57
|
+
self.organization = OrganizationClient(self.core_client)
|
|
58
|
+
self.directory = DirectoryClient(self.core_client)
|
|
59
|
+
except Exception as exp:
|
|
60
|
+
raise exp
|
|
61
|
+
|
|
62
|
+
def get_authorization_url(
|
|
63
|
+
self, redirect_uri: str, options: AuthorizationUrlOptions | None
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Method to get authorization URL
|
|
67
|
+
|
|
68
|
+
:param redirect_uri : Redirect URI for SAML SSO
|
|
69
|
+
:type : ``` str ```
|
|
70
|
+
:param options : Auth URL options object
|
|
71
|
+
:type : ``` obj ```
|
|
72
|
+
|
|
73
|
+
:returns:
|
|
74
|
+
Authorization URL
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
scopes = (
|
|
78
|
+
options.scopes if options.scopes else ["openid", "profile", "email"]
|
|
79
|
+
)
|
|
80
|
+
url_params_dict = {
|
|
81
|
+
"response_type": "code",
|
|
82
|
+
"client_id": self.core_client.client_id,
|
|
83
|
+
"redirect_uri": redirect_uri,
|
|
84
|
+
"scope": " ".join(scopes),
|
|
85
|
+
"state": options.state,
|
|
86
|
+
"nonce": options.nonce,
|
|
87
|
+
"login_hint": options.login_hint,
|
|
88
|
+
"domain_hint": options.domain_hint,
|
|
89
|
+
"connection_id": options.connection_id,
|
|
90
|
+
"organization_id": options.organization_id,
|
|
91
|
+
"provider": options.provider,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
valid_auth_params = {k: v for k, v in url_params_dict.items() if v}
|
|
95
|
+
query_string = urlencode(valid_auth_params)
|
|
96
|
+
|
|
97
|
+
return f"{self.core_client.env_url}/{AUTHORIZE_ENDPOINT}?{query_string}"
|
|
98
|
+
except Exception as exp:
|
|
99
|
+
raise exp
|
|
100
|
+
|
|
101
|
+
def authenticate_with_code(
|
|
102
|
+
self, code, redirect_uri, options: CodeAuthenticationOptions
|
|
103
|
+
):
|
|
104
|
+
"""
|
|
105
|
+
Method to authenticate with code options
|
|
106
|
+
|
|
107
|
+
:param code : authorization_code
|
|
108
|
+
:type : ``` str ```
|
|
109
|
+
:param redirect_uri : Redirect URI
|
|
110
|
+
:type : ``` str ```
|
|
111
|
+
:param options : CodeAuthenticationOptions Object
|
|
112
|
+
:type : ``` obj ```
|
|
113
|
+
|
|
114
|
+
:returns:
|
|
115
|
+
dict with user, id token & access token
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
response = self.core_client.authenticate(
|
|
119
|
+
json.dumps(
|
|
120
|
+
{
|
|
121
|
+
"code": code,
|
|
122
|
+
"redirect_uri": redirect_uri,
|
|
123
|
+
"grant_type": GrantType.AuthorizationCode.value,
|
|
124
|
+
"client_id": self.core_client.client_id,
|
|
125
|
+
"client_secret": self.core_client.client_secret,
|
|
126
|
+
"code_verifier": options.code_verifier,
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
response = json.loads(response.content)
|
|
131
|
+
id_token = response["id_token"]
|
|
132
|
+
access_token = response["access_token"]
|
|
133
|
+
# Validate id_token
|
|
134
|
+
claims = self.__validate_token(id_token, {"verify_aud": False})
|
|
135
|
+
user = {}
|
|
136
|
+
for k, v in claims.items():
|
|
137
|
+
if id_token_claim_to_user_map.get(k, None):
|
|
138
|
+
user[id_token_claim_to_user_map[k]] = v
|
|
139
|
+
|
|
140
|
+
return {"user": user, "id_token": id_token, "access_token": access_token}
|
|
141
|
+
|
|
142
|
+
except Exception as exp:
|
|
143
|
+
raise exp
|
|
144
|
+
|
|
145
|
+
def validate_access_token(self, token: str) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
Method to validate access token
|
|
148
|
+
|
|
149
|
+
:param token : access token
|
|
150
|
+
:type : ``` str ```
|
|
151
|
+
|
|
152
|
+
:returns:
|
|
153
|
+
bool
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
self.__validate_token(token)
|
|
157
|
+
return True
|
|
158
|
+
except jwt.exceptions.InvalidTokenError:
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
def get_idp_initiated_login_claims(self, idp_initiated_login_token: str) -> IdpInitiatedLoginClaims:
|
|
162
|
+
"""
|
|
163
|
+
Method to get IDP initiated login claims
|
|
164
|
+
|
|
165
|
+
:param idp_initiated_login_token : IDP initiated login token
|
|
166
|
+
:type : ``` str ```
|
|
167
|
+
|
|
168
|
+
:returns:
|
|
169
|
+
``` IdpInitiatedLoginClaims ```
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
claims = self.__validate_token(idp_initiated_login_token, {"verify_aud": False})
|
|
173
|
+
return claims
|
|
174
|
+
except Exception as exp:
|
|
175
|
+
raise exp
|
|
176
|
+
|
|
177
|
+
def __validate_token(
|
|
178
|
+
self, token: str, options: Optional[Dict] = None
|
|
179
|
+
) -> Dict[str, Any]:
|
|
180
|
+
"""
|
|
181
|
+
Method to validate token
|
|
182
|
+
|
|
183
|
+
:param token : token
|
|
184
|
+
:type : ``` str ```
|
|
185
|
+
|
|
186
|
+
:returns:
|
|
187
|
+
payload
|
|
188
|
+
"""
|
|
189
|
+
self.core_client.get_jwks()
|
|
190
|
+
kid = jwt.get_unverified_header(token)["kid"]
|
|
191
|
+
key = self.core_client.keys[kid]
|
|
192
|
+
|
|
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')}"
|
|
@@ -48,3 +48,13 @@ class AuthenticationOptions:
|
|
|
48
48
|
def __init__(self):
|
|
49
49
|
""" """
|
|
50
50
|
self.refresh_token: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class IdpInitiatedLoginClaims:
|
|
54
|
+
"""Class definition for IDP Initiated Login Claims"""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
self.connection_id: str
|
|
58
|
+
self.organization_id: str
|
|
59
|
+
self.login_hint: str
|
|
60
|
+
self.relay_state: Optional[str] = None
|
|
@@ -25,7 +25,7 @@ class WithCall(Protocol):
|
|
|
25
25
|
class CoreClient:
|
|
26
26
|
"""Class definition for Core Client"""
|
|
27
27
|
|
|
28
|
-
sdk_version = "Scalekit-Python/1.0.
|
|
28
|
+
sdk_version = "Scalekit-Python/1.0.3"
|
|
29
29
|
api_version = "20240430"
|
|
30
30
|
user_agent = f"{sdk_version} Python/{platform.python_version()} ({platform.system()}; {platform.architecture()}"
|
|
31
31
|
|
|
@@ -83,6 +83,8 @@ class CoreClient:
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
response = self.authenticate(data=json.dumps(params))
|
|
86
|
+
if response.status_code != 200:
|
|
87
|
+
raise Exception(response.content)
|
|
86
88
|
response = json.loads(response.content)
|
|
87
89
|
self.access_token = response["access_token"]
|
|
88
90
|
|
|
@@ -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
|
+
)
|