nextmv 0.18.0__py3-none-any.whl → 1.0.0.dev2__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.
- nextmv/__about__.py +1 -1
- nextmv/__entrypoint__.py +8 -13
- nextmv/__init__.py +53 -0
- nextmv/_serialization.py +96 -0
- nextmv/base_model.py +54 -9
- nextmv/cli/CONTRIBUTING.md +511 -0
- nextmv/cli/__init__.py +0 -0
- nextmv/cli/cloud/__init__.py +47 -0
- nextmv/cli/cloud/acceptance/__init__.py +27 -0
- nextmv/cli/cloud/acceptance/create.py +393 -0
- nextmv/cli/cloud/acceptance/delete.py +68 -0
- nextmv/cli/cloud/acceptance/get.py +104 -0
- nextmv/cli/cloud/acceptance/list.py +62 -0
- nextmv/cli/cloud/acceptance/update.py +95 -0
- nextmv/cli/cloud/account/__init__.py +28 -0
- nextmv/cli/cloud/account/create.py +83 -0
- nextmv/cli/cloud/account/delete.py +60 -0
- nextmv/cli/cloud/account/get.py +66 -0
- nextmv/cli/cloud/account/update.py +70 -0
- nextmv/cli/cloud/app/__init__.py +35 -0
- nextmv/cli/cloud/app/create.py +141 -0
- nextmv/cli/cloud/app/delete.py +58 -0
- nextmv/cli/cloud/app/exists.py +44 -0
- nextmv/cli/cloud/app/get.py +66 -0
- nextmv/cli/cloud/app/list.py +61 -0
- nextmv/cli/cloud/app/push.py +137 -0
- nextmv/cli/cloud/app/update.py +124 -0
- nextmv/cli/cloud/batch/__init__.py +29 -0
- nextmv/cli/cloud/batch/create.py +454 -0
- nextmv/cli/cloud/batch/delete.py +68 -0
- nextmv/cli/cloud/batch/get.py +104 -0
- nextmv/cli/cloud/batch/list.py +63 -0
- nextmv/cli/cloud/batch/metadata.py +66 -0
- nextmv/cli/cloud/batch/update.py +95 -0
- nextmv/cli/cloud/data/__init__.py +26 -0
- nextmv/cli/cloud/data/upload.py +162 -0
- nextmv/cli/cloud/ensemble/__init__.py +31 -0
- nextmv/cli/cloud/ensemble/create.py +414 -0
- nextmv/cli/cloud/ensemble/delete.py +67 -0
- nextmv/cli/cloud/ensemble/get.py +65 -0
- nextmv/cli/cloud/ensemble/update.py +103 -0
- nextmv/cli/cloud/input_set/__init__.py +30 -0
- nextmv/cli/cloud/input_set/create.py +170 -0
- nextmv/cli/cloud/input_set/get.py +63 -0
- nextmv/cli/cloud/input_set/list.py +63 -0
- nextmv/cli/cloud/input_set/update.py +123 -0
- nextmv/cli/cloud/instance/__init__.py +35 -0
- nextmv/cli/cloud/instance/create.py +290 -0
- nextmv/cli/cloud/instance/delete.py +62 -0
- nextmv/cli/cloud/instance/exists.py +39 -0
- nextmv/cli/cloud/instance/get.py +62 -0
- nextmv/cli/cloud/instance/list.py +60 -0
- nextmv/cli/cloud/instance/update.py +216 -0
- nextmv/cli/cloud/managed_input/__init__.py +31 -0
- nextmv/cli/cloud/managed_input/create.py +146 -0
- nextmv/cli/cloud/managed_input/delete.py +65 -0
- nextmv/cli/cloud/managed_input/get.py +63 -0
- nextmv/cli/cloud/managed_input/list.py +60 -0
- nextmv/cli/cloud/managed_input/update.py +97 -0
- nextmv/cli/cloud/run/__init__.py +37 -0
- nextmv/cli/cloud/run/cancel.py +37 -0
- nextmv/cli/cloud/run/create.py +530 -0
- nextmv/cli/cloud/run/get.py +199 -0
- nextmv/cli/cloud/run/input.py +86 -0
- nextmv/cli/cloud/run/list.py +80 -0
- nextmv/cli/cloud/run/logs.py +167 -0
- nextmv/cli/cloud/run/metadata.py +67 -0
- nextmv/cli/cloud/run/track.py +501 -0
- nextmv/cli/cloud/scenario/__init__.py +29 -0
- nextmv/cli/cloud/scenario/create.py +451 -0
- nextmv/cli/cloud/scenario/delete.py +65 -0
- nextmv/cli/cloud/scenario/get.py +102 -0
- nextmv/cli/cloud/scenario/list.py +63 -0
- nextmv/cli/cloud/scenario/metadata.py +67 -0
- nextmv/cli/cloud/scenario/update.py +93 -0
- nextmv/cli/cloud/secrets/__init__.py +33 -0
- nextmv/cli/cloud/secrets/create.py +206 -0
- nextmv/cli/cloud/secrets/delete.py +67 -0
- nextmv/cli/cloud/secrets/get.py +66 -0
- nextmv/cli/cloud/secrets/list.py +60 -0
- nextmv/cli/cloud/secrets/update.py +147 -0
- nextmv/cli/cloud/shadow/__init__.py +33 -0
- nextmv/cli/cloud/shadow/create.py +184 -0
- nextmv/cli/cloud/shadow/delete.py +68 -0
- nextmv/cli/cloud/shadow/get.py +61 -0
- nextmv/cli/cloud/shadow/list.py +63 -0
- nextmv/cli/cloud/shadow/metadata.py +66 -0
- nextmv/cli/cloud/shadow/start.py +43 -0
- nextmv/cli/cloud/shadow/stop.py +43 -0
- nextmv/cli/cloud/shadow/update.py +95 -0
- nextmv/cli/cloud/upload/__init__.py +22 -0
- nextmv/cli/cloud/upload/create.py +39 -0
- nextmv/cli/cloud/version/__init__.py +33 -0
- nextmv/cli/cloud/version/create.py +97 -0
- nextmv/cli/cloud/version/delete.py +62 -0
- nextmv/cli/cloud/version/exists.py +39 -0
- nextmv/cli/cloud/version/get.py +62 -0
- nextmv/cli/cloud/version/list.py +60 -0
- nextmv/cli/cloud/version/update.py +92 -0
- nextmv/cli/community/__init__.py +24 -0
- nextmv/cli/community/clone.py +270 -0
- nextmv/cli/community/list.py +265 -0
- nextmv/cli/configuration/__init__.py +23 -0
- nextmv/cli/configuration/config.py +195 -0
- nextmv/cli/configuration/create.py +94 -0
- nextmv/cli/configuration/delete.py +67 -0
- nextmv/cli/configuration/list.py +77 -0
- nextmv/cli/main.py +188 -0
- nextmv/cli/message.py +153 -0
- nextmv/cli/options.py +206 -0
- nextmv/cli/version.py +38 -0
- nextmv/cloud/__init__.py +71 -17
- nextmv/cloud/acceptance_test.py +757 -51
- nextmv/cloud/account.py +406 -17
- nextmv/cloud/application/__init__.py +957 -0
- nextmv/cloud/application/_acceptance.py +419 -0
- nextmv/cloud/application/_batch_scenario.py +860 -0
- nextmv/cloud/application/_ensemble.py +251 -0
- nextmv/cloud/application/_input_set.py +227 -0
- nextmv/cloud/application/_instance.py +289 -0
- nextmv/cloud/application/_managed_input.py +227 -0
- nextmv/cloud/application/_run.py +1393 -0
- nextmv/cloud/application/_secrets.py +294 -0
- nextmv/cloud/application/_shadow.py +314 -0
- nextmv/cloud/application/_utils.py +54 -0
- nextmv/cloud/application/_version.py +303 -0
- nextmv/cloud/assets.py +48 -0
- nextmv/cloud/batch_experiment.py +294 -33
- nextmv/cloud/client.py +307 -66
- nextmv/cloud/ensemble.py +247 -0
- nextmv/cloud/input_set.py +120 -2
- nextmv/cloud/instance.py +133 -8
- nextmv/cloud/integration.py +533 -0
- nextmv/cloud/package.py +168 -53
- nextmv/cloud/scenario.py +410 -0
- nextmv/cloud/secrets.py +234 -0
- nextmv/cloud/shadow.py +190 -0
- nextmv/cloud/url.py +73 -0
- nextmv/cloud/version.py +132 -4
- nextmv/default_app/.gitignore +1 -0
- nextmv/default_app/README.md +32 -0
- nextmv/default_app/app.yaml +12 -0
- nextmv/default_app/input.json +5 -0
- nextmv/default_app/main.py +37 -0
- nextmv/default_app/requirements.txt +2 -0
- nextmv/default_app/src/__init__.py +0 -0
- nextmv/default_app/src/visuals.py +36 -0
- nextmv/deprecated.py +47 -0
- nextmv/input.py +861 -90
- nextmv/local/__init__.py +5 -0
- nextmv/local/application.py +1251 -0
- nextmv/local/executor.py +1042 -0
- nextmv/local/geojson_handler.py +323 -0
- nextmv/local/local.py +97 -0
- nextmv/local/plotly_handler.py +61 -0
- nextmv/local/runner.py +274 -0
- nextmv/logger.py +80 -9
- nextmv/manifest.py +1466 -0
- nextmv/model.py +241 -66
- nextmv/options.py +708 -115
- nextmv/output.py +1301 -274
- nextmv/polling.py +325 -0
- nextmv/run.py +1702 -0
- nextmv/safe.py +145 -0
- nextmv/status.py +122 -0
- nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
- nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
- {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
- nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
- nextmv/cloud/application.py +0 -1405
- nextmv/cloud/manifest.py +0 -234
- nextmv/cloud/status.py +0 -29
- nextmv-0.18.0.dist-info/METADATA +0 -770
- nextmv-0.18.0.dist-info/RECORD +0 -25
- {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
nextmv/cloud/client.py
CHANGED
|
@@ -1,29 +1,104 @@
|
|
|
1
|
-
"""Module with the client class.
|
|
1
|
+
"""Module with the client class.
|
|
2
|
+
|
|
3
|
+
This module provides the `Client` class for interacting with the Nextmv Cloud
|
|
4
|
+
API, and a helper function `get_size` to determine the size of objects.
|
|
5
|
+
|
|
6
|
+
Classes
|
|
7
|
+
-------
|
|
8
|
+
Client
|
|
9
|
+
Client that interacts directly with the Nextmv Cloud API.
|
|
10
|
+
|
|
11
|
+
Functions
|
|
12
|
+
---------
|
|
13
|
+
get_size(obj)
|
|
14
|
+
Finds the size of an object in bytes.
|
|
15
|
+
"""
|
|
2
16
|
|
|
3
|
-
import json
|
|
4
17
|
import os
|
|
5
18
|
from dataclasses import dataclass, field
|
|
6
|
-
from typing import IO, Any
|
|
19
|
+
from typing import IO, Any
|
|
7
20
|
from urllib.parse import urljoin
|
|
8
21
|
|
|
9
22
|
import requests
|
|
10
23
|
import yaml
|
|
11
24
|
from requests.adapters import HTTPAdapter, Retry
|
|
12
25
|
|
|
26
|
+
from nextmv._serialization import deflated_serialize_json
|
|
27
|
+
|
|
13
28
|
_MAX_LAMBDA_PAYLOAD_SIZE: int = 500 * 1024 * 1024
|
|
14
|
-
"""Maximum size of the payload handled by the Nextmv Cloud API.
|
|
29
|
+
"""int: Maximum size of the payload handled by the Nextmv Cloud API.
|
|
30
|
+
|
|
31
|
+
This constant defines the upper limit for the size of data payloads that can
|
|
32
|
+
be sent to the Nextmv Cloud API, specifically for lambda functions. It is set
|
|
33
|
+
to 500 MiB.
|
|
34
|
+
"""
|
|
15
35
|
|
|
16
36
|
|
|
17
37
|
@dataclass
|
|
18
38
|
class Client:
|
|
19
39
|
"""
|
|
20
|
-
Client that interacts directly with the Nextmv Cloud API.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
Client that interacts directly with the Nextmv Cloud API.
|
|
41
|
+
|
|
42
|
+
You can import the `Client` class directly from `cloud`:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from nextmv.cloud import Client
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The API key will be searched, in order of precedence, in:
|
|
49
|
+
|
|
50
|
+
1. The `api_key` argument in the constructor.
|
|
51
|
+
2. The `NEXTMV_API_KEY` environment variable.
|
|
52
|
+
3. The `~/.nextmv/config.yaml` file used by the Nextmv CLI.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
api_key : str, optional
|
|
57
|
+
API key to use for authenticating with the Nextmv Cloud API. If not
|
|
58
|
+
provided, the client will look for the `NEXTMV_API_KEY` environment
|
|
59
|
+
variable.
|
|
60
|
+
allowed_methods : list[str]
|
|
61
|
+
Allowed HTTP methods to use for retries in requests to the Nextmv
|
|
62
|
+
Cloud API. Defaults to ``["GET", "POST", "PUT", "DELETE"]``.
|
|
63
|
+
backoff_factor : float
|
|
64
|
+
Exponential backoff factor to use for requests to the Nextmv Cloud
|
|
65
|
+
API. Defaults to ``1``.
|
|
66
|
+
backoff_jitter : float
|
|
67
|
+
Jitter to use for requests to the Nextmv Cloud API when backing off.
|
|
68
|
+
Defaults to ``0.1``.
|
|
69
|
+
backoff_max : float
|
|
70
|
+
Maximum backoff time to use for requests to the Nextmv Cloud API, in
|
|
71
|
+
seconds. Defaults to ``60``.
|
|
72
|
+
configuration_file : str
|
|
73
|
+
Path to the configuration file used by the Nextmv CLI. Defaults to
|
|
74
|
+
``"~/.nextmv/config.yaml"``.
|
|
75
|
+
headers : dict[str, str], optional
|
|
76
|
+
Headers to use for requests to the Nextmv Cloud API. Automatically
|
|
77
|
+
set up with the API key.
|
|
78
|
+
max_retries : int
|
|
79
|
+
Maximum number of retries to use for requests to the Nextmv Cloud
|
|
80
|
+
API. Defaults to ``10``.
|
|
81
|
+
status_forcelist : list[int]
|
|
82
|
+
Status codes to retry for requests to the Nextmv Cloud API. Defaults
|
|
83
|
+
to ``[429]``.
|
|
84
|
+
timeout : float
|
|
85
|
+
Timeout to use for requests to the Nextmv Cloud API, in seconds.
|
|
86
|
+
Defaults to ``20``.
|
|
87
|
+
url : str
|
|
88
|
+
URL of the Nextmv Cloud API. Defaults to
|
|
89
|
+
``"https://api.cloud.nextmv.io"``.
|
|
90
|
+
console_url : str
|
|
91
|
+
URL of the Nextmv Cloud console. Defaults to
|
|
92
|
+
``"https://cloud.nextmv.io"``.
|
|
93
|
+
|
|
94
|
+
Examples
|
|
95
|
+
--------
|
|
96
|
+
>>> client = Client(api_key="YOUR_API_KEY")
|
|
97
|
+
>>> response = client.request(method="GET", endpoint="/v1/applications")
|
|
98
|
+
>>> print(response.json())
|
|
24
99
|
"""
|
|
25
100
|
|
|
26
|
-
api_key:
|
|
101
|
+
api_key: str | None = None
|
|
27
102
|
"""API key to use for authenticating with the Nextmv Cloud API. If not
|
|
28
103
|
provided, the client will look for the NEXTMV_API_KEY environment
|
|
29
104
|
variable."""
|
|
@@ -42,22 +117,40 @@ class Client:
|
|
|
42
117
|
seconds."""
|
|
43
118
|
configuration_file: str = "~/.nextmv/config.yaml"
|
|
44
119
|
"""Path to the configuration file used by the Nextmv CLI."""
|
|
45
|
-
headers:
|
|
120
|
+
headers: dict[str, str] | None = None
|
|
46
121
|
"""Headers to use for requests to the Nextmv Cloud API."""
|
|
47
122
|
max_retries: int = 10
|
|
48
123
|
"""Maximum number of retries to use for requests to the Nextmv Cloud
|
|
49
124
|
API."""
|
|
50
125
|
status_forcelist: list[int] = field(
|
|
51
|
-
default_factory=lambda: [429
|
|
126
|
+
default_factory=lambda: [429],
|
|
52
127
|
)
|
|
53
128
|
"""Status codes to retry for requests to the Nextmv Cloud API."""
|
|
54
129
|
timeout: float = 20
|
|
55
130
|
"""Timeout to use for requests to the Nextmv Cloud API."""
|
|
56
131
|
url: str = "https://api.cloud.nextmv.io"
|
|
57
132
|
"""URL of the Nextmv Cloud API."""
|
|
133
|
+
console_url: str = "https://cloud.nextmv.io"
|
|
134
|
+
"""URL of the Nextmv Cloud console."""
|
|
58
135
|
|
|
59
136
|
def __post_init__(self):
|
|
60
|
-
"""
|
|
137
|
+
"""
|
|
138
|
+
Initializes the client after dataclass construction.
|
|
139
|
+
|
|
140
|
+
This method handles the logic for API key retrieval and header
|
|
141
|
+
setup. It checks for the API key in the constructor, environment
|
|
142
|
+
variables, and the configuration file, in that order.
|
|
143
|
+
|
|
144
|
+
Raises
|
|
145
|
+
------
|
|
146
|
+
ValueError
|
|
147
|
+
If `api_key` is an empty string.
|
|
148
|
+
If no API key is found in any of the lookup locations.
|
|
149
|
+
If a profile is specified via `NEXTMV_PROFILE` but not found in
|
|
150
|
+
the configuration file.
|
|
151
|
+
If `apikey` is not found in the configuration file for the
|
|
152
|
+
selected profile.
|
|
153
|
+
"""
|
|
61
154
|
|
|
62
155
|
if self.api_key is not None and self.api_key != "":
|
|
63
156
|
self._set_headers_api_key(self.api_key)
|
|
@@ -75,7 +168,7 @@ class Client:
|
|
|
75
168
|
config_path = os.path.expanduser(self.configuration_file)
|
|
76
169
|
if not os.path.exists(config_path):
|
|
77
170
|
raise ValueError(
|
|
78
|
-
"no API key set in constructor or NEXTMV_API_KEY env var, and
|
|
171
|
+
f"no API key set in constructor or NEXTMV_API_KEY env var, and {self.configuration_file} does not exist"
|
|
79
172
|
)
|
|
80
173
|
|
|
81
174
|
with open(config_path) as f:
|
|
@@ -86,11 +179,11 @@ class Client:
|
|
|
86
179
|
if profile is not None:
|
|
87
180
|
parent = config.get(profile)
|
|
88
181
|
if parent is None:
|
|
89
|
-
raise ValueError(f"profile {profile} set via NEXTMV_PROFILE but not found in
|
|
182
|
+
raise ValueError(f"profile {profile} set via NEXTMV_PROFILE but not found in {self.configuration_file}")
|
|
90
183
|
|
|
91
184
|
api_key = parent.get("apikey")
|
|
92
185
|
if api_key is None:
|
|
93
|
-
raise ValueError("no apiKey found in
|
|
186
|
+
raise ValueError(f"no apiKey found in {self.configuration_file}")
|
|
94
187
|
self.api_key = api_key
|
|
95
188
|
|
|
96
189
|
endpoint = parent.get("endpoint")
|
|
@@ -103,46 +196,91 @@ class Client:
|
|
|
103
196
|
self,
|
|
104
197
|
method: str,
|
|
105
198
|
endpoint: str,
|
|
106
|
-
data:
|
|
107
|
-
headers:
|
|
108
|
-
payload:
|
|
109
|
-
query_params:
|
|
199
|
+
data: Any | None = None,
|
|
200
|
+
headers: dict[str, str] | None = None,
|
|
201
|
+
payload: dict[str, Any] | None = None,
|
|
202
|
+
query_params: dict[str, Any] | None = None,
|
|
203
|
+
json_configurations: dict[str, Any] | None = None,
|
|
110
204
|
) -> requests.Response:
|
|
111
205
|
"""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
206
|
+
Makes a request to the Nextmv Cloud API.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
method : str
|
|
211
|
+
HTTP method to use (e.g., "GET", "POST").
|
|
212
|
+
endpoint : str
|
|
213
|
+
API endpoint to send the request to (e.g., "/v1/applications").
|
|
214
|
+
data : Any, optional
|
|
215
|
+
Data to send in the request body. Typically used for form data.
|
|
216
|
+
Cannot be used if `payload` is also provided.
|
|
217
|
+
headers : dict[str, str], optional
|
|
218
|
+
Additional headers to send with the request. These will override
|
|
219
|
+
the default client headers if keys conflict.
|
|
220
|
+
payload : dict[str, Any], optional
|
|
221
|
+
JSON payload to send with the request. Prefer using this over
|
|
222
|
+
`data` for JSON requests. Cannot be used if `data` is also
|
|
223
|
+
provided.
|
|
224
|
+
query_params : dict[str, Any], optional
|
|
225
|
+
Query parameters to append to the request URL.
|
|
226
|
+
json_configurations : dict[str, Any], optional
|
|
227
|
+
Additional configurations for JSON serialization. This allows
|
|
228
|
+
customization of the Python `json.dumps` function, such as
|
|
229
|
+
specifying `indent` for pretty printing or `default` for custom
|
|
230
|
+
serialization functions.
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
requests.Response
|
|
235
|
+
The response object from the Nextmv Cloud API.
|
|
236
|
+
|
|
237
|
+
Raises
|
|
238
|
+
------
|
|
239
|
+
requests.HTTPError
|
|
240
|
+
If the response status code is not in the 2xx range.
|
|
241
|
+
ValueError
|
|
242
|
+
If both `data` and `payload` are provided.
|
|
243
|
+
If the `payload` size exceeds `_MAX_LAMBDA_PAYLOAD_SIZE`.
|
|
244
|
+
If the `data` size exceeds `_MAX_LAMBDA_PAYLOAD_SIZE`.
|
|
245
|
+
|
|
246
|
+
Examples
|
|
247
|
+
--------
|
|
248
|
+
>>> client = Client(api_key="YOUR_API_KEY")
|
|
249
|
+
>>> # Get a list of applications
|
|
250
|
+
>>> response = client.request(method="GET", endpoint="/v1/applications")
|
|
251
|
+
>>> print(response.status_code)
|
|
252
|
+
200
|
|
253
|
+
>>> # Create a new run
|
|
254
|
+
>>> run_payload = {
|
|
255
|
+
... "applicationId": "app_id",
|
|
256
|
+
... "instanceId": "instance_id",
|
|
257
|
+
... "input": {"value": 10}
|
|
258
|
+
... }
|
|
259
|
+
>>> response = client.request(
|
|
260
|
+
... method="POST",
|
|
261
|
+
... endpoint="/v1/runs",
|
|
262
|
+
... payload=run_payload
|
|
263
|
+
... )
|
|
264
|
+
>>> print(response.json()["id"])
|
|
265
|
+
run_xxxxxxxxxxxx
|
|
131
266
|
"""
|
|
132
267
|
|
|
133
268
|
if payload is not None and data is not None:
|
|
134
269
|
raise ValueError("cannot use both data and payload")
|
|
135
270
|
|
|
136
|
-
if
|
|
271
|
+
if (
|
|
272
|
+
payload is not None
|
|
273
|
+
and get_size(payload, json_configurations=json_configurations) > _MAX_LAMBDA_PAYLOAD_SIZE
|
|
274
|
+
):
|
|
137
275
|
raise ValueError(
|
|
138
|
-
f"payload size of {get_size(payload)} bytes exceeds
|
|
139
|
-
f"allowed size of {_MAX_LAMBDA_PAYLOAD_SIZE} bytes"
|
|
276
|
+
f"payload size of {get_size(payload, json_configurations=json_configurations)} bytes exceeds "
|
|
277
|
+
+ f"the maximum allowed size of {_MAX_LAMBDA_PAYLOAD_SIZE} bytes"
|
|
140
278
|
)
|
|
141
279
|
|
|
142
|
-
if data is not None and get_size(data) > _MAX_LAMBDA_PAYLOAD_SIZE:
|
|
280
|
+
if data is not None and get_size(data, json_configurations=json_configurations) > _MAX_LAMBDA_PAYLOAD_SIZE:
|
|
143
281
|
raise ValueError(
|
|
144
|
-
f"data size of {get_size(data)} bytes exceeds
|
|
145
|
-
f"allowed size of {_MAX_LAMBDA_PAYLOAD_SIZE} bytes"
|
|
282
|
+
f"data size of {get_size(data, json_configurations=json_configurations)} bytes exceeds "
|
|
283
|
+
+ f"the maximum allowed size of {_MAX_LAMBDA_PAYLOAD_SIZE} bytes"
|
|
146
284
|
)
|
|
147
285
|
|
|
148
286
|
session = requests.Session()
|
|
@@ -157,7 +295,7 @@ class Client:
|
|
|
157
295
|
adapter = HTTPAdapter(max_retries=retries)
|
|
158
296
|
session.mount("https://", adapter)
|
|
159
297
|
|
|
160
|
-
kwargs = {
|
|
298
|
+
kwargs: dict[str, Any] = {
|
|
161
299
|
"url": urljoin(self.url, endpoint),
|
|
162
300
|
"timeout": self.timeout,
|
|
163
301
|
}
|
|
@@ -165,7 +303,11 @@ class Client:
|
|
|
165
303
|
if data is not None:
|
|
166
304
|
kwargs["data"] = data
|
|
167
305
|
if payload is not None:
|
|
168
|
-
|
|
306
|
+
if isinstance(payload, (dict, list)):
|
|
307
|
+
data = deflated_serialize_json(payload, json_configurations=json_configurations)
|
|
308
|
+
kwargs["data"] = data
|
|
309
|
+
else:
|
|
310
|
+
raise ValueError("payload must be a dictionary or a list")
|
|
169
311
|
if query_params is not None:
|
|
170
312
|
kwargs["params"] = query_params
|
|
171
313
|
|
|
@@ -182,23 +324,59 @@ class Client:
|
|
|
182
324
|
|
|
183
325
|
def upload_to_presigned_url(
|
|
184
326
|
self,
|
|
185
|
-
data:
|
|
327
|
+
data: dict[str, Any] | str | None,
|
|
186
328
|
url: str,
|
|
329
|
+
json_configurations: dict[str, Any] | None = None,
|
|
330
|
+
tar_file: str | None = None,
|
|
187
331
|
) -> None:
|
|
188
332
|
"""
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
333
|
+
Uploads data to a presigned URL.
|
|
334
|
+
|
|
335
|
+
This method is typically used for uploading large input or output files
|
|
336
|
+
directly to cloud storage, bypassing the main API for efficiency.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
data : Union[dict[str, Any], str], optional
|
|
341
|
+
The data to upload. If a dictionary is provided, it will be
|
|
342
|
+
JSON-serialized. If a string is provided, it will be uploaded
|
|
343
|
+
as is.
|
|
344
|
+
url : str
|
|
345
|
+
The presigned URL to which the data will be uploaded.
|
|
346
|
+
json_configurations : dict[str, Any], optional
|
|
347
|
+
Additional configurations for JSON serialization. This allows
|
|
348
|
+
customization of the Python `json.dumps` function, such as
|
|
349
|
+
specifying `indent` for pretty printing or `default` for custom
|
|
350
|
+
serialization functions.
|
|
351
|
+
tar_file : str, optional
|
|
352
|
+
If provided, this will be used to upload a tar file instead of
|
|
353
|
+
a JSON string or dictionary. This is useful for uploading large
|
|
354
|
+
files that are already packaged as a tarball. If this is provided,
|
|
355
|
+
`data` is expected to be `None`.
|
|
356
|
+
|
|
357
|
+
Raises
|
|
358
|
+
------
|
|
359
|
+
ValueError
|
|
360
|
+
If `data` is not a dictionary or a string.
|
|
361
|
+
requests.HTTPError
|
|
362
|
+
If the upload request fails.
|
|
363
|
+
|
|
364
|
+
Examples
|
|
365
|
+
--------
|
|
366
|
+
Assume `presigned_upload_url` is obtained from a previous API call.
|
|
367
|
+
>>> client = Client(api_key="YOUR_API_KEY")
|
|
368
|
+
>>> input_data = {"value": 42, "items": [1, 2, 3]}
|
|
369
|
+
>>> client.upload_to_presigned_url(data=input_data, url="PRE_SIGNED_URL") # doctest: +SKIP
|
|
193
370
|
"""
|
|
194
371
|
|
|
195
|
-
upload_data = None
|
|
196
|
-
if
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
372
|
+
upload_data: str | None = None
|
|
373
|
+
if data is not None:
|
|
374
|
+
if isinstance(data, dict):
|
|
375
|
+
upload_data = deflated_serialize_json(data, json_configurations=json_configurations)
|
|
376
|
+
elif isinstance(data, str):
|
|
377
|
+
upload_data = data
|
|
378
|
+
else:
|
|
379
|
+
raise ValueError("data must be a dictionary or a string")
|
|
202
380
|
|
|
203
381
|
session = requests.Session()
|
|
204
382
|
retries = Retry(
|
|
@@ -211,12 +389,21 @@ class Client:
|
|
|
211
389
|
)
|
|
212
390
|
adapter = HTTPAdapter(max_retries=retries)
|
|
213
391
|
session.mount("https://", adapter)
|
|
214
|
-
|
|
392
|
+
|
|
393
|
+
kwargs: dict[str, Any] = {
|
|
215
394
|
"url": url,
|
|
216
395
|
"timeout": self.timeout,
|
|
217
|
-
"data": upload_data,
|
|
218
396
|
}
|
|
219
397
|
|
|
398
|
+
if upload_data is not None:
|
|
399
|
+
kwargs["data"] = upload_data
|
|
400
|
+
elif tar_file is not None and tar_file != "":
|
|
401
|
+
if not os.path.exists(tar_file):
|
|
402
|
+
raise ValueError(f"tar_file {tar_file} does not exist")
|
|
403
|
+
kwargs["data"] = open(tar_file, "rb")
|
|
404
|
+
else:
|
|
405
|
+
raise ValueError("either data or tar_file must be provided")
|
|
406
|
+
|
|
220
407
|
response = session.put(**kwargs)
|
|
221
408
|
|
|
222
409
|
try:
|
|
@@ -224,11 +411,21 @@ class Client:
|
|
|
224
411
|
except requests.HTTPError as e:
|
|
225
412
|
raise requests.HTTPError(
|
|
226
413
|
f"upload to presigned URL {url} failed with "
|
|
227
|
-
|
|
414
|
+
f"status code {response.status_code} and message: {response.text}"
|
|
228
415
|
) from e
|
|
229
416
|
|
|
230
417
|
def _set_headers_api_key(self, api_key: str) -> None:
|
|
231
|
-
"""
|
|
418
|
+
"""
|
|
419
|
+
Sets the Authorization and Content-Type headers.
|
|
420
|
+
|
|
421
|
+
This is an internal method used to configure the necessary headers
|
|
422
|
+
for API authentication and content type specification.
|
|
423
|
+
|
|
424
|
+
Parameters
|
|
425
|
+
----------
|
|
426
|
+
api_key : str
|
|
427
|
+
The API key to be included in the Authorization header.
|
|
428
|
+
"""
|
|
232
429
|
|
|
233
430
|
self.headers = {
|
|
234
431
|
"Authorization": f"Bearer {api_key}",
|
|
@@ -236,11 +433,55 @@ class Client:
|
|
|
236
433
|
}
|
|
237
434
|
|
|
238
435
|
|
|
239
|
-
def get_size(obj:
|
|
240
|
-
"""
|
|
436
|
+
def get_size(obj: dict[str, Any] | IO[bytes] | str, json_configurations: dict[str, Any] | None = None) -> int:
|
|
437
|
+
"""
|
|
438
|
+
Finds the size of an object in bytes.
|
|
439
|
+
|
|
440
|
+
This function supports dictionaries (JSON-serialized), file-like objects
|
|
441
|
+
(by reading their content), and strings.
|
|
442
|
+
|
|
443
|
+
Parameters
|
|
444
|
+
----------
|
|
445
|
+
obj : dict[str, Any] or IO[bytes] or str
|
|
446
|
+
The object whose size is to be determined.
|
|
447
|
+
- If a dict, it's converted to a JSON string.
|
|
448
|
+
- If a file-like object (e.g., opened file), its size is read.
|
|
449
|
+
- If a string, its UTF-8 encoded byte length is calculated.
|
|
450
|
+
json_configurations : dict[str, Any], optional
|
|
451
|
+
Additional configurations for JSON serialization. This allows
|
|
452
|
+
customization of the Python `json.dumps` function, such as specifying
|
|
453
|
+
`indent` for pretty printing or `default` for custom serialization
|
|
454
|
+
functions.
|
|
455
|
+
|
|
456
|
+
Returns
|
|
457
|
+
-------
|
|
458
|
+
int
|
|
459
|
+
The size of the object in bytes.
|
|
460
|
+
|
|
461
|
+
Raises
|
|
462
|
+
------
|
|
463
|
+
TypeError
|
|
464
|
+
If the object type is not supported (i.e., not a dict,
|
|
465
|
+
file-like object, or string).
|
|
466
|
+
|
|
467
|
+
Examples
|
|
468
|
+
--------
|
|
469
|
+
>>> my_dict = {"key": "value", "number": 123}
|
|
470
|
+
>>> get_size(my_dict)
|
|
471
|
+
30
|
|
472
|
+
>>> import io
|
|
473
|
+
>>> my_string = "Hello, Nextmv!"
|
|
474
|
+
>>> string_io = io.StringIO(my_string)
|
|
475
|
+
>>> # To get size of underlying buffer for StringIO, we need to encode
|
|
476
|
+
>>> string_bytes_io = io.BytesIO(my_string.encode('utf-8'))
|
|
477
|
+
>>> get_size(string_bytes_io)
|
|
478
|
+
14
|
|
479
|
+
>>> get_size("Hello, Nextmv!")
|
|
480
|
+
14
|
|
481
|
+
"""
|
|
241
482
|
|
|
242
483
|
if isinstance(obj, dict):
|
|
243
|
-
obj_str =
|
|
484
|
+
obj_str = deflated_serialize_json(obj, json_configurations=json_configurations)
|
|
244
485
|
return len(obj_str.encode("utf-8"))
|
|
245
486
|
|
|
246
487
|
elif hasattr(obj, "read"):
|
|
@@ -253,4 +494,4 @@ def get_size(obj: Union[dict[str, Any], IO[bytes]]) -> int:
|
|
|
253
494
|
return len(obj.encode("utf-8"))
|
|
254
495
|
|
|
255
496
|
else:
|
|
256
|
-
raise TypeError("Unsupported type. Only dictionaries
|
|
497
|
+
raise TypeError("Unsupported type. Only dictionaries, file objects (IO[bytes]), and strings are supported.")
|