UncountablePythonSDK 0.0.24__py3-none-any.whl → 0.0.25__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.
Potentially problematic release.
This version of UncountablePythonSDK might be problematic. Click here for more details.
- {UncountablePythonSDK-0.0.24.dist-info → UncountablePythonSDK-0.0.25.dist-info}/METADATA +1 -1
- {UncountablePythonSDK-0.0.24.dist-info → UncountablePythonSDK-0.0.25.dist-info}/RECORD +8 -8
- examples/create_entity.py +4 -4
- uncountable/core/__init__.py +3 -2
- uncountable/core/client.py +83 -20
- uncountable/core/types.py +7 -13
- {UncountablePythonSDK-0.0.24.dist-info → UncountablePythonSDK-0.0.25.dist-info}/WHEEL +0 -0
- {UncountablePythonSDK-0.0.24.dist-info → UncountablePythonSDK-0.0.25.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: UncountablePythonSDK
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.25
|
|
4
4
|
Summary: Uncountable SDK
|
|
5
5
|
Project-URL: Homepage, https://github.com/uncountableinc/uncountable-python-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/uncountableinc/uncountable-python-sdk.git
|
|
@@ -15,7 +15,7 @@ docs/static/favicons/manifest.json,sha256=6q_3nZkcg_x0xut4eE-xpdeMY1TydwiZIcbXlL
|
|
|
15
15
|
docs/static/favicons/mstile-150x150.png,sha256=eAK4QdEofhdLtfmjuPTpnX3MJqYnvGXsHYUjlcQekyY,1035
|
|
16
16
|
docs/static/favicons/safari-pinned-tab.svg,sha256=S84fRnz0ZxLnQrKtmmFZytiRyu1xLtMR_RVy5jmwU7k,1926
|
|
17
17
|
examples/async_batch.py,sha256=wpf_3P547375vTIO4pKv5vw6WCkUnzqvw_S3idfhjvM,1122
|
|
18
|
-
examples/create_entity.py,sha256=
|
|
18
|
+
examples/create_entity.py,sha256=2ciY0Cy4McGmfyElFF19OyUrbAa7jOEG8PQfR7Hq6NI,636
|
|
19
19
|
examples/upload_files.py,sha256=ZsMChgOioraVHv207YREpivAOf4dq3IxGIBoROoDX_4,482
|
|
20
20
|
examples/recipe-import/importer.py,sha256=baD71xuNibxDTe3bGHsMEIZEf9Xtb-IumBNpCEV0RZU,1134
|
|
21
21
|
pkgs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -105,11 +105,11 @@ type_spec/external/api/recipes/set_recipe_tags.yaml,sha256=IrdkbryxZjNy8n4aMNLRT
|
|
|
105
105
|
type_spec/external/api/triggers/run_trigger.yaml,sha256=c8xDV3bQRjcRRDG4Y7kdQmMMu1fj3ae5eUi-Sdbsi54,405
|
|
106
106
|
uncountable/__init__.py,sha256=281cC2hs8pbrD0jVKMol-tbWSh7Zcsc8oRT42dKteyE,102
|
|
107
107
|
uncountable/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
108
|
-
uncountable/core/__init__.py,sha256=
|
|
108
|
+
uncountable/core/__init__.py,sha256=J0CeeztqyJe7klvHM-8fwSivN1sud6xZThOdaThnQrU,314
|
|
109
109
|
uncountable/core/async_batch.py,sha256=0cRmCr6Z9sNxZyfY9Dl8wlCA4anISVZuHGgBegHhUbc,749
|
|
110
|
-
uncountable/core/client.py,sha256=
|
|
110
|
+
uncountable/core/client.py,sha256=cP6yEHv5KGE2NJn0BAQh470mirBfITMLsfEs-zj-6Ys,9024
|
|
111
111
|
uncountable/core/file_upload.py,sha256=zTpAFSd7_-TmEVWxOn1rDznyWE6_AdZyuDQC3LP34iI,2667
|
|
112
|
-
uncountable/core/types.py,sha256=
|
|
112
|
+
uncountable/core/types.py,sha256=RaNVuUPpcMBCfk-stS4Jh-9WBFzKK6_cVgRfPv7Dz6g,280
|
|
113
113
|
uncountable/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
114
114
|
uncountable/integration/construct_client.py,sha256=r6M5pnIO0fKcjf5d_AREPtWZ6AkWgcjkdu_jHQEYlT8,1084
|
|
115
115
|
uncountable/integration/cron.py,sha256=TIPqMPMSMtMJTu4aXwLf6QY-OLrpmyITLDp48UIr4Ok,919
|
|
@@ -210,7 +210,7 @@ uncountable/types/api/recipes/set_recipe_outputs.py,sha256=QYq39TNchQ80ET1C77OE9
|
|
|
210
210
|
uncountable/types/api/recipes/set_recipe_tags.py,sha256=U710hgq9-t6QZGRB-ZGHskpt4iXwYEjIRb67eh3P518,2453
|
|
211
211
|
uncountable/types/api/triggers/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
|
|
212
212
|
uncountable/types/api/triggers/run_trigger.py,sha256=9m9M8-nlGB_sAU2Qm2lWugp4h4Osqj6QpjNfU8osd1U,901
|
|
213
|
-
UncountablePythonSDK-0.0.
|
|
214
|
-
UncountablePythonSDK-0.0.
|
|
215
|
-
UncountablePythonSDK-0.0.
|
|
216
|
-
UncountablePythonSDK-0.0.
|
|
213
|
+
UncountablePythonSDK-0.0.25.dist-info/METADATA,sha256=LU2djI-Urro-UFLucKkSiaI6kq9ueCClAJhRDI2snAM,1613
|
|
214
|
+
UncountablePythonSDK-0.0.25.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
215
|
+
UncountablePythonSDK-0.0.25.dist-info/top_level.txt,sha256=HaMiBnH1wA7SG9-RVHIJPBH3l8X5gee2jUf-77Nz-Dk,41
|
|
216
|
+
UncountablePythonSDK-0.0.25.dist-info/RECORD,,
|
examples/create_entity.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from uncountable.core import
|
|
1
|
+
from uncountable.core import Client
|
|
2
|
+
from uncountable.core.client import AuthDetailsOAuth
|
|
2
3
|
from uncountable.types import (
|
|
3
4
|
entity_t,
|
|
4
5
|
field_values_t,
|
|
@@ -7,9 +8,8 @@ from uncountable.types import (
|
|
|
7
8
|
|
|
8
9
|
client = Client(
|
|
9
10
|
base_url="https://app.uncountable.com",
|
|
10
|
-
auth_details=
|
|
11
|
-
|
|
12
|
-
api_secret_key="X",
|
|
11
|
+
auth_details=AuthDetailsOAuth(
|
|
12
|
+
refresh_token="x"
|
|
13
13
|
),
|
|
14
14
|
)
|
|
15
15
|
entities = client.create_entity(
|
uncountable/core/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
from .client import
|
|
1
|
+
from .client import Client
|
|
2
|
+
from .types import AuthDetailsApiKey, AuthDetailsOAuth
|
|
2
3
|
from .file_upload import MediaFileUpload, UploadedFile
|
|
3
4
|
from .async_batch import AsyncBatchProcessor
|
|
4
5
|
|
|
5
|
-
__all__: list[str] = ["AuthDetailsApiKey", "AsyncBatchProcessor", "Client", "MediaFileUpload", "UploadedFile"]
|
|
6
|
+
__all__: list[str] = ["AuthDetailsApiKey", "AuthDetailsOAuth", "AsyncBatchProcessor", "Client", "MediaFileUpload", "UploadedFile"]
|
uncountable/core/client.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import base64
|
|
2
|
+
from datetime import datetime, timedelta
|
|
2
3
|
import json
|
|
3
4
|
import typing
|
|
4
5
|
from dataclasses import dataclass
|
|
@@ -14,7 +15,7 @@ from pkgs.serialization_util.serialization_helpers import JsonValue
|
|
|
14
15
|
from uncountable.types.client_base import APIRequest, ClientMethods
|
|
15
16
|
|
|
16
17
|
from .file_upload import FileUpload, FileUploader, UploadedFile
|
|
17
|
-
from .types import AuthDetails, AuthDetailsApiKey
|
|
18
|
+
from .types import AuthDetails, AuthDetailsApiKey, AuthDetailsOAuth
|
|
18
19
|
|
|
19
20
|
DT = typing.TypeVar("DT")
|
|
20
21
|
|
|
@@ -48,11 +49,13 @@ class HTTPPostRequest(HTTPRequestBase):
|
|
|
48
49
|
HTTPRequest = HTTPPostRequest | HTTPGetRequest
|
|
49
50
|
|
|
50
51
|
|
|
51
|
-
|
|
52
52
|
@dataclass(kw_only=True)
|
|
53
|
-
class ClientConfig
|
|
53
|
+
class ClientConfig:
|
|
54
54
|
allow_insecure_tls: bool = False
|
|
55
|
-
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
OAUTH_REFRESH_WINDOW_SECONDS = 60 * 5
|
|
58
|
+
|
|
56
59
|
|
|
57
60
|
class APIResponseError(BaseException):
|
|
58
61
|
status_code: int
|
|
@@ -107,19 +110,60 @@ class SDKError(BaseException):
|
|
|
107
110
|
return f"internal SDK error, please contact Uncountable support: {self.message}"
|
|
108
111
|
|
|
109
112
|
|
|
113
|
+
@dataclass(kw_only=True)
|
|
114
|
+
class OAuthBearerTokenCache:
|
|
115
|
+
token: str
|
|
116
|
+
expires_at: datetime
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(kw_only=True)
|
|
120
|
+
class GetOauthBearerTokenData:
|
|
121
|
+
access_token: str
|
|
122
|
+
expires_in: int
|
|
123
|
+
token_type: str
|
|
124
|
+
scope: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
oauth_bearer_token_data_parser = CachedParser(GetOauthBearerTokenData)
|
|
128
|
+
|
|
129
|
+
|
|
110
130
|
class Client(ClientMethods):
|
|
111
131
|
_parser_map: dict[type, CachedParser] = {}
|
|
112
132
|
_auth_details: AuthDetails
|
|
113
133
|
_base_url: str
|
|
114
134
|
_file_uploader: FileUploader
|
|
115
135
|
_cfg: ClientConfig
|
|
136
|
+
_oauth_bearer_token_cache: OAuthBearerTokenCache | None = None
|
|
116
137
|
|
|
117
|
-
def __init__(
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
*,
|
|
141
|
+
base_url: str,
|
|
142
|
+
auth_details: AuthDetails,
|
|
143
|
+
config: ClientConfig | None = None,
|
|
144
|
+
):
|
|
118
145
|
self._auth_details = auth_details
|
|
119
146
|
self._base_url = base_url
|
|
120
147
|
self._file_uploader = FileUploader(self._base_url, self._auth_details)
|
|
121
148
|
self._cfg = config or ClientConfig()
|
|
122
149
|
|
|
150
|
+
def _get_response_json(self, response: requests.Response) -> dict[str, JsonValue]:
|
|
151
|
+
if response.status_code < 200 or response.status_code > 299:
|
|
152
|
+
extra_details: dict[str, JsonValue] | None = None
|
|
153
|
+
try:
|
|
154
|
+
data = response.json()
|
|
155
|
+
if "error" in data:
|
|
156
|
+
extra_details = data["error"]
|
|
157
|
+
except JSONDecodeError:
|
|
158
|
+
pass
|
|
159
|
+
raise APIResponseError.construct_error(
|
|
160
|
+
status_code=response.status_code, extra_details=extra_details
|
|
161
|
+
)
|
|
162
|
+
try:
|
|
163
|
+
return response.json()
|
|
164
|
+
except JSONDecodeError:
|
|
165
|
+
raise SDKError("unable to process response")
|
|
166
|
+
|
|
123
167
|
def do_request(self, *, api_request: APIRequest, return_type: type[DT]) -> DT:
|
|
124
168
|
http_request = self._build_http_request(api_request=api_request)
|
|
125
169
|
match http_request:
|
|
@@ -128,7 +172,7 @@ class Client(ClientMethods):
|
|
|
128
172
|
http_request.url,
|
|
129
173
|
headers=http_request.headers,
|
|
130
174
|
params=http_request.query_params,
|
|
131
|
-
verify=not self._cfg.allow_insecure_tls
|
|
175
|
+
verify=not self._cfg.allow_insecure_tls,
|
|
132
176
|
)
|
|
133
177
|
case HTTPPostRequest():
|
|
134
178
|
response = requests.post(
|
|
@@ -136,26 +180,16 @@ class Client(ClientMethods):
|
|
|
136
180
|
headers=http_request.headers,
|
|
137
181
|
data=http_request.body,
|
|
138
182
|
params=http_request.query_params,
|
|
139
|
-
verify=not self._cfg.allow_insecure_tls
|
|
183
|
+
verify=not self._cfg.allow_insecure_tls,
|
|
140
184
|
)
|
|
141
185
|
case _:
|
|
142
186
|
typing.assert_never(http_request)
|
|
143
|
-
|
|
144
|
-
extra_details: dict[str, JsonValue] | None = None
|
|
145
|
-
try:
|
|
146
|
-
data = response.json()
|
|
147
|
-
if "error" in data:
|
|
148
|
-
extra_details = data["error"]
|
|
149
|
-
except JSONDecodeError:
|
|
150
|
-
pass
|
|
151
|
-
raise APIResponseError.construct_error(
|
|
152
|
-
status_code=response.status_code, extra_details=extra_details
|
|
153
|
-
)
|
|
187
|
+
response_data = self._get_response_json(response)
|
|
154
188
|
cached_parser = self._get_cached_parser(return_type)
|
|
155
189
|
try:
|
|
156
|
-
data =
|
|
190
|
+
data = response_data["data"]
|
|
157
191
|
return cached_parser.parse_api(data)
|
|
158
|
-
except ValueError | JSONDecodeError:
|
|
192
|
+
except ValueError | JSONDecodeError | KeyError:
|
|
159
193
|
raise SDKError("unable to process response")
|
|
160
194
|
|
|
161
195
|
def _get_cached_parser(self, data_type: type[DT]) -> CachedParser[DT]:
|
|
@@ -163,6 +197,32 @@ class Client(ClientMethods):
|
|
|
163
197
|
self._parser_map[data_type] = CachedParser(data_type)
|
|
164
198
|
return self._parser_map[data_type]
|
|
165
199
|
|
|
200
|
+
def _get_oauth_bearer_token(self, *, oauth_details: AuthDetailsOAuth) -> str:
|
|
201
|
+
if (
|
|
202
|
+
self._oauth_bearer_token_cache is None
|
|
203
|
+
or (
|
|
204
|
+
self._oauth_bearer_token_cache.expires_at - datetime.now()
|
|
205
|
+
).total_seconds()
|
|
206
|
+
< OAUTH_REFRESH_WINDOW_SECONDS
|
|
207
|
+
):
|
|
208
|
+
refresh_url = urljoin(self._base_url, "/token/get_bearer_token")
|
|
209
|
+
response = requests.post(
|
|
210
|
+
refresh_url,
|
|
211
|
+
data={
|
|
212
|
+
"client_secret": oauth_details.refresh_token,
|
|
213
|
+
"scope": oauth_details.scope,
|
|
214
|
+
"grant_type": "client_credentials",
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
data = self._get_response_json(response)
|
|
218
|
+
token_data = oauth_bearer_token_data_parser.parse_storage(data)
|
|
219
|
+
self._oauth_bearer_token_cache = OAuthBearerTokenCache(
|
|
220
|
+
token=token_data.access_token,
|
|
221
|
+
expires_at=datetime.now() + timedelta(seconds=token_data.expires_in),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return self._oauth_bearer_token_cache.token
|
|
225
|
+
|
|
166
226
|
def _build_auth_headers(self) -> dict[str, str]:
|
|
167
227
|
match self._auth_details:
|
|
168
228
|
case AuthDetailsApiKey():
|
|
@@ -170,6 +230,9 @@ class Client(ClientMethods):
|
|
|
170
230
|
f"{self._auth_details.api_id}:{self._auth_details.api_secret_key}".encode()
|
|
171
231
|
).decode("utf-8")
|
|
172
232
|
return {"Authorization": f"Basic {encoded}"}
|
|
233
|
+
case AuthDetailsOAuth():
|
|
234
|
+
token = self._get_oauth_bearer_token(oauth_details=self._auth_details)
|
|
235
|
+
return {"Authorization": f"Bearer {token}"}
|
|
173
236
|
typing.assert_never(self._auth_details)
|
|
174
237
|
|
|
175
238
|
def _build_http_request(self, *, api_request: APIRequest) -> HTTPRequest:
|
uncountable/core/types.py
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import json
|
|
3
|
-
import typing
|
|
4
1
|
from dataclasses import dataclass
|
|
5
|
-
from enum import StrEnum
|
|
6
|
-
from urllib.parse import urljoin
|
|
7
|
-
|
|
8
|
-
import aiohttp
|
|
9
|
-
import requests
|
|
10
|
-
|
|
11
|
-
from pkgs.argument_parser import CachedParser
|
|
12
|
-
from pkgs.serialization_util import serialize_for_api
|
|
13
|
-
from uncountable.types.client_base import APIRequest, ClientMethods
|
|
14
2
|
|
|
15
3
|
|
|
16
4
|
@dataclass(kw_only=True)
|
|
@@ -19,4 +7,10 @@ class AuthDetailsApiKey:
|
|
|
19
7
|
api_secret_key: str
|
|
20
8
|
|
|
21
9
|
|
|
22
|
-
|
|
10
|
+
@dataclass(kw_only=True)
|
|
11
|
+
class AuthDetailsOAuth:
|
|
12
|
+
refresh_token: str
|
|
13
|
+
scope: str = "unc.rnd"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
AuthDetails = AuthDetailsApiKey | AuthDetailsOAuth
|
|
File without changes
|
{UncountablePythonSDK-0.0.24.dist-info → UncountablePythonSDK-0.0.25.dist-info}/top_level.txt
RENAMED
|
File without changes
|