workos 1.5.1__py3-none-any.whl → 5.38.0__py3-none-any.whl
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.
- workos/__about__.py +1 -1
- workos/__init__.py +3 -7
- workos/_base_client.py +138 -0
- workos/_client_configuration.py +10 -0
- workos/api_keys.py +53 -0
- workos/async_client.py +144 -0
- workos/audit_logs.py +125 -0
- workos/client.py +110 -18
- workos/directory_sync.py +379 -99
- workos/events.py +111 -0
- workos/exceptions.py +53 -26
- workos/fga.py +649 -0
- workos/mfa.py +205 -0
- workos/organization_domains.py +179 -0
- workos/organizations.py +403 -73
- workos/passwordless.py +67 -43
- workos/pipes.py +93 -0
- workos/portal.py +51 -28
- workos/session.py +337 -0
- workos/sso.py +311 -101
- workos/types/__init__.py +4 -0
- workos/types/api_keys/__init__.py +1 -0
- workos/types/api_keys/api_keys.py +20 -0
- workos/types/audit_logs/__init__.py +6 -0
- workos/types/audit_logs/audit_log_event.py +16 -0
- workos/types/audit_logs/audit_log_event_actor.py +12 -0
- workos/types/audit_logs/audit_log_event_context.py +8 -0
- workos/types/audit_logs/audit_log_event_target.py +12 -0
- workos/types/audit_logs/audit_log_export.py +18 -0
- workos/types/audit_logs/audit_log_metadata.py +4 -0
- workos/types/directory_sync/__init__.py +5 -0
- workos/types/directory_sync/directory.py +31 -0
- workos/types/directory_sync/directory_group.py +16 -0
- workos/types/directory_sync/directory_state.py +28 -0
- workos/types/directory_sync/directory_type.py +24 -0
- workos/types/directory_sync/directory_user.py +50 -0
- workos/types/directory_sync/list_filters.py +21 -0
- workos/types/events/__init__.py +13 -0
- workos/types/events/authentication_payload.py +70 -0
- workos/types/events/connection_payload_with_legacy_fields.py +5 -0
- workos/types/events/directory_group_membership_payload.py +9 -0
- workos/types/events/directory_group_with_previous_attributes.py +6 -0
- workos/types/events/directory_payload.py +16 -0
- workos/types/events/directory_payload_with_legacy_fields.py +29 -0
- workos/types/events/directory_user_with_previous_attributes.py +6 -0
- workos/types/events/event.py +324 -0
- workos/types/events/event_model.py +103 -0
- workos/types/events/event_type.py +59 -0
- workos/types/events/list_filters.py +10 -0
- workos/types/events/organization_domain_verification_failed_payload.py +14 -0
- workos/types/events/previous_attributes.py +3 -0
- workos/types/events/session_payload.py +27 -0
- workos/types/feature_flags/__init__.py +3 -0
- workos/types/feature_flags/feature_flag.py +12 -0
- workos/types/feature_flags/list_filters.py +5 -0
- workos/types/fga/__init__.py +5 -0
- workos/types/fga/authorization_resource_types.py +9 -0
- workos/types/fga/authorization_resources.py +10 -0
- workos/types/fga/check.py +51 -0
- workos/types/fga/list_filters.py +24 -0
- workos/types/fga/warnings.py +33 -0
- workos/types/fga/warrant.py +49 -0
- workos/types/list_resource.py +198 -0
- workos/types/metadata.py +4 -0
- workos/types/mfa/__init__.py +5 -0
- workos/types/mfa/authentication_challenge.py +14 -0
- workos/types/mfa/authentication_challenge_verification_response.py +9 -0
- workos/types/mfa/authentication_factor.py +70 -0
- workos/types/mfa/authentication_factor_totp_and_challenge_response.py +10 -0
- workos/types/mfa/enroll_authentication_factor_type.py +8 -0
- workos/types/organization_domains/__init__.py +1 -0
- workos/types/organization_domains/organization_domain.py +18 -0
- workos/types/organizations/__init__.py +6 -0
- workos/types/organizations/domain_data_input.py +7 -0
- workos/types/organizations/list_filters.py +6 -0
- workos/types/organizations/organization.py +13 -0
- workos/types/organizations/organization_common.py +12 -0
- workos/types/passwordless/__init__.py +2 -0
- workos/types/passwordless/passwordless_session.py +12 -0
- workos/types/passwordless/passwordless_session_type.py +3 -0
- workos/types/pipes/__init__.py +6 -0
- workos/types/pipes/pipes.py +34 -0
- workos/types/portal/__init__.py +2 -0
- workos/types/portal/portal_link.py +7 -0
- workos/types/portal/portal_link_intent.py +11 -0
- workos/types/portal/portal_link_intent_options.py +9 -0
- workos/types/roles/__init__.py +0 -0
- workos/types/roles/role.py +27 -0
- workos/types/sso/__init__.py +4 -0
- workos/types/sso/connection.py +70 -0
- workos/types/sso/connection_domain.py +8 -0
- workos/types/sso/profile.py +35 -0
- workos/types/sso/sso_provider_type.py +10 -0
- workos/types/user_management/__init__.py +12 -0
- workos/types/user_management/authenticate_with_common.py +66 -0
- workos/types/user_management/authentication_response.py +53 -0
- workos/types/user_management/email_verification.py +18 -0
- workos/types/user_management/impersonator.py +8 -0
- workos/types/user_management/invitation.py +26 -0
- workos/types/user_management/list_filters.py +29 -0
- workos/types/user_management/magic_auth.py +18 -0
- workos/types/user_management/oauth_tokens.py +21 -0
- workos/types/user_management/organization_membership.py +25 -0
- workos/types/user_management/password_hash_type.py +4 -0
- workos/types/user_management/password_reset.py +18 -0
- workos/types/user_management/screen_hint.py +3 -0
- workos/types/user_management/session.py +79 -0
- workos/types/user_management/user.py +22 -0
- workos/types/user_management/user_management_provider_type.py +11 -0
- workos/types/vault/__init__.py +2 -0
- workos/types/vault/key.py +25 -0
- workos/types/vault/object.py +38 -0
- workos/types/webhooks/__init__.py +0 -0
- workos/types/webhooks/webhook.py +330 -0
- workos/types/webhooks/webhook_model.py +14 -0
- workos/types/webhooks/webhook_payload.py +4 -0
- workos/types/widgets/__init__.py +2 -0
- workos/types/widgets/widget_scope.py +4 -0
- workos/types/widgets/widget_token_response.py +7 -0
- workos/types/workos_model.py +26 -0
- workos/typing/__init__.py +1 -0
- workos/typing/literals.py +32 -0
- workos/typing/sync_or_async.py +5 -0
- workos/typing/untyped_literal.py +37 -0
- workos/typing/webhooks.py +18 -0
- workos/user_management.py +2400 -0
- workos/utils/_base_http_client.py +252 -0
- workos/utils/crypto_provider.py +39 -0
- workos/utils/http_client.py +214 -0
- workos/utils/pagination_order.py +4 -0
- workos/utils/request_helper.py +27 -0
- workos/vault.py +544 -0
- workos/webhooks.py +96 -39
- workos/widgets.py +55 -0
- {workos-1.5.1.dist-info → workos-5.38.0.dist-info}/LICENSE +1 -1
- workos-5.38.0.dist-info/METADATA +107 -0
- workos-5.38.0.dist-info/RECORD +141 -0
- {workos-1.5.1.dist-info → workos-5.38.0.dist-info}/WHEEL +1 -1
- workos/audit_trail.py +0 -172
- workos/resources/base.py +0 -36
- workos/resources/event.py +0 -42
- workos/resources/event_action.py +0 -11
- workos/resources/sso.py +0 -53
- workos/utils/connection_types.py +0 -17
- workos/utils/request.py +0 -95
- workos/utils/validation.py +0 -45
- workos-1.5.1.dist-info/METADATA +0 -77
- workos-1.5.1.dist-info/RECORD +0 -25
- /workos/{resources/__init__.py → py.typed} +0 -0
- {workos-1.5.1.dist-info → workos-5.38.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import platform
|
|
2
|
+
from typing import (
|
|
3
|
+
Any,
|
|
4
|
+
Mapping,
|
|
5
|
+
Sequence,
|
|
6
|
+
cast,
|
|
7
|
+
Dict,
|
|
8
|
+
Generic,
|
|
9
|
+
Optional,
|
|
10
|
+
TypeVar,
|
|
11
|
+
Union,
|
|
12
|
+
)
|
|
13
|
+
from typing_extensions import NotRequired, TypedDict
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from httpx._types import QueryParamTypes
|
|
17
|
+
|
|
18
|
+
from workos.exceptions import (
|
|
19
|
+
ConflictException,
|
|
20
|
+
ServerException,
|
|
21
|
+
AuthenticationException,
|
|
22
|
+
AuthorizationException,
|
|
23
|
+
EmailVerificationRequiredException,
|
|
24
|
+
NotFoundException,
|
|
25
|
+
BadRequestException,
|
|
26
|
+
)
|
|
27
|
+
from workos.utils.request_helper import REQUEST_METHOD_DELETE, REQUEST_METHOD_GET
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
DEFAULT_REQUEST_TIMEOUT = 25
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ParamsType = Optional[Mapping[str, Any]]
|
|
37
|
+
HeadersType = Optional[Dict[str, str]]
|
|
38
|
+
JsonType = Optional[Union[Mapping[str, Any], Sequence[Any]]]
|
|
39
|
+
ResponseJson = Mapping[Any, Any]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class PreparedRequest(TypedDict):
|
|
43
|
+
method: str
|
|
44
|
+
url: str
|
|
45
|
+
headers: httpx.Headers
|
|
46
|
+
params: NotRequired[Optional[QueryParamTypes]]
|
|
47
|
+
json: NotRequired[JsonType]
|
|
48
|
+
timeout: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BaseHTTPClient(Generic[_HttpxClientT]):
|
|
52
|
+
_client: _HttpxClientT
|
|
53
|
+
|
|
54
|
+
_api_key: str
|
|
55
|
+
_client_id: str
|
|
56
|
+
_base_url: str
|
|
57
|
+
_version: str
|
|
58
|
+
_timeout: int
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
api_key: str,
|
|
64
|
+
base_url: str,
|
|
65
|
+
client_id: str,
|
|
66
|
+
version: str,
|
|
67
|
+
timeout: Optional[int] = DEFAULT_REQUEST_TIMEOUT,
|
|
68
|
+
) -> None:
|
|
69
|
+
self._api_key = api_key
|
|
70
|
+
self._base_url = base_url
|
|
71
|
+
self._client_id = client_id
|
|
72
|
+
self._version = version
|
|
73
|
+
self._timeout = DEFAULT_REQUEST_TIMEOUT if timeout is None else timeout
|
|
74
|
+
|
|
75
|
+
def _generate_api_url(self, path: str) -> str:
|
|
76
|
+
return f"{self._base_url}{path}"
|
|
77
|
+
|
|
78
|
+
def _build_headers(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
custom_headers: Union[HeadersType, None],
|
|
82
|
+
exclude_default_auth_headers: bool = False,
|
|
83
|
+
) -> httpx.Headers:
|
|
84
|
+
if custom_headers is None:
|
|
85
|
+
custom_headers = {}
|
|
86
|
+
|
|
87
|
+
default_headers = {
|
|
88
|
+
**self.default_headers,
|
|
89
|
+
**({} if exclude_default_auth_headers else self.auth_headers),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# httpx.Headers is case-insensitive while dictionaries are not.
|
|
93
|
+
return httpx.Headers({**default_headers, **custom_headers})
|
|
94
|
+
|
|
95
|
+
def _maybe_raise_error_by_status_code(
|
|
96
|
+
self, response: httpx.Response, response_json: Union[ResponseJson, None]
|
|
97
|
+
) -> None:
|
|
98
|
+
status_code = response.status_code
|
|
99
|
+
if status_code >= 400 and status_code < 500:
|
|
100
|
+
if status_code == 401:
|
|
101
|
+
raise AuthenticationException(response, response_json)
|
|
102
|
+
elif status_code == 403:
|
|
103
|
+
if (
|
|
104
|
+
response_json is not None
|
|
105
|
+
and response_json.get("code") == "email_verification_required"
|
|
106
|
+
):
|
|
107
|
+
raise EmailVerificationRequiredException(response, response_json)
|
|
108
|
+
raise AuthorizationException(response, response_json)
|
|
109
|
+
elif status_code == 404:
|
|
110
|
+
raise NotFoundException(response, response_json)
|
|
111
|
+
elif status_code == 409:
|
|
112
|
+
raise ConflictException(response, response_json)
|
|
113
|
+
|
|
114
|
+
raise BadRequestException(response, response_json)
|
|
115
|
+
elif status_code >= 500 and status_code < 600:
|
|
116
|
+
raise ServerException(response, response_json)
|
|
117
|
+
|
|
118
|
+
def _prepare_request(
|
|
119
|
+
self,
|
|
120
|
+
path: str,
|
|
121
|
+
method: Optional[str] = REQUEST_METHOD_GET,
|
|
122
|
+
params: ParamsType = None,
|
|
123
|
+
json: JsonType = None,
|
|
124
|
+
headers: HeadersType = None,
|
|
125
|
+
exclude_default_auth_headers: bool = False,
|
|
126
|
+
) -> PreparedRequest:
|
|
127
|
+
"""Executes a request against the WorkOS API.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
path (str): Path for the api request that'd be appended to the base API URL
|
|
131
|
+
|
|
132
|
+
Kwargs:
|
|
133
|
+
method Optional[str]: One of the supported methods as defined by the REQUEST_METHOD_X constants
|
|
134
|
+
params Optional[dict]: Query params or body payload to be added to the request
|
|
135
|
+
headers Optional[dict]: Custom headers to be added to the request
|
|
136
|
+
token Optional[str]: Bearer token
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
dict: Response from WorkOS
|
|
140
|
+
"""
|
|
141
|
+
url = self._generate_api_url(path)
|
|
142
|
+
parsed_headers = self._build_headers(
|
|
143
|
+
custom_headers=headers,
|
|
144
|
+
exclude_default_auth_headers=exclude_default_auth_headers,
|
|
145
|
+
)
|
|
146
|
+
parsed_method = REQUEST_METHOD_GET if method is None else method
|
|
147
|
+
bodyless_http_method = parsed_method.lower() in [
|
|
148
|
+
REQUEST_METHOD_DELETE,
|
|
149
|
+
REQUEST_METHOD_GET,
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
if bodyless_http_method and json is not None:
|
|
153
|
+
raise ValueError(f"Cannot send a body with a {parsed_method} request")
|
|
154
|
+
|
|
155
|
+
# Remove any parameters that are None
|
|
156
|
+
if params is not None:
|
|
157
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
158
|
+
|
|
159
|
+
# Remove any body values that are None
|
|
160
|
+
if json is not None and isinstance(json, Mapping):
|
|
161
|
+
json = {k: v for k, v in json.items() if v is not None}
|
|
162
|
+
|
|
163
|
+
# We'll spread these return values onto the HTTP client request method
|
|
164
|
+
if bodyless_http_method:
|
|
165
|
+
return {
|
|
166
|
+
"method": parsed_method,
|
|
167
|
+
"url": url,
|
|
168
|
+
"headers": parsed_headers,
|
|
169
|
+
"params": params,
|
|
170
|
+
"timeout": self.timeout,
|
|
171
|
+
}
|
|
172
|
+
else:
|
|
173
|
+
return {
|
|
174
|
+
"method": parsed_method,
|
|
175
|
+
"url": url,
|
|
176
|
+
"headers": parsed_headers,
|
|
177
|
+
"params": params,
|
|
178
|
+
"json": json,
|
|
179
|
+
"timeout": self.timeout,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
def _handle_response(self, response: httpx.Response) -> ResponseJson:
|
|
183
|
+
response_json = None
|
|
184
|
+
content_type = (
|
|
185
|
+
response.headers.get("content-type")
|
|
186
|
+
if response.headers is not None
|
|
187
|
+
else None
|
|
188
|
+
)
|
|
189
|
+
if content_type is not None and "application/json" in content_type:
|
|
190
|
+
try:
|
|
191
|
+
response_json = response.json()
|
|
192
|
+
except ValueError:
|
|
193
|
+
raise ServerException(response, None)
|
|
194
|
+
|
|
195
|
+
self._maybe_raise_error_by_status_code(response, response_json)
|
|
196
|
+
|
|
197
|
+
return cast(ResponseJson, response_json)
|
|
198
|
+
|
|
199
|
+
def build_request_url(
|
|
200
|
+
self,
|
|
201
|
+
url: str,
|
|
202
|
+
method: Optional[str] = REQUEST_METHOD_GET,
|
|
203
|
+
params: Optional[QueryParamTypes] = None,
|
|
204
|
+
) -> str:
|
|
205
|
+
return self._client.build_request(
|
|
206
|
+
method=method or REQUEST_METHOD_GET, url=url, params=params
|
|
207
|
+
).url.__str__()
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def api_key(self) -> str:
|
|
211
|
+
return self._api_key
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def base_url(self) -> str:
|
|
215
|
+
return self._base_url
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def client_id(self) -> str:
|
|
219
|
+
return self._client_id
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def auth_headers(self) -> Mapping[str, str]:
|
|
223
|
+
return self.auth_header_from_token(self._api_key)
|
|
224
|
+
|
|
225
|
+
def auth_header_from_token(self, token: str) -> Mapping[str, str]:
|
|
226
|
+
return {
|
|
227
|
+
"Authorization": f"Bearer {token}",
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def default_headers(self) -> Dict[str, str]:
|
|
232
|
+
return {
|
|
233
|
+
"Accept": "application/json",
|
|
234
|
+
"Content-Type": "application/json",
|
|
235
|
+
"User-Agent": self.user_agent,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def user_agent(self) -> str:
|
|
240
|
+
# TODO: Include sync/async in user agent
|
|
241
|
+
return "WorkOS Python/{} Python SDK/{}".format(
|
|
242
|
+
platform.python_version(),
|
|
243
|
+
self._version,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def timeout(self) -> int:
|
|
248
|
+
return self._timeout
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def version(self) -> str:
|
|
252
|
+
return self._version
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional, Dict
|
|
3
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
4
|
+
from cryptography.hazmat.backends import default_backend
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CryptoProvider:
|
|
8
|
+
def encrypt(
|
|
9
|
+
self, plaintext: bytes, key: bytes, iv: bytes, aad: Optional[bytes]
|
|
10
|
+
) -> Dict[str, bytes]:
|
|
11
|
+
encryptor = Cipher(
|
|
12
|
+
algorithms.AES(key), modes.GCM(iv), backend=default_backend()
|
|
13
|
+
).encryptor()
|
|
14
|
+
|
|
15
|
+
if aad:
|
|
16
|
+
encryptor.authenticate_additional_data(aad)
|
|
17
|
+
|
|
18
|
+
ciphertext = encryptor.update(plaintext) + encryptor.finalize()
|
|
19
|
+
return {"ciphertext": ciphertext, "iv": iv, "tag": encryptor.tag}
|
|
20
|
+
|
|
21
|
+
def decrypt(
|
|
22
|
+
self,
|
|
23
|
+
ciphertext: bytes,
|
|
24
|
+
key: bytes,
|
|
25
|
+
iv: bytes,
|
|
26
|
+
tag: bytes,
|
|
27
|
+
aad: Optional[bytes] = None,
|
|
28
|
+
) -> bytes:
|
|
29
|
+
decryptor = Cipher(
|
|
30
|
+
algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend()
|
|
31
|
+
).decryptor()
|
|
32
|
+
|
|
33
|
+
if aad:
|
|
34
|
+
decryptor.authenticate_additional_data(aad)
|
|
35
|
+
|
|
36
|
+
return decryptor.update(ciphertext) + decryptor.finalize()
|
|
37
|
+
|
|
38
|
+
def random_bytes(self, n: int) -> bytes:
|
|
39
|
+
return os.urandom(n)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from types import TracebackType
|
|
3
|
+
from typing import Optional, Type, Union
|
|
4
|
+
|
|
5
|
+
# Self was added to typing in Python 3.11
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from workos.utils._base_http_client import (
|
|
11
|
+
BaseHTTPClient,
|
|
12
|
+
HeadersType,
|
|
13
|
+
JsonType,
|
|
14
|
+
ParamsType,
|
|
15
|
+
ResponseJson,
|
|
16
|
+
)
|
|
17
|
+
from workos.utils.request_helper import REQUEST_METHOD_GET
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SyncHttpxClientWrapper(httpx.Client):
|
|
21
|
+
def __del__(self) -> None:
|
|
22
|
+
try:
|
|
23
|
+
self.close()
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SyncHTTPClient(BaseHTTPClient[httpx.Client]):
|
|
29
|
+
"""Sync HTTP Client for a convenient way to access the WorkOS feature set."""
|
|
30
|
+
|
|
31
|
+
_client: httpx.Client
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
api_key: str,
|
|
37
|
+
base_url: str,
|
|
38
|
+
client_id: str,
|
|
39
|
+
version: str,
|
|
40
|
+
timeout: Optional[int] = None,
|
|
41
|
+
# If no custom transport is provided, let httpx use the default
|
|
42
|
+
# so we don't overwrite environment configurations like proxies
|
|
43
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
super().__init__(
|
|
46
|
+
api_key=api_key,
|
|
47
|
+
base_url=base_url,
|
|
48
|
+
client_id=client_id,
|
|
49
|
+
version=version,
|
|
50
|
+
timeout=timeout,
|
|
51
|
+
)
|
|
52
|
+
self._client = SyncHttpxClientWrapper(
|
|
53
|
+
base_url=base_url,
|
|
54
|
+
timeout=timeout,
|
|
55
|
+
follow_redirects=True,
|
|
56
|
+
transport=transport,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def is_closed(self) -> bool:
|
|
60
|
+
return self._client.is_closed
|
|
61
|
+
|
|
62
|
+
def close(self) -> None:
|
|
63
|
+
"""Close the underlying HTTPX client.
|
|
64
|
+
|
|
65
|
+
The client will *not* be usable after this.
|
|
66
|
+
"""
|
|
67
|
+
# If an error is thrown while constructing a client, self._client
|
|
68
|
+
# may not be present
|
|
69
|
+
if hasattr(self, "_client"):
|
|
70
|
+
self._client.close()
|
|
71
|
+
|
|
72
|
+
def __enter__(self) -> Self:
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __exit__(
|
|
76
|
+
self,
|
|
77
|
+
exc_type: Optional[Type[BaseException]],
|
|
78
|
+
exc: Optional[BaseException],
|
|
79
|
+
exc_tb: Optional[TracebackType],
|
|
80
|
+
) -> None:
|
|
81
|
+
self.close()
|
|
82
|
+
|
|
83
|
+
def request(
|
|
84
|
+
self,
|
|
85
|
+
path: str,
|
|
86
|
+
method: Optional[str] = REQUEST_METHOD_GET,
|
|
87
|
+
params: ParamsType = None,
|
|
88
|
+
json: JsonType = None,
|
|
89
|
+
headers: HeadersType = None,
|
|
90
|
+
exclude_default_auth_headers: bool = False,
|
|
91
|
+
) -> ResponseJson:
|
|
92
|
+
"""Executes a request against the WorkOS API.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
path (str): Path for the api request that'd be appended to the base API URL
|
|
96
|
+
|
|
97
|
+
Kwargs:
|
|
98
|
+
method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants
|
|
99
|
+
params (ParamsType): Query params to be added to the request
|
|
100
|
+
json (JsonType): Body payload to be added to the request
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
ResponseJson: Response from WorkOS
|
|
104
|
+
"""
|
|
105
|
+
prepared_request_parameters = self._prepare_request(
|
|
106
|
+
path=path,
|
|
107
|
+
method=method,
|
|
108
|
+
params=params,
|
|
109
|
+
json=json,
|
|
110
|
+
headers=headers,
|
|
111
|
+
exclude_default_auth_headers=exclude_default_auth_headers,
|
|
112
|
+
)
|
|
113
|
+
response = self._client.request(**prepared_request_parameters)
|
|
114
|
+
return self._handle_response(response)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class AsyncHttpxClientWrapper(httpx.AsyncClient):
|
|
118
|
+
def __del__(self) -> None:
|
|
119
|
+
try:
|
|
120
|
+
asyncio.get_running_loop().create_task(self.aclose())
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class AsyncHTTPClient(BaseHTTPClient[httpx.AsyncClient]):
|
|
126
|
+
"""Async HTTP Client for a convenient way to access the WorkOS feature set."""
|
|
127
|
+
|
|
128
|
+
_client: httpx.AsyncClient
|
|
129
|
+
|
|
130
|
+
_api_key: str
|
|
131
|
+
_client_id: str
|
|
132
|
+
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
*,
|
|
136
|
+
base_url: str,
|
|
137
|
+
api_key: str,
|
|
138
|
+
client_id: str,
|
|
139
|
+
version: str,
|
|
140
|
+
timeout: Optional[int] = None,
|
|
141
|
+
# If no custom transport is provided, let httpx use the default
|
|
142
|
+
# so we don't overwrite environment configurations like proxies
|
|
143
|
+
transport: Optional[httpx.AsyncBaseTransport] = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
super().__init__(
|
|
146
|
+
base_url=base_url,
|
|
147
|
+
api_key=api_key,
|
|
148
|
+
client_id=client_id,
|
|
149
|
+
version=version,
|
|
150
|
+
timeout=timeout,
|
|
151
|
+
)
|
|
152
|
+
self._client = AsyncHttpxClientWrapper(
|
|
153
|
+
base_url=base_url,
|
|
154
|
+
timeout=timeout,
|
|
155
|
+
follow_redirects=True,
|
|
156
|
+
transport=transport,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def is_closed(self) -> bool:
|
|
160
|
+
return self._client.is_closed
|
|
161
|
+
|
|
162
|
+
async def close(self) -> None:
|
|
163
|
+
"""Close the underlying HTTPX client.
|
|
164
|
+
|
|
165
|
+
The client will *not* be usable after this.
|
|
166
|
+
"""
|
|
167
|
+
await self._client.aclose()
|
|
168
|
+
|
|
169
|
+
async def __aenter__(self) -> Self:
|
|
170
|
+
return self
|
|
171
|
+
|
|
172
|
+
async def __aexit__(
|
|
173
|
+
self,
|
|
174
|
+
exc_type: Optional[Type[BaseException]],
|
|
175
|
+
exc: Optional[BaseException],
|
|
176
|
+
exc_tb: Optional[TracebackType],
|
|
177
|
+
) -> None:
|
|
178
|
+
await self.close()
|
|
179
|
+
|
|
180
|
+
async def request(
|
|
181
|
+
self,
|
|
182
|
+
path: str,
|
|
183
|
+
method: Optional[str] = REQUEST_METHOD_GET,
|
|
184
|
+
params: ParamsType = None,
|
|
185
|
+
json: JsonType = None,
|
|
186
|
+
headers: HeadersType = None,
|
|
187
|
+
exclude_default_auth_headers: bool = False,
|
|
188
|
+
) -> ResponseJson:
|
|
189
|
+
"""Executes a request against the WorkOS API.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
path (str): Path for the api request that'd be appended to the base API URL
|
|
193
|
+
|
|
194
|
+
Kwargs:
|
|
195
|
+
method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants
|
|
196
|
+
params (ParamsType): Query params to be added to the request
|
|
197
|
+
json (JsonType): Body payload to be added to the request
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
ResponseJson: Response from WorkOS
|
|
201
|
+
"""
|
|
202
|
+
prepared_request_parameters = self._prepare_request(
|
|
203
|
+
path=path,
|
|
204
|
+
method=method,
|
|
205
|
+
params=params,
|
|
206
|
+
json=json,
|
|
207
|
+
headers=headers,
|
|
208
|
+
exclude_default_auth_headers=exclude_default_auth_headers,
|
|
209
|
+
)
|
|
210
|
+
response = await self._client.request(**prepared_request_parameters)
|
|
211
|
+
return self._handle_response(response)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
HTTPClient = Union[AsyncHTTPClient, SyncHTTPClient]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from typing import Dict, Union
|
|
2
|
+
import urllib.parse
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
DEFAULT_LIST_RESPONSE_LIMIT = 10
|
|
6
|
+
RESPONSE_TYPE_CODE = "code"
|
|
7
|
+
REQUEST_METHOD_DELETE = "delete"
|
|
8
|
+
REQUEST_METHOD_GET = "get"
|
|
9
|
+
REQUEST_METHOD_POST = "post"
|
|
10
|
+
REQUEST_METHOD_PUT = "put"
|
|
11
|
+
|
|
12
|
+
QueryParameterValue = Union[str, int, bool, None]
|
|
13
|
+
QueryParameters = Dict[str, QueryParameterValue]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RequestHelper:
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def build_parameterized_url(cls, url: str, **params: QueryParameterValue) -> str:
|
|
20
|
+
escaped_params = {k: urllib.parse.quote(str(v)) for k, v in params.items()}
|
|
21
|
+
return url.format(**escaped_params)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def build_url_with_query_params(
|
|
25
|
+
cls, base_url: str, path: str, **params: QueryParameterValue
|
|
26
|
+
) -> str:
|
|
27
|
+
return base_url + path + "?" + urllib.parse.urlencode(params)
|