cognite-toolkit 0.6.97__py3-none-any.whl → 0.7.30__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.
- cognite_toolkit/_cdf.py +16 -17
- cognite_toolkit/_cdf_tk/apps/__init__.py +2 -0
- cognite_toolkit/_cdf_tk/apps/_core_app.py +13 -5
- cognite_toolkit/_cdf_tk/apps/_data_app.py +1 -1
- cognite_toolkit/_cdf_tk/apps/_dev_app.py +86 -0
- cognite_toolkit/_cdf_tk/apps/_download_app.py +692 -24
- cognite_toolkit/_cdf_tk/apps/_dump_app.py +43 -101
- cognite_toolkit/_cdf_tk/apps/_landing_app.py +18 -4
- cognite_toolkit/_cdf_tk/apps/_migrate_app.py +249 -9
- cognite_toolkit/_cdf_tk/apps/_modules_app.py +0 -3
- cognite_toolkit/_cdf_tk/apps/_purge.py +15 -43
- cognite_toolkit/_cdf_tk/apps/_run.py +11 -0
- cognite_toolkit/_cdf_tk/apps/_upload_app.py +45 -6
- cognite_toolkit/_cdf_tk/builders/__init__.py +2 -2
- cognite_toolkit/_cdf_tk/builders/_base.py +28 -42
- cognite_toolkit/_cdf_tk/cdf_toml.py +20 -1
- cognite_toolkit/_cdf_tk/client/_toolkit_client.py +23 -3
- cognite_toolkit/_cdf_tk/client/api/extended_functions.py +6 -9
- cognite_toolkit/_cdf_tk/client/api/infield.py +93 -1
- cognite_toolkit/_cdf_tk/client/api/migration.py +175 -1
- cognite_toolkit/_cdf_tk/client/api/streams.py +84 -0
- cognite_toolkit/_cdf_tk/client/api/three_d.py +50 -0
- cognite_toolkit/_cdf_tk/client/data_classes/base.py +25 -1
- cognite_toolkit/_cdf_tk/client/data_classes/canvas.py +46 -3
- cognite_toolkit/_cdf_tk/client/data_classes/charts.py +3 -3
- cognite_toolkit/_cdf_tk/client/data_classes/charts_data.py +95 -213
- cognite_toolkit/_cdf_tk/client/data_classes/infield.py +32 -18
- cognite_toolkit/_cdf_tk/client/data_classes/migration.py +10 -2
- cognite_toolkit/_cdf_tk/client/data_classes/streams.py +90 -0
- cognite_toolkit/_cdf_tk/client/data_classes/three_d.py +47 -0
- cognite_toolkit/_cdf_tk/client/testing.py +18 -2
- cognite_toolkit/_cdf_tk/commands/__init__.py +6 -6
- cognite_toolkit/_cdf_tk/commands/_changes.py +3 -42
- cognite_toolkit/_cdf_tk/commands/_download.py +21 -11
- cognite_toolkit/_cdf_tk/commands/_migrate/__init__.py +0 -2
- cognite_toolkit/_cdf_tk/commands/_migrate/command.py +22 -20
- cognite_toolkit/_cdf_tk/commands/_migrate/conversion.py +133 -91
- cognite_toolkit/_cdf_tk/commands/_migrate/data_classes.py +73 -22
- cognite_toolkit/_cdf_tk/commands/_migrate/data_mapper.py +311 -43
- cognite_toolkit/_cdf_tk/commands/_migrate/default_mappings.py +5 -5
- cognite_toolkit/_cdf_tk/commands/_migrate/issues.py +33 -0
- cognite_toolkit/_cdf_tk/commands/_migrate/migration_io.py +157 -8
- cognite_toolkit/_cdf_tk/commands/_migrate/selectors.py +9 -4
- cognite_toolkit/_cdf_tk/commands/_purge.py +27 -28
- cognite_toolkit/_cdf_tk/commands/_questionary_style.py +16 -0
- cognite_toolkit/_cdf_tk/commands/_upload.py +109 -86
- cognite_toolkit/_cdf_tk/commands/about.py +221 -0
- cognite_toolkit/_cdf_tk/commands/auth.py +19 -12
- cognite_toolkit/_cdf_tk/commands/build_cmd.py +15 -61
- cognite_toolkit/_cdf_tk/commands/clean.py +63 -16
- cognite_toolkit/_cdf_tk/commands/deploy.py +20 -17
- cognite_toolkit/_cdf_tk/commands/dump_resource.py +6 -4
- cognite_toolkit/_cdf_tk/commands/init.py +225 -3
- cognite_toolkit/_cdf_tk/commands/modules.py +20 -44
- cognite_toolkit/_cdf_tk/commands/pull.py +6 -19
- cognite_toolkit/_cdf_tk/commands/resources.py +179 -0
- cognite_toolkit/_cdf_tk/constants.py +20 -1
- cognite_toolkit/_cdf_tk/cruds/__init__.py +19 -5
- cognite_toolkit/_cdf_tk/cruds/_base_cruds.py +14 -70
- cognite_toolkit/_cdf_tk/cruds/_data_cruds.py +8 -17
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/__init__.py +4 -1
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/agent.py +11 -9
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/auth.py +4 -14
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/classic.py +44 -43
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/configuration.py +4 -11
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/data_organization.py +4 -13
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/datamodel.py +205 -66
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/extraction_pipeline.py +5 -17
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/fieldops.py +116 -27
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/file.py +6 -27
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/function.py +9 -28
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/hosted_extractors.py +12 -30
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/industrial_tool.py +3 -7
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/location.py +3 -15
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/migration.py +4 -12
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/raw.py +4 -10
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/relationship.py +3 -8
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/robotics.py +15 -44
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/streams.py +94 -0
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/three_d_model.py +3 -7
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/timeseries.py +5 -15
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/transformation.py +39 -31
- cognite_toolkit/_cdf_tk/cruds/_resource_cruds/workflow.py +20 -40
- cognite_toolkit/_cdf_tk/cruds/_worker.py +24 -36
- cognite_toolkit/_cdf_tk/feature_flags.py +16 -36
- cognite_toolkit/_cdf_tk/plugins.py +2 -1
- cognite_toolkit/_cdf_tk/resource_classes/__init__.py +4 -0
- cognite_toolkit/_cdf_tk/resource_classes/capabilities.py +12 -0
- cognite_toolkit/_cdf_tk/resource_classes/functions.py +3 -1
- cognite_toolkit/_cdf_tk/resource_classes/infield_cdm_location_config.py +109 -0
- cognite_toolkit/_cdf_tk/resource_classes/migration.py +8 -17
- cognite_toolkit/_cdf_tk/resource_classes/streams.py +29 -0
- cognite_toolkit/_cdf_tk/storageio/__init__.py +9 -21
- cognite_toolkit/_cdf_tk/storageio/_annotations.py +19 -16
- cognite_toolkit/_cdf_tk/storageio/_applications.py +338 -26
- cognite_toolkit/_cdf_tk/storageio/_asset_centric.py +67 -104
- cognite_toolkit/_cdf_tk/storageio/_base.py +61 -29
- cognite_toolkit/_cdf_tk/storageio/_datapoints.py +276 -20
- cognite_toolkit/_cdf_tk/storageio/_file_content.py +436 -0
- cognite_toolkit/_cdf_tk/storageio/_instances.py +34 -2
- cognite_toolkit/_cdf_tk/storageio/_raw.py +26 -0
- cognite_toolkit/_cdf_tk/storageio/selectors/__init__.py +62 -4
- cognite_toolkit/_cdf_tk/storageio/selectors/_base.py +14 -2
- cognite_toolkit/_cdf_tk/storageio/selectors/_canvas.py +14 -0
- cognite_toolkit/_cdf_tk/storageio/selectors/_charts.py +14 -0
- cognite_toolkit/_cdf_tk/storageio/selectors/_datapoints.py +23 -3
- cognite_toolkit/_cdf_tk/storageio/selectors/_file_content.py +164 -0
- cognite_toolkit/_cdf_tk/tk_warnings/other.py +4 -0
- cognite_toolkit/_cdf_tk/tracker.py +2 -2
- cognite_toolkit/_cdf_tk/utils/dtype_conversion.py +9 -3
- cognite_toolkit/_cdf_tk/utils/fileio/__init__.py +2 -0
- cognite_toolkit/_cdf_tk/utils/fileio/_base.py +5 -1
- cognite_toolkit/_cdf_tk/utils/fileio/_readers.py +112 -20
- cognite_toolkit/_cdf_tk/utils/fileio/_writers.py +15 -15
- cognite_toolkit/_cdf_tk/utils/http_client/_client.py +284 -18
- cognite_toolkit/_cdf_tk/utils/http_client/_data_classes.py +50 -4
- cognite_toolkit/_cdf_tk/utils/http_client/_data_classes2.py +187 -0
- cognite_toolkit/_cdf_tk/utils/interactive_select.py +9 -14
- cognite_toolkit/_cdf_tk/utils/sql_parser.py +2 -3
- cognite_toolkit/_cdf_tk/utils/useful_types.py +6 -2
- cognite_toolkit/_cdf_tk/validation.py +79 -1
- cognite_toolkit/_repo_files/GitHub/.github/workflows/deploy.yaml +1 -1
- cognite_toolkit/_repo_files/GitHub/.github/workflows/dry-run.yaml +1 -1
- cognite_toolkit/_resources/cdf.toml +5 -4
- cognite_toolkit/_version.py +1 -1
- cognite_toolkit/config.dev.yaml +13 -0
- {cognite_toolkit-0.6.97.dist-info → cognite_toolkit-0.7.30.dist-info}/METADATA +24 -24
- {cognite_toolkit-0.6.97.dist-info → cognite_toolkit-0.7.30.dist-info}/RECORD +153 -143
- cognite_toolkit-0.7.30.dist-info/WHEEL +4 -0
- {cognite_toolkit-0.6.97.dist-info → cognite_toolkit-0.7.30.dist-info}/entry_points.txt +1 -0
- cognite_toolkit/_cdf_tk/commands/_migrate/canvas.py +0 -201
- cognite_toolkit/_cdf_tk/commands/dump_data.py +0 -489
- cognite_toolkit/_cdf_tk/commands/featureflag.py +0 -27
- cognite_toolkit/_cdf_tk/utils/table_writers.py +0 -434
- cognite_toolkit-0.6.97.dist-info/WHEEL +0 -4
- cognite_toolkit-0.6.97.dist-info/licenses/LICENSE +0 -18
|
@@ -4,7 +4,7 @@ import sys
|
|
|
4
4
|
import time
|
|
5
5
|
from collections import deque
|
|
6
6
|
from collections.abc import MutableMapping, Sequence, Set
|
|
7
|
-
from typing import Literal
|
|
7
|
+
from typing import Literal, TypeVar
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
10
|
from cognite.client import global_config
|
|
@@ -23,6 +23,20 @@ from cognite_toolkit._cdf_tk.utils.http_client._data_classes import (
|
|
|
23
23
|
ResponseList,
|
|
24
24
|
ResponseMessage,
|
|
25
25
|
)
|
|
26
|
+
from cognite_toolkit._cdf_tk.utils.http_client._data_classes2 import (
|
|
27
|
+
BaseRequestMessage,
|
|
28
|
+
ErrorDetails2,
|
|
29
|
+
FailedRequest2,
|
|
30
|
+
FailedResponse2,
|
|
31
|
+
HTTPResult2,
|
|
32
|
+
ItemsFailedRequest2,
|
|
33
|
+
ItemsFailedResponse2,
|
|
34
|
+
ItemsRequest2,
|
|
35
|
+
ItemsResultMessage2,
|
|
36
|
+
ItemsSuccessResponse2,
|
|
37
|
+
RequestMessage2,
|
|
38
|
+
SuccessResponse2,
|
|
39
|
+
)
|
|
26
40
|
from cognite_toolkit._cdf_tk.utils.useful_types import PrimitiveType
|
|
27
41
|
|
|
28
42
|
if sys.version_info >= (3, 11):
|
|
@@ -32,6 +46,8 @@ else:
|
|
|
32
46
|
|
|
33
47
|
from cognite_toolkit._cdf_tk.client.config import ToolkitClientConfig
|
|
34
48
|
|
|
49
|
+
_T_Request_Message = TypeVar("_T_Request_Message", bound=BaseRequestMessage)
|
|
50
|
+
|
|
35
51
|
|
|
36
52
|
class HTTPClient:
|
|
37
53
|
"""An HTTP client.
|
|
@@ -48,6 +64,7 @@ class HTTPClient:
|
|
|
48
64
|
Default is {408, 429, 502, 503, 504}.
|
|
49
65
|
split_items_status_codes (frozenset[int]): In the case of ItemRequest with multiple
|
|
50
66
|
items, these status codes will trigger splitting the request into smaller batches.
|
|
67
|
+
console (Console | None): Optional Rich Console for printing warnings.
|
|
51
68
|
|
|
52
69
|
"""
|
|
53
70
|
|
|
@@ -59,6 +76,7 @@ class HTTPClient:
|
|
|
59
76
|
pool_maxsize: int = 20,
|
|
60
77
|
retry_status_codes: Set[int] = frozenset({408, 429, 502, 503, 504}),
|
|
61
78
|
split_items_status_codes: Set[int] = frozenset({400, 408, 409, 422, 502, 503, 504}),
|
|
79
|
+
console: Console | None = None,
|
|
62
80
|
):
|
|
63
81
|
self.config = config
|
|
64
82
|
self._max_retries = max_retries
|
|
@@ -66,6 +84,7 @@ class HTTPClient:
|
|
|
66
84
|
self._pool_maxsize = pool_maxsize
|
|
67
85
|
self._retry_status_codes = retry_status_codes
|
|
68
86
|
self._split_items_status_codes = split_items_status_codes
|
|
87
|
+
self._console = console
|
|
69
88
|
|
|
70
89
|
# Thread-safe session for connection pooling
|
|
71
90
|
self.session = self._create_thread_safe_session()
|
|
@@ -80,12 +99,11 @@ class HTTPClient:
|
|
|
80
99
|
self.session.close()
|
|
81
100
|
return False # Do not suppress exceptions
|
|
82
101
|
|
|
83
|
-
def request(self, message: RequestMessage
|
|
102
|
+
def request(self, message: RequestMessage) -> Sequence[HTTPMessage]:
|
|
84
103
|
"""Send an HTTP request and return the response.
|
|
85
104
|
|
|
86
105
|
Args:
|
|
87
106
|
message (RequestMessage): The request message to send.
|
|
88
|
-
console (Console | None): The rich console to use for printing warnings.
|
|
89
107
|
|
|
90
108
|
Returns:
|
|
91
109
|
Sequence[HTTPMessage]: The response message(s). This can also
|
|
@@ -98,12 +116,12 @@ class HTTPClient:
|
|
|
98
116
|
return message.create_failed_request(error_msg)
|
|
99
117
|
try:
|
|
100
118
|
response = self._make_request(message)
|
|
101
|
-
results = self._handle_response(response, message
|
|
119
|
+
results = self._handle_response(response, message)
|
|
102
120
|
except Exception as e:
|
|
103
121
|
results = self._handle_error(e, message)
|
|
104
122
|
return results
|
|
105
123
|
|
|
106
|
-
def request_with_retries(self, message: RequestMessage
|
|
124
|
+
def request_with_retries(self, message: RequestMessage) -> ResponseList:
|
|
107
125
|
"""Send an HTTP request and handle retries.
|
|
108
126
|
|
|
109
127
|
This method will keep retrying the request until it either succeeds or
|
|
@@ -114,7 +132,6 @@ class HTTPClient:
|
|
|
114
132
|
|
|
115
133
|
Args:
|
|
116
134
|
message (RequestMessage): The request message to send.
|
|
117
|
-
console (Console | None): The rich console to use for printing warnings.
|
|
118
135
|
|
|
119
136
|
Returns:
|
|
120
137
|
Sequence[ResponseMessage | FailedRequestMessage]: The final response
|
|
@@ -127,7 +144,7 @@ class HTTPClient:
|
|
|
127
144
|
final_responses = ResponseList([])
|
|
128
145
|
while pending_requests:
|
|
129
146
|
current_request = pending_requests.popleft()
|
|
130
|
-
results = self.request(current_request
|
|
147
|
+
results = self.request(current_request)
|
|
131
148
|
|
|
132
149
|
for result in results:
|
|
133
150
|
if isinstance(result, RequestMessage):
|
|
@@ -149,34 +166,40 @@ class HTTPClient:
|
|
|
149
166
|
)
|
|
150
167
|
|
|
151
168
|
def _create_headers(
|
|
152
|
-
self,
|
|
169
|
+
self,
|
|
170
|
+
api_version: str | None = None,
|
|
171
|
+
content_type: str = "application/json",
|
|
172
|
+
accept: str = "application/json",
|
|
173
|
+
content_length: int | None = None,
|
|
153
174
|
) -> MutableMapping[str, str]:
|
|
154
175
|
headers: MutableMapping[str, str] = {}
|
|
155
176
|
headers["User-Agent"] = f"httpx/{httpx.__version__} {get_user_agent()}"
|
|
156
177
|
auth_name, auth_value = self.config.credentials.authorization_header()
|
|
157
178
|
headers[auth_name] = auth_value
|
|
158
|
-
headers["
|
|
179
|
+
headers["Content-Type"] = content_type
|
|
180
|
+
if content_length is not None:
|
|
181
|
+
headers["Content-Length"] = str(content_length)
|
|
159
182
|
headers["accept"] = accept
|
|
160
183
|
headers["x-cdp-sdk"] = f"CogniteToolkit:{get_current_toolkit_version()}"
|
|
161
184
|
headers["x-cdp-app"] = self.config.client_name
|
|
162
185
|
headers["cdf-version"] = api_version or self.config.api_subversion
|
|
163
|
-
if not global_config.disable_gzip:
|
|
186
|
+
if not global_config.disable_gzip and content_length is None:
|
|
164
187
|
headers["Content-Encoding"] = "gzip"
|
|
165
188
|
return headers
|
|
166
189
|
|
|
167
190
|
def _make_request(self, item: RequestMessage) -> httpx.Response:
|
|
168
|
-
headers = self._create_headers(item.api_version, item.content_type, item.accept)
|
|
191
|
+
headers = self._create_headers(item.api_version, item.content_type, item.accept, item.content_length)
|
|
169
192
|
params: dict[str, PrimitiveType] | None = None
|
|
170
193
|
if isinstance(item, ParamRequest):
|
|
171
194
|
params = item.parameters
|
|
172
195
|
data: str | bytes | None = None
|
|
173
196
|
if isinstance(item, BodyRequest):
|
|
174
197
|
data = item.data()
|
|
175
|
-
if not global_config.disable_gzip:
|
|
198
|
+
if not global_config.disable_gzip and item.content_length is None:
|
|
176
199
|
data = gzip.compress(data.encode("utf-8"))
|
|
177
200
|
elif isinstance(item, DataBodyRequest):
|
|
178
201
|
data = item.data()
|
|
179
|
-
if not global_config.disable_gzip:
|
|
202
|
+
if not global_config.disable_gzip and item.content_length is None:
|
|
180
203
|
data = gzip.compress(data)
|
|
181
204
|
return self.session.request(
|
|
182
205
|
method=item.method,
|
|
@@ -188,9 +211,7 @@ class HTTPClient:
|
|
|
188
211
|
follow_redirects=False,
|
|
189
212
|
)
|
|
190
213
|
|
|
191
|
-
def _handle_response(
|
|
192
|
-
self, response: httpx.Response, request: RequestMessage, console: Console | None = None
|
|
193
|
-
) -> Sequence[HTTPMessage]:
|
|
214
|
+
def _handle_response(self, response: httpx.Response, request: RequestMessage) -> Sequence[HTTPMessage]:
|
|
194
215
|
if 200 <= response.status_code < 300:
|
|
195
216
|
return request.create_success_response(response)
|
|
196
217
|
elif (
|
|
@@ -210,11 +231,11 @@ class HTTPClient:
|
|
|
210
231
|
|
|
211
232
|
retry_after = self._get_retry_after_in_header(response)
|
|
212
233
|
if retry_after is not None and response.status_code == 429 and request.status_attempt < self._max_retries:
|
|
213
|
-
if
|
|
234
|
+
if self._console is not None:
|
|
214
235
|
short_url = request.endpoint_url.removeprefix(self.config.base_api_url)
|
|
215
236
|
HighSeverityWarning(
|
|
216
237
|
f"Rate limit exceeded for the {short_url!r} endpoint. Retrying after {retry_after} seconds."
|
|
217
|
-
).print_warning(console=
|
|
238
|
+
).print_warning(console=self._console)
|
|
218
239
|
request.status_attempt += 1
|
|
219
240
|
time.sleep(retry_after)
|
|
220
241
|
return [request]
|
|
@@ -267,3 +288,248 @@ class HTTPClient:
|
|
|
267
288
|
error_msg = f"RequestException after {request.total_attempts - 1} attempts ({error_type} error): {e!s}"
|
|
268
289
|
|
|
269
290
|
return request.create_failed_request(error_msg)
|
|
291
|
+
|
|
292
|
+
def request_single(self, message: RequestMessage2) -> RequestMessage2 | HTTPResult2:
|
|
293
|
+
"""Send an HTTP request and return the response.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
message (RequestMessage2): The request message to send.
|
|
297
|
+
Returns:
|
|
298
|
+
HTTPMessage: The response message.
|
|
299
|
+
"""
|
|
300
|
+
try:
|
|
301
|
+
response = self._make_request2(message)
|
|
302
|
+
result = self._handle_response_single(response, message)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
result = self._handle_error_single(e, message)
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
def request_single_retries(self, message: RequestMessage2) -> HTTPResult2:
|
|
308
|
+
"""Send an HTTP request and handle retries.
|
|
309
|
+
|
|
310
|
+
This method will keep retrying the request until it either succeeds or
|
|
311
|
+
exhausts the maximum number of retries.
|
|
312
|
+
|
|
313
|
+
Note this method will use the current thread to process all request, thus
|
|
314
|
+
it is blocking.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
message (RequestMessage2): The request message to send.
|
|
318
|
+
Returns:
|
|
319
|
+
HTTPMessage2: The final response message, which can be either successful response or failed request.
|
|
320
|
+
"""
|
|
321
|
+
if message.total_attempts > 0:
|
|
322
|
+
raise RuntimeError(f"RequestMessage has already been attempted {message.total_attempts} times.")
|
|
323
|
+
current_request = message
|
|
324
|
+
while True:
|
|
325
|
+
result = self.request_single(current_request)
|
|
326
|
+
if isinstance(result, RequestMessage2):
|
|
327
|
+
current_request = result
|
|
328
|
+
elif isinstance(result, HTTPResult2):
|
|
329
|
+
return result
|
|
330
|
+
else:
|
|
331
|
+
raise TypeError(f"Unexpected result type: {type(result)}")
|
|
332
|
+
|
|
333
|
+
def _make_request2(self, message: BaseRequestMessage) -> httpx.Response:
|
|
334
|
+
headers = self._create_headers(message.api_version, message.content_type, message.accept)
|
|
335
|
+
return self.session.request(
|
|
336
|
+
method=message.method,
|
|
337
|
+
url=message.endpoint_url,
|
|
338
|
+
content=message.content,
|
|
339
|
+
headers=headers,
|
|
340
|
+
params=message.parameters,
|
|
341
|
+
timeout=self.config.timeout,
|
|
342
|
+
follow_redirects=False,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
def _handle_response_single(
|
|
346
|
+
self, response: httpx.Response, request: RequestMessage2
|
|
347
|
+
) -> RequestMessage2 | HTTPResult2:
|
|
348
|
+
if 200 <= response.status_code < 300:
|
|
349
|
+
return SuccessResponse2(
|
|
350
|
+
status_code=response.status_code,
|
|
351
|
+
body=response.text,
|
|
352
|
+
content=response.content,
|
|
353
|
+
)
|
|
354
|
+
if retry_request := self._retry_request(response, request):
|
|
355
|
+
return retry_request
|
|
356
|
+
else:
|
|
357
|
+
# Permanent failure
|
|
358
|
+
return FailedResponse2(
|
|
359
|
+
status_code=response.status_code,
|
|
360
|
+
body=response.text,
|
|
361
|
+
error=ErrorDetails2.from_response(response),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def _retry_request(self, response: httpx.Response, request: _T_Request_Message) -> _T_Request_Message | None:
|
|
365
|
+
retry_after = self._get_retry_after_in_header(response)
|
|
366
|
+
if retry_after is not None and response.status_code == 429 and request.status_attempt < self._max_retries:
|
|
367
|
+
if self._console is not None:
|
|
368
|
+
short_url = request.endpoint_url.removeprefix(self.config.base_api_url)
|
|
369
|
+
HighSeverityWarning(
|
|
370
|
+
f"Rate limit exceeded for the {short_url!r} endpoint. Retrying after {retry_after} seconds."
|
|
371
|
+
).print_warning(console=self._console)
|
|
372
|
+
request.status_attempt += 1
|
|
373
|
+
time.sleep(retry_after)
|
|
374
|
+
return request
|
|
375
|
+
|
|
376
|
+
if request.status_attempt < self._max_retries and response.status_code in self._retry_status_codes:
|
|
377
|
+
request.status_attempt += 1
|
|
378
|
+
time.sleep(self._backoff_time(request.total_attempts))
|
|
379
|
+
return request
|
|
380
|
+
return None
|
|
381
|
+
|
|
382
|
+
def _handle_error_single(self, e: Exception, request: RequestMessage2) -> RequestMessage2 | HTTPResult2:
|
|
383
|
+
if isinstance(e, httpx.ReadTimeout | httpx.TimeoutException):
|
|
384
|
+
error_type = "read"
|
|
385
|
+
request.read_attempt += 1
|
|
386
|
+
attempts = request.read_attempt
|
|
387
|
+
elif isinstance(e, ConnectionError | httpx.ConnectError | httpx.ConnectTimeout):
|
|
388
|
+
error_type = "connect"
|
|
389
|
+
request.connect_attempt += 1
|
|
390
|
+
attempts = request.connect_attempt
|
|
391
|
+
else:
|
|
392
|
+
error_msg = f"Unexpected exception: {e!s}"
|
|
393
|
+
return FailedRequest2(error=error_msg)
|
|
394
|
+
|
|
395
|
+
if attempts <= self._max_retries:
|
|
396
|
+
time.sleep(self._backoff_time(request.total_attempts))
|
|
397
|
+
return request
|
|
398
|
+
else:
|
|
399
|
+
# We have already incremented the attempt count, so we subtract 1 here
|
|
400
|
+
error_msg = f"RequestException after {request.total_attempts - 1} attempts ({error_type} error): {e!s}"
|
|
401
|
+
|
|
402
|
+
return FailedRequest2(error=error_msg)
|
|
403
|
+
|
|
404
|
+
def request_items(self, message: ItemsRequest2) -> Sequence[ItemsRequest2 | ItemsResultMessage2]:
|
|
405
|
+
"""Send an HTTP request with multiple items and return the response.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
message (ItemsRequest2): The request message to send.
|
|
409
|
+
Returns:
|
|
410
|
+
Sequence[ItemsRequest2 | ItemsResultMessage2]: The response message(s). This can also
|
|
411
|
+
include ItemsRequest2(s) to be retried or split.
|
|
412
|
+
"""
|
|
413
|
+
if message.tracker and message.tracker.limit_reached():
|
|
414
|
+
return [
|
|
415
|
+
ItemsFailedRequest2(
|
|
416
|
+
ids=[item.as_id() for item in message.items],
|
|
417
|
+
error_message=f"Aborting further splitting of requests after {message.tracker.failed_split_count} failed attempts.",
|
|
418
|
+
)
|
|
419
|
+
]
|
|
420
|
+
try:
|
|
421
|
+
response = self._make_request2(message)
|
|
422
|
+
results = self._handle_items_response(response, message)
|
|
423
|
+
except Exception as e:
|
|
424
|
+
results = self._handle_items_error(e, message)
|
|
425
|
+
return results
|
|
426
|
+
|
|
427
|
+
def request_items_retries(self, message: ItemsRequest2) -> Sequence[ItemsResultMessage2]:
|
|
428
|
+
"""Send an HTTP request with multiple items and handle retries.
|
|
429
|
+
|
|
430
|
+
This method will keep retrying the request until it either succeeds or
|
|
431
|
+
exhausts the maximum number of retries.
|
|
432
|
+
|
|
433
|
+
Note this method will use the current thread to process all request, thus
|
|
434
|
+
it is blocking.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
message (ItemsRequest2): The request message to send.
|
|
438
|
+
Returns:
|
|
439
|
+
Sequence[ItemsResultMessage2]: The final response message, which can be either successful response or failed request.
|
|
440
|
+
"""
|
|
441
|
+
if message.total_attempts > 0:
|
|
442
|
+
raise RuntimeError(f"ItemsRequest2 has already been attempted {message.total_attempts} times.")
|
|
443
|
+
pending_requests: deque[ItemsRequest2] = deque()
|
|
444
|
+
pending_requests.append(message)
|
|
445
|
+
final_responses: list[ItemsResultMessage2] = []
|
|
446
|
+
while pending_requests:
|
|
447
|
+
current_request = pending_requests.popleft()
|
|
448
|
+
results = self.request_items(current_request)
|
|
449
|
+
|
|
450
|
+
for result in results:
|
|
451
|
+
if isinstance(result, ItemsRequest2):
|
|
452
|
+
pending_requests.append(result)
|
|
453
|
+
elif isinstance(result, ItemsResultMessage2):
|
|
454
|
+
final_responses.append(result)
|
|
455
|
+
else:
|
|
456
|
+
raise TypeError(f"Unexpected result type: {type(result)}")
|
|
457
|
+
|
|
458
|
+
return final_responses
|
|
459
|
+
|
|
460
|
+
def _handle_items_response(
|
|
461
|
+
self, response: httpx.Response, request: ItemsRequest2
|
|
462
|
+
) -> Sequence[ItemsRequest2 | ItemsResultMessage2]:
|
|
463
|
+
if 200 <= response.status_code < 300:
|
|
464
|
+
return [
|
|
465
|
+
ItemsSuccessResponse2(
|
|
466
|
+
ids=[item.as_id() for item in request.items],
|
|
467
|
+
status_code=response.status_code,
|
|
468
|
+
body=response.text,
|
|
469
|
+
content=response.content,
|
|
470
|
+
)
|
|
471
|
+
]
|
|
472
|
+
elif len(request.items) > 1 and response.status_code in self._split_items_status_codes:
|
|
473
|
+
# 4XX: Status there is at least one item that is invalid, split the batch to get all valid items processed
|
|
474
|
+
# 5xx: Server error, split to reduce the number of items in each request, and count as a status attempt
|
|
475
|
+
status_attempts = request.status_attempt
|
|
476
|
+
if 500 <= response.status_code < 600:
|
|
477
|
+
status_attempts += 1
|
|
478
|
+
splits = request.split(status_attempts=status_attempts)
|
|
479
|
+
if splits[0].tracker and splits[0].tracker.limit_reached():
|
|
480
|
+
return [
|
|
481
|
+
ItemsFailedResponse2(
|
|
482
|
+
ids=[item.as_id() for item in request.items],
|
|
483
|
+
status_code=response.status_code,
|
|
484
|
+
body=response.text,
|
|
485
|
+
error=ErrorDetails2.from_response(response),
|
|
486
|
+
)
|
|
487
|
+
]
|
|
488
|
+
return splits
|
|
489
|
+
|
|
490
|
+
if retry_request := self._retry_request(response, request):
|
|
491
|
+
return [retry_request]
|
|
492
|
+
else:
|
|
493
|
+
# Permanent failure
|
|
494
|
+
return [
|
|
495
|
+
ItemsFailedResponse2(
|
|
496
|
+
ids=[item.as_id() for item in request.items],
|
|
497
|
+
status_code=response.status_code,
|
|
498
|
+
body=response.text,
|
|
499
|
+
error=ErrorDetails2.from_response(response),
|
|
500
|
+
)
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
def _handle_items_error(
|
|
504
|
+
self, e: Exception, request: ItemsRequest2
|
|
505
|
+
) -> Sequence[ItemsRequest2 | ItemsResultMessage2]:
|
|
506
|
+
if isinstance(e, httpx.ReadTimeout | httpx.TimeoutException):
|
|
507
|
+
error_type = "read"
|
|
508
|
+
request.read_attempt += 1
|
|
509
|
+
attempts = request.read_attempt
|
|
510
|
+
elif isinstance(e, ConnectionError | httpx.ConnectError | httpx.ConnectTimeout):
|
|
511
|
+
error_type = "connect"
|
|
512
|
+
request.connect_attempt += 1
|
|
513
|
+
attempts = request.connect_attempt
|
|
514
|
+
else:
|
|
515
|
+
error_msg = f"Unexpected exception: {e!s}"
|
|
516
|
+
return [
|
|
517
|
+
ItemsFailedRequest2(
|
|
518
|
+
ids=[item.as_id() for item in request.items],
|
|
519
|
+
error_message=error_msg,
|
|
520
|
+
)
|
|
521
|
+
]
|
|
522
|
+
|
|
523
|
+
if attempts <= self._max_retries:
|
|
524
|
+
time.sleep(self._backoff_time(request.total_attempts))
|
|
525
|
+
return [request]
|
|
526
|
+
else:
|
|
527
|
+
# We have already incremented the attempt count, so we subtract 1 here
|
|
528
|
+
error_msg = f"RequestException after {request.total_attempts - 1} attempts ({error_type} error): {e!s}"
|
|
529
|
+
|
|
530
|
+
return [
|
|
531
|
+
ItemsFailedRequest2(
|
|
532
|
+
ids=[item.as_id() for item in request.items],
|
|
533
|
+
error_message=error_msg,
|
|
534
|
+
)
|
|
535
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from collections import UserList
|
|
3
|
-
from collections.abc import Sequence
|
|
3
|
+
from collections.abc import Hashable, Sequence
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import Generic, Literal, Protocol, TypeAlias, TypeVar
|
|
6
6
|
|
|
@@ -87,13 +87,14 @@ class RequestMessage(HTTPMessage):
|
|
|
87
87
|
"""Base class for HTTP request messages"""
|
|
88
88
|
|
|
89
89
|
endpoint_url: str
|
|
90
|
-
method: Literal["GET", "POST", "PATCH", "DELETE"]
|
|
90
|
+
method: Literal["GET", "POST", "PATCH", "DELETE", "PUT"]
|
|
91
91
|
connect_attempt: int = 0
|
|
92
92
|
read_attempt: int = 0
|
|
93
93
|
status_attempt: int = 0
|
|
94
94
|
api_version: str | None = None
|
|
95
95
|
content_type: str = "application/json"
|
|
96
96
|
accept: str = "application/json"
|
|
97
|
+
content_length: int | None = None
|
|
97
98
|
|
|
98
99
|
@property
|
|
99
100
|
def total_attempts(self) -> int:
|
|
@@ -115,6 +116,13 @@ class RequestMessage(HTTPMessage):
|
|
|
115
116
|
@dataclass
|
|
116
117
|
class SuccessResponse(ResponseMessage):
|
|
117
118
|
body: str
|
|
119
|
+
content: bytes
|
|
120
|
+
|
|
121
|
+
def dump(self) -> dict[str, JsonVal]:
|
|
122
|
+
output = super().dump()
|
|
123
|
+
# We cannot serialize bytes, so we indicate its presence instead
|
|
124
|
+
output["content"] = "<bytes>" if self.content else None
|
|
125
|
+
return output
|
|
118
126
|
|
|
119
127
|
|
|
120
128
|
@dataclass
|
|
@@ -134,7 +142,7 @@ class SimpleRequest(RequestMessage):
|
|
|
134
142
|
|
|
135
143
|
@classmethod
|
|
136
144
|
def create_success_response(cls, response: httpx.Response) -> Sequence[ResponseMessage]:
|
|
137
|
-
return [SuccessResponse(status_code=response.status_code, body=response.text)]
|
|
145
|
+
return [SuccessResponse(status_code=response.status_code, body=response.text, content=response.content)]
|
|
138
146
|
|
|
139
147
|
@classmethod
|
|
140
148
|
def create_failure_response(cls, response: httpx.Response) -> Sequence[HTTPMessage]:
|
|
@@ -309,7 +317,11 @@ class ItemsRequest(Generic[T_COVARIANT_ID], BodyRequest):
|
|
|
309
317
|
|
|
310
318
|
def create_success_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
|
|
311
319
|
ids = [item.as_id() for item in self.items]
|
|
312
|
-
return [
|
|
320
|
+
return [
|
|
321
|
+
SuccessResponseItems(
|
|
322
|
+
status_code=response.status_code, ids=ids, body=response.text, content=response.content
|
|
323
|
+
)
|
|
324
|
+
]
|
|
313
325
|
|
|
314
326
|
def create_failure_response(self, response: httpx.Response) -> Sequence[HTTPMessage]:
|
|
315
327
|
error = ErrorDetails.from_response(response)
|
|
@@ -338,6 +350,18 @@ class ResponseList(UserList[ResponseMessage | FailedRequestMessage]):
|
|
|
338
350
|
error_messages += "; ".join(f"Request error: {err.error}" for err in failed_requests)
|
|
339
351
|
raise ToolkitAPIError(f"One or more requests failed: {error_messages}")
|
|
340
352
|
|
|
353
|
+
@property
|
|
354
|
+
def has_failed(self) -> bool:
|
|
355
|
+
"""Indicates whether any response in the list indicates a failure.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
bool: True if there are any failed responses or requests, False otherwise.
|
|
359
|
+
"""
|
|
360
|
+
for resp in self.data:
|
|
361
|
+
if isinstance(resp, FailedResponse | FailedRequestMessage):
|
|
362
|
+
return True
|
|
363
|
+
return False
|
|
364
|
+
|
|
341
365
|
def get_first_body(self) -> dict[str, JsonVal]:
|
|
342
366
|
"""Returns the body of the first successful response in the list.
|
|
343
367
|
|
|
@@ -352,6 +376,28 @@ class ResponseList(UserList[ResponseMessage | FailedRequestMessage]):
|
|
|
352
376
|
return _json.loads(resp.body)
|
|
353
377
|
raise ValueError("No successful responses with a body found.")
|
|
354
378
|
|
|
379
|
+
def as_item_responses(self, item_id: Hashable) -> list[ResponseMessage | FailedRequestMessage]:
|
|
380
|
+
# Convert the responses to per-item responses
|
|
381
|
+
results: list[ResponseMessage | FailedRequestMessage] = []
|
|
382
|
+
for message in self.data:
|
|
383
|
+
if isinstance(message, SuccessResponse):
|
|
384
|
+
results.append(
|
|
385
|
+
SuccessResponseItems(
|
|
386
|
+
status_code=message.status_code, content=message.content, ids=[item_id], body=message.body
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
elif isinstance(message, FailedResponse):
|
|
390
|
+
results.append(
|
|
391
|
+
FailedResponseItems(
|
|
392
|
+
status_code=message.status_code, ids=[item_id], body=message.body, error=message.error
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
elif isinstance(message, FailedRequestMessage):
|
|
396
|
+
results.append(FailedRequestItems(ids=[item_id], error=message.error))
|
|
397
|
+
else:
|
|
398
|
+
results.append(message)
|
|
399
|
+
return results
|
|
400
|
+
|
|
355
401
|
|
|
356
402
|
def _dump_body(body: dict[str, JsonVal]) -> str:
|
|
357
403
|
try:
|