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