nextmv 0.30.0__py3-none-any.whl → 0.31.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nextmv/__about__.py +1 -1
- nextmv/__init__.py +40 -0
- nextmv/cloud/__init__.py +38 -33
- nextmv/cloud/account.py +1 -1
- nextmv/cloud/application.py +212 -481
- nextmv/cloud/input_set.py +1 -1
- nextmv/cloud/package.py +1 -1
- nextmv/cloud/url.py +73 -0
- nextmv/default_app/.gitignore +1 -0
- nextmv/default_app/app.yaml +2 -0
- nextmv/default_app/src/main.py +2 -1
- nextmv/input.py +17 -1
- nextmv/local/__init__.py +5 -0
- nextmv/local/application.py +1147 -0
- nextmv/local/executor.py +718 -0
- nextmv/local/geojson_handler.py +323 -0
- nextmv/local/plotly_handler.py +61 -0
- nextmv/local/runner.py +312 -0
- nextmv/{cloud/manifest.py → manifest.py} +110 -69
- nextmv/output.py +61 -8
- nextmv/polling.py +287 -0
- nextmv/{cloud/run.py → run.py} +390 -53
- nextmv/{cloud/safe.py → safe.py} +35 -3
- nextmv/{cloud/status.py → status.py} +9 -9
- {nextmv-0.30.0.dist-info → nextmv-0.31.0.dist-info}/METADATA +5 -1
- nextmv-0.31.0.dist-info/RECORD +46 -0
- nextmv-0.30.0.dist-info/RECORD +0 -37
- {nextmv-0.30.0.dist-info → nextmv-0.31.0.dist-info}/WHEEL +0 -0
- {nextmv-0.30.0.dist-info → nextmv-0.31.0.dist-info}/licenses/LICENSE +0 -0
nextmv/cloud/application.py
CHANGED
|
@@ -24,13 +24,9 @@ poll
|
|
|
24
24
|
|
|
25
25
|
import json
|
|
26
26
|
import os
|
|
27
|
-
import random
|
|
28
27
|
import shutil
|
|
29
28
|
import tarfile
|
|
30
29
|
import tempfile
|
|
31
|
-
import time
|
|
32
|
-
import uuid
|
|
33
|
-
from collections.abc import Callable
|
|
34
30
|
from dataclasses import dataclass
|
|
35
31
|
from datetime import datetime
|
|
36
32
|
from typing import Any, Optional, Union
|
|
@@ -52,8 +48,18 @@ from nextmv.cloud.batch_experiment import (
|
|
|
52
48
|
from nextmv.cloud.client import Client, get_size
|
|
53
49
|
from nextmv.cloud.input_set import InputSet, ManagedInput
|
|
54
50
|
from nextmv.cloud.instance import Instance, InstanceConfiguration
|
|
55
|
-
from nextmv.cloud.
|
|
56
|
-
from nextmv.cloud.
|
|
51
|
+
from nextmv.cloud.scenario import Scenario, ScenarioInputType, _option_sets, _scenarios_by_id
|
|
52
|
+
from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
|
|
53
|
+
from nextmv.cloud.url import DownloadURL, UploadURL
|
|
54
|
+
from nextmv.cloud.version import Version
|
|
55
|
+
from nextmv.input import Input, InputFormat
|
|
56
|
+
from nextmv.logger import log
|
|
57
|
+
from nextmv.manifest import Manifest
|
|
58
|
+
from nextmv.model import Model, ModelConfiguration
|
|
59
|
+
from nextmv.options import Options
|
|
60
|
+
from nextmv.output import Output, OutputFormat
|
|
61
|
+
from nextmv.polling import DEFAULT_POLLING_OPTIONS, PollingOptions, poll
|
|
62
|
+
from nextmv.run import (
|
|
57
63
|
ExternalRunResult,
|
|
58
64
|
Format,
|
|
59
65
|
FormatInput,
|
|
@@ -64,16 +70,8 @@ from nextmv.cloud.run import (
|
|
|
64
70
|
RunResult,
|
|
65
71
|
TrackedRun,
|
|
66
72
|
)
|
|
67
|
-
from nextmv.
|
|
68
|
-
from nextmv.
|
|
69
|
-
from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
|
|
70
|
-
from nextmv.cloud.status import StatusV2
|
|
71
|
-
from nextmv.cloud.version import Version
|
|
72
|
-
from nextmv.input import Input, InputFormat
|
|
73
|
-
from nextmv.logger import log
|
|
74
|
-
from nextmv.model import Model, ModelConfiguration
|
|
75
|
-
from nextmv.options import Options
|
|
76
|
-
from nextmv.output import Output, OutputFormat
|
|
73
|
+
from nextmv.safe import safe_id, safe_name_and_id
|
|
74
|
+
from nextmv.status import StatusV2
|
|
77
75
|
|
|
78
76
|
# Maximum size of the run input/output in bytes. This constant defines the
|
|
79
77
|
# maximum allowed size for run inputs and outputs. When the size exceeds this
|
|
@@ -82,180 +80,6 @@ from nextmv.output import Output, OutputFormat
|
|
|
82
80
|
_MAX_RUN_SIZE: int = 5 * 1024 * 1024
|
|
83
81
|
|
|
84
82
|
|
|
85
|
-
class DownloadURL(BaseModel):
|
|
86
|
-
"""
|
|
87
|
-
Result of getting a download URL.
|
|
88
|
-
|
|
89
|
-
You can import the `DownloadURL` class directly from `cloud`:
|
|
90
|
-
|
|
91
|
-
```python
|
|
92
|
-
from nextmv.cloud import DownloadURL
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
This class represents a download URL that can be used to fetch content
|
|
96
|
-
from Nextmv Cloud, typically used for downloading large run results.
|
|
97
|
-
|
|
98
|
-
Attributes
|
|
99
|
-
----------
|
|
100
|
-
url : str
|
|
101
|
-
URL to use for downloading the file.
|
|
102
|
-
|
|
103
|
-
Examples
|
|
104
|
-
--------
|
|
105
|
-
>>> download_url = DownloadURL(url="https://example.com/download")
|
|
106
|
-
>>> response = requests.get(download_url.url)
|
|
107
|
-
"""
|
|
108
|
-
|
|
109
|
-
url: str
|
|
110
|
-
"""URL to use for downloading the file."""
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@dataclass
|
|
114
|
-
class PollingOptions:
|
|
115
|
-
"""
|
|
116
|
-
Options to use when polling for a run result.
|
|
117
|
-
|
|
118
|
-
You can import the `PollingOptions` class directly from `cloud`:
|
|
119
|
-
|
|
120
|
-
```python
|
|
121
|
-
from nextmv.cloud import PollingOptions
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
The Cloud API will be polled for the result. The polling stops if:
|
|
125
|
-
|
|
126
|
-
* The maximum number of polls (tries) are exhausted. This is specified by
|
|
127
|
-
the `max_tries` parameter.
|
|
128
|
-
* The maximum duration of the polling strategy is reached. This is
|
|
129
|
-
specified by the `max_duration` parameter.
|
|
130
|
-
|
|
131
|
-
Before conducting the first poll, the `initial_delay` is used to sleep.
|
|
132
|
-
After each poll, a sleep duration is calculated using the following
|
|
133
|
-
strategy, based on exponential backoff with jitter:
|
|
134
|
-
|
|
135
|
-
```
|
|
136
|
-
sleep_duration = min(`max_delay`, `delay` + `backoff` * 2 ** i + Uniform(0, `jitter`))
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
Where:
|
|
140
|
-
* i is the retry (poll) number.
|
|
141
|
-
* Uniform is the uniform distribution.
|
|
142
|
-
|
|
143
|
-
Note that the sleep duration is capped by the `max_delay` parameter.
|
|
144
|
-
|
|
145
|
-
Parameters
|
|
146
|
-
----------
|
|
147
|
-
backoff : float, default=0.9
|
|
148
|
-
Exponential backoff factor, in seconds, to use between polls.
|
|
149
|
-
delay : float, default=0.1
|
|
150
|
-
Base delay to use between polls, in seconds.
|
|
151
|
-
initial_delay : float, default=1.0
|
|
152
|
-
Initial delay to use before starting the polling strategy, in seconds.
|
|
153
|
-
max_delay : float, default=20.0
|
|
154
|
-
Maximum delay to use between polls, in seconds.
|
|
155
|
-
max_duration : float, default=300.0
|
|
156
|
-
Maximum duration of the polling strategy, in seconds.
|
|
157
|
-
max_tries : int, default=100
|
|
158
|
-
Maximum number of tries to use.
|
|
159
|
-
jitter : float, default=1.0
|
|
160
|
-
Jitter to use for the polling strategy. A uniform distribution is sampled
|
|
161
|
-
between 0 and this number. The resulting random number is added to the
|
|
162
|
-
delay for each poll, adding a random noise. Set this to 0 to avoid using
|
|
163
|
-
random jitter.
|
|
164
|
-
verbose : bool, default=False
|
|
165
|
-
Whether to log the polling strategy. This is useful for debugging.
|
|
166
|
-
stop : callable, default=None
|
|
167
|
-
Function to call to check if the polling should stop. This is useful for
|
|
168
|
-
stopping the polling based on external conditions. The function should
|
|
169
|
-
return True to stop the polling and False to continue. The function does
|
|
170
|
-
not receive any arguments. The function is called before each poll.
|
|
171
|
-
|
|
172
|
-
Examples
|
|
173
|
-
--------
|
|
174
|
-
>>> from nextmv.cloud import PollingOptions
|
|
175
|
-
>>> # Create polling options with custom settings
|
|
176
|
-
>>> polling_options = PollingOptions(
|
|
177
|
-
... max_tries=50,
|
|
178
|
-
... max_duration=600,
|
|
179
|
-
... verbose=True
|
|
180
|
-
... )
|
|
181
|
-
"""
|
|
182
|
-
|
|
183
|
-
backoff: float = 0.9
|
|
184
|
-
"""
|
|
185
|
-
Exponential backoff factor, in seconds, to use between polls.
|
|
186
|
-
"""
|
|
187
|
-
delay: float = 0.1
|
|
188
|
-
"""Base delay to use between polls, in seconds."""
|
|
189
|
-
initial_delay: float = 1
|
|
190
|
-
"""
|
|
191
|
-
Initial delay to use before starting the polling strategy, in seconds.
|
|
192
|
-
"""
|
|
193
|
-
max_delay: float = 20
|
|
194
|
-
"""Maximum delay to use between polls, in seconds."""
|
|
195
|
-
max_duration: float = -1
|
|
196
|
-
"""
|
|
197
|
-
Maximum duration of the polling strategy, in seconds. A negative value means no limit.
|
|
198
|
-
"""
|
|
199
|
-
max_tries: int = -1
|
|
200
|
-
"""Maximum number of tries to use. A negative value means no limit."""
|
|
201
|
-
jitter: float = 1
|
|
202
|
-
"""
|
|
203
|
-
Jitter to use for the polling strategy. A uniform distribution is sampled
|
|
204
|
-
between 0 and this number. The resulting random number is added to the
|
|
205
|
-
delay for each poll, adding a random noise. Set this to 0 to avoid using
|
|
206
|
-
random jitter.
|
|
207
|
-
"""
|
|
208
|
-
verbose: bool = False
|
|
209
|
-
"""Whether to log the polling strategy. This is useful for debugging."""
|
|
210
|
-
stop: Optional[Callable[[], bool]] = None
|
|
211
|
-
"""
|
|
212
|
-
Function to call to check if the polling should stop. This is useful for
|
|
213
|
-
stopping the polling based on external conditions. The function should
|
|
214
|
-
return True to stop the polling and False to continue. The function does
|
|
215
|
-
not receive any arguments. The function is called before each poll.
|
|
216
|
-
"""
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
# Default polling options to use when polling for a run result. This constant
|
|
220
|
-
# provides the default values for `PollingOptions` used across the module.
|
|
221
|
-
# Using these defaults is recommended for most use cases unless specific timing
|
|
222
|
-
# needs are required.
|
|
223
|
-
_DEFAULT_POLLING_OPTIONS: PollingOptions = PollingOptions()
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
class UploadURL(BaseModel):
|
|
227
|
-
"""
|
|
228
|
-
Result of getting an upload URL.
|
|
229
|
-
|
|
230
|
-
You can import the `UploadURL` class directly from `cloud`:
|
|
231
|
-
|
|
232
|
-
```python
|
|
233
|
-
from nextmv.cloud import UploadURL
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
This class represents an upload URL that can be used to send data to
|
|
237
|
-
Nextmv Cloud, typically used for uploading large inputs for runs.
|
|
238
|
-
|
|
239
|
-
Attributes
|
|
240
|
-
----------
|
|
241
|
-
upload_id : str
|
|
242
|
-
ID of the upload, used to reference the uploaded content.
|
|
243
|
-
upload_url : str
|
|
244
|
-
URL to use for uploading the file.
|
|
245
|
-
|
|
246
|
-
Examples
|
|
247
|
-
--------
|
|
248
|
-
>>> upload_url = UploadURL(upload_id="123", upload_url="https://example.com/upload")
|
|
249
|
-
>>> with open("large_input.json", "rb") as f:
|
|
250
|
-
... requests.put(upload_url.upload_url, data=f)
|
|
251
|
-
"""
|
|
252
|
-
|
|
253
|
-
upload_id: str
|
|
254
|
-
"""ID of the upload."""
|
|
255
|
-
upload_url: str
|
|
256
|
-
"""URL to use for uploading the file."""
|
|
257
|
-
|
|
258
|
-
|
|
259
83
|
@dataclass
|
|
260
84
|
class Application:
|
|
261
85
|
"""
|
|
@@ -305,15 +129,6 @@ class Application:
|
|
|
305
129
|
experiments_endpoint: str = "{base}/experiments"
|
|
306
130
|
"""Base endpoint for the experiments in the application."""
|
|
307
131
|
|
|
308
|
-
# Local experience parameters.
|
|
309
|
-
src: Optional[str] = None
|
|
310
|
-
"""
|
|
311
|
-
Source of the application, if initialized locally. This is the path
|
|
312
|
-
to the application's source code.
|
|
313
|
-
"""
|
|
314
|
-
description: Optional[str] = None
|
|
315
|
-
"""Description of the application."""
|
|
316
|
-
|
|
317
132
|
def __post_init__(self):
|
|
318
133
|
"""Initialize the endpoint and experiments_endpoint attributes.
|
|
319
134
|
|
|
@@ -323,92 +138,6 @@ class Application:
|
|
|
323
138
|
self.endpoint = self.endpoint.format(id=self.id)
|
|
324
139
|
self.experiments_endpoint = self.experiments_endpoint.format(base=self.endpoint)
|
|
325
140
|
|
|
326
|
-
@classmethod
|
|
327
|
-
def initialize(
|
|
328
|
-
cls,
|
|
329
|
-
name: str,
|
|
330
|
-
id: Optional[str] = None,
|
|
331
|
-
description: Optional[str] = None,
|
|
332
|
-
destination: Optional[str] = None,
|
|
333
|
-
client: Optional[Client] = None,
|
|
334
|
-
) -> "Application":
|
|
335
|
-
"""
|
|
336
|
-
Initialize a Nextmv application, locally.
|
|
337
|
-
|
|
338
|
-
This method will create a new application in the local file system. The
|
|
339
|
-
application is a folder with the name given by `name`, under the
|
|
340
|
-
location given by `destination`. If the `destination` parameter is not
|
|
341
|
-
specified, the current working directory is used as default. This
|
|
342
|
-
method will scaffold the application with the necessary files and
|
|
343
|
-
directories to have an opinionated structure for your decision model.
|
|
344
|
-
Once the application is initialized, you are encouraged to complete it
|
|
345
|
-
with the decision model itself, so that the application can be run,
|
|
346
|
-
locally or remotely.
|
|
347
|
-
|
|
348
|
-
This method differs from the `Application.new` method in that it
|
|
349
|
-
creates the application locally rather than in the Cloud.
|
|
350
|
-
|
|
351
|
-
Although not required, you are encouraged to specify the `client`
|
|
352
|
-
parameter, so that the application can be pushed and synced remotely,
|
|
353
|
-
with the Nextmv Cloud. If you don't specify the `client`, and intend to
|
|
354
|
-
interact with the Nextmv Cloud, you will encounter an error. Make sure
|
|
355
|
-
you set the `client` parameter on the `Application` instance after
|
|
356
|
-
initialization, if you don't provide it here.
|
|
357
|
-
|
|
358
|
-
Use the `destination` parameter to specify where you want the app to be
|
|
359
|
-
initialized, using the current working directory by default.
|
|
360
|
-
|
|
361
|
-
Parameters
|
|
362
|
-
----------
|
|
363
|
-
name : str
|
|
364
|
-
Name of the application.
|
|
365
|
-
id : str, optional
|
|
366
|
-
ID of the application. Will be generated if not provided.
|
|
367
|
-
description : str, optional
|
|
368
|
-
Description of the application.
|
|
369
|
-
destination : str, optional
|
|
370
|
-
Destination directory where the application will be initialized. If
|
|
371
|
-
not provided, the current working directory will be used.
|
|
372
|
-
client : Client, optional
|
|
373
|
-
Client to use for interacting with the Nextmv Cloud API.
|
|
374
|
-
|
|
375
|
-
Returns
|
|
376
|
-
-------
|
|
377
|
-
Application
|
|
378
|
-
The initialized application instance.
|
|
379
|
-
"""
|
|
380
|
-
|
|
381
|
-
destination_dir = os.getcwd() if destination is None else destination
|
|
382
|
-
app_id = id if id is not None else str(uuid.uuid4())
|
|
383
|
-
|
|
384
|
-
# Create the new directory with the given name.
|
|
385
|
-
src = os.path.join(destination_dir, name)
|
|
386
|
-
os.makedirs(src, exist_ok=True)
|
|
387
|
-
|
|
388
|
-
# Get the path to the initial app structure template.
|
|
389
|
-
current_file_dir = os.path.dirname(os.path.abspath(__file__))
|
|
390
|
-
initial_app_structure_path = os.path.join(current_file_dir, "..", "default_app")
|
|
391
|
-
initial_app_structure_path = os.path.normpath(initial_app_structure_path)
|
|
392
|
-
|
|
393
|
-
# Copy everything from initial_app_structure to the new directory.
|
|
394
|
-
if os.path.exists(initial_app_structure_path):
|
|
395
|
-
for item in os.listdir(initial_app_structure_path):
|
|
396
|
-
source_path = os.path.join(initial_app_structure_path, item)
|
|
397
|
-
dest_path = os.path.join(src, item)
|
|
398
|
-
|
|
399
|
-
if os.path.isdir(source_path):
|
|
400
|
-
shutil.copytree(source_path, dest_path, dirs_exist_ok=True)
|
|
401
|
-
continue
|
|
402
|
-
|
|
403
|
-
shutil.copy2(source_path, dest_path)
|
|
404
|
-
|
|
405
|
-
return cls(
|
|
406
|
-
id=app_id,
|
|
407
|
-
client=client,
|
|
408
|
-
src=src,
|
|
409
|
-
description=description,
|
|
410
|
-
)
|
|
411
|
-
|
|
412
141
|
@classmethod
|
|
413
142
|
def new(
|
|
414
143
|
cls,
|
|
@@ -510,7 +239,7 @@ class Application:
|
|
|
510
239
|
def acceptance_test_with_polling(
|
|
511
240
|
self,
|
|
512
241
|
acceptance_test_id: str,
|
|
513
|
-
polling_options: PollingOptions =
|
|
242
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
514
243
|
) -> AcceptanceTest:
|
|
515
244
|
"""
|
|
516
245
|
Retrieve details of an acceptance test using polling.
|
|
@@ -627,7 +356,7 @@ class Application:
|
|
|
627
356
|
def batch_experiment_with_polling(
|
|
628
357
|
self,
|
|
629
358
|
batch_id: str,
|
|
630
|
-
polling_options: PollingOptions =
|
|
359
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
631
360
|
) -> BatchExperiment:
|
|
632
361
|
"""
|
|
633
362
|
Get a batch experiment with polling.
|
|
@@ -1327,7 +1056,7 @@ class Application:
|
|
|
1327
1056
|
name: str,
|
|
1328
1057
|
input_set_id: Optional[str] = None,
|
|
1329
1058
|
description: Optional[str] = None,
|
|
1330
|
-
polling_options: PollingOptions =
|
|
1059
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
1331
1060
|
) -> AcceptanceTest:
|
|
1332
1061
|
"""
|
|
1333
1062
|
Create a new acceptance test and poll for the result.
|
|
@@ -1491,7 +1220,7 @@ class Application:
|
|
|
1491
1220
|
option_sets: Optional[dict[str, dict[str, str]]] = None,
|
|
1492
1221
|
runs: Optional[list[Union[BatchExperimentRun, dict[str, Any]]]] = None,
|
|
1493
1222
|
type: Optional[str] = "batch",
|
|
1494
|
-
polling_options: PollingOptions =
|
|
1223
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
1495
1224
|
) -> BatchExperiment:
|
|
1496
1225
|
"""
|
|
1497
1226
|
Convenience method to create a new batch experiment and poll for the
|
|
@@ -1931,13 +1660,7 @@ class Application:
|
|
|
1931
1660
|
|
|
1932
1661
|
tar_file = self.__package_inputs(input_dir_path)
|
|
1933
1662
|
|
|
1934
|
-
input_data =
|
|
1935
|
-
if isinstance(input, BaseModel):
|
|
1936
|
-
input_data = input.to_dict()
|
|
1937
|
-
elif isinstance(input, dict) or isinstance(input, str):
|
|
1938
|
-
input_data = input
|
|
1939
|
-
elif isinstance(input, Input):
|
|
1940
|
-
input_data = input.data
|
|
1663
|
+
input_data = self.__extract_input_data(input)
|
|
1941
1664
|
|
|
1942
1665
|
input_size = 0
|
|
1943
1666
|
if input_data is not None:
|
|
@@ -1950,20 +1673,10 @@ class Application:
|
|
|
1950
1673
|
upload_id = upload_url.upload_id
|
|
1951
1674
|
upload_id_used = True
|
|
1952
1675
|
|
|
1953
|
-
options_dict =
|
|
1954
|
-
if isinstance(input, Input) and input.options is not None:
|
|
1955
|
-
options_dict = input.options.to_dict_cloud()
|
|
1956
|
-
|
|
1957
|
-
if options is not None:
|
|
1958
|
-
if isinstance(options, Options):
|
|
1959
|
-
options_dict = options.to_dict_cloud()
|
|
1960
|
-
elif isinstance(options, dict):
|
|
1961
|
-
for k, v in options.items():
|
|
1962
|
-
if isinstance(v, str):
|
|
1963
|
-
options_dict[k] = v
|
|
1964
|
-
else:
|
|
1965
|
-
options_dict[k] = deflated_serialize_json(v, json_configurations=json_configurations)
|
|
1676
|
+
options_dict = self.__extract_options_dict(options, json_configurations)
|
|
1966
1677
|
|
|
1678
|
+
# Builds the payload progressively based on the different arguments
|
|
1679
|
+
# that must be provided.
|
|
1967
1680
|
payload = {}
|
|
1968
1681
|
if upload_id_used:
|
|
1969
1682
|
payload["upload_id"] = upload_id
|
|
@@ -1980,15 +1693,7 @@ class Application:
|
|
|
1980
1693
|
raise ValueError(f"options must be dict[str,str], option {k} has type {type(v)} instead.")
|
|
1981
1694
|
payload["options"] = options_dict
|
|
1982
1695
|
|
|
1983
|
-
|
|
1984
|
-
configuration_dict = (
|
|
1985
|
-
configuration.to_dict() if isinstance(configuration, RunConfiguration) else configuration
|
|
1986
|
-
)
|
|
1987
|
-
else:
|
|
1988
|
-
configuration = RunConfiguration()
|
|
1989
|
-
configuration.resolve(input=input, dir_path=input_dir_path)
|
|
1990
|
-
configuration_dict = configuration.to_dict()
|
|
1991
|
-
|
|
1696
|
+
configuration_dict = self.__extract_run_config(input, configuration, input_dir_path)
|
|
1992
1697
|
payload["configuration"] = configuration_dict
|
|
1993
1698
|
|
|
1994
1699
|
if batch_experiment_id is not None:
|
|
@@ -2020,7 +1725,7 @@ class Application:
|
|
|
2020
1725
|
description: Optional[str] = None,
|
|
2021
1726
|
upload_id: Optional[str] = None,
|
|
2022
1727
|
run_options: Optional[Union[Options, dict[str, str]]] = None,
|
|
2023
|
-
polling_options: PollingOptions =
|
|
1728
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2024
1729
|
configuration: Optional[Union[RunConfiguration, dict[str, Any]]] = None,
|
|
2025
1730
|
batch_experiment_id: Optional[str] = None,
|
|
2026
1731
|
external_result: Optional[Union[ExternalRunResult, dict[str, Any]]] = None,
|
|
@@ -2289,7 +1994,7 @@ class Application:
|
|
|
2289
1994
|
scenarios: list[Scenario],
|
|
2290
1995
|
description: Optional[str] = None,
|
|
2291
1996
|
repetitions: Optional[int] = 0,
|
|
2292
|
-
polling_options: PollingOptions =
|
|
1997
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2293
1998
|
) -> BatchExperiment:
|
|
2294
1999
|
"""
|
|
2295
2000
|
Convenience method to create a new scenario test and poll for the
|
|
@@ -2496,7 +2201,7 @@ class Application:
|
|
|
2496
2201
|
return self.version(version_id=id)
|
|
2497
2202
|
|
|
2498
2203
|
if id is None:
|
|
2499
|
-
id =
|
|
2204
|
+
id = safe_id(prefix="version")
|
|
2500
2205
|
|
|
2501
2206
|
payload = {
|
|
2502
2207
|
"id": id,
|
|
@@ -2827,7 +2532,7 @@ class Application:
|
|
|
2827
2532
|
def run_result_with_polling(
|
|
2828
2533
|
self,
|
|
2829
2534
|
run_id: str,
|
|
2830
|
-
polling_options: PollingOptions =
|
|
2535
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2831
2536
|
output_dir_path: Optional[str] = ".",
|
|
2832
2537
|
) -> RunResult:
|
|
2833
2538
|
"""
|
|
@@ -2965,7 +2670,7 @@ class Application:
|
|
|
2965
2670
|
def scenario_test_with_polling(
|
|
2966
2671
|
self,
|
|
2967
2672
|
scenario_test_id: str,
|
|
2968
|
-
polling_options: PollingOptions =
|
|
2673
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2969
2674
|
) -> BatchExperiment:
|
|
2970
2675
|
"""
|
|
2971
2676
|
Get a scenario test with polling.
|
|
@@ -3004,7 +2709,12 @@ class Application:
|
|
|
3004
2709
|
|
|
3005
2710
|
return self.batch_experiment_with_polling(batch_id=scenario_test_id, polling_options=polling_options)
|
|
3006
2711
|
|
|
3007
|
-
def track_run(
|
|
2712
|
+
def track_run( # noqa: C901
|
|
2713
|
+
self,
|
|
2714
|
+
tracked_run: TrackedRun,
|
|
2715
|
+
instance_id: Optional[str] = None,
|
|
2716
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]] = None,
|
|
2717
|
+
) -> str:
|
|
3008
2718
|
"""
|
|
3009
2719
|
Track an external run.
|
|
3010
2720
|
|
|
@@ -3013,6 +2723,14 @@ class Application:
|
|
|
3013
2723
|
information about a run in Nextmv is useful for things like
|
|
3014
2724
|
experimenting and testing.
|
|
3015
2725
|
|
|
2726
|
+
Please read the documentation on the `TrackedRun` class carefully, as
|
|
2727
|
+
there are important considerations to take into account when using this
|
|
2728
|
+
method. For example, if you intend to upload JSON input/output, use the
|
|
2729
|
+
`input`/`output` attributes of the `TrackedRun` class. On the other
|
|
2730
|
+
hand, if you intend to track files-based input/output, use the
|
|
2731
|
+
`input_dir_path`/`output_dir_path` attributes of the `TrackedRun`
|
|
2732
|
+
class.
|
|
2733
|
+
|
|
3016
2734
|
Parameters
|
|
3017
2735
|
----------
|
|
3018
2736
|
tracked_run : TrackedRun
|
|
@@ -3020,6 +2738,11 @@ class Application:
|
|
|
3020
2738
|
instance_id : Optional[str], default=None
|
|
3021
2739
|
Optional instance ID if you want to associate your tracked run with
|
|
3022
2740
|
an instance.
|
|
2741
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]]
|
|
2742
|
+
Configuration to use for the run. This can be a
|
|
2743
|
+
`cloud.RunConfiguration` object or a dict. If the object is used,
|
|
2744
|
+
then the `.to_dict()` method is applied to extract the
|
|
2745
|
+
configuration.
|
|
3023
2746
|
|
|
3024
2747
|
Returns
|
|
3025
2748
|
-------
|
|
@@ -3042,22 +2765,55 @@ class Application:
|
|
|
3042
2765
|
>>> run_id = app.track_run(tracked_run)
|
|
3043
2766
|
"""
|
|
3044
2767
|
|
|
2768
|
+
# Get the URL to upload the input to.
|
|
3045
2769
|
url_input = self.upload_url()
|
|
3046
2770
|
|
|
2771
|
+
# Handle the case where the input is being uploaded as files. We need
|
|
2772
|
+
# to tar them.
|
|
2773
|
+
input_tar_file = ""
|
|
2774
|
+
input_dir_path = tracked_run.input_dir_path
|
|
2775
|
+
if input_dir_path is not None and input_dir_path != "":
|
|
2776
|
+
if not os.path.exists(input_dir_path):
|
|
2777
|
+
raise ValueError(f"Directory {input_dir_path} does not exist.")
|
|
2778
|
+
|
|
2779
|
+
if not os.path.isdir(input_dir_path):
|
|
2780
|
+
raise ValueError(f"Path {input_dir_path} is not a directory.")
|
|
2781
|
+
|
|
2782
|
+
input_tar_file = self.__package_inputs(input_dir_path)
|
|
2783
|
+
|
|
2784
|
+
# Handle the case where the input is uploaded as Input or a dict.
|
|
3047
2785
|
upload_input = tracked_run.input
|
|
3048
|
-
if isinstance(tracked_run.input, Input):
|
|
2786
|
+
if upload_input is not None and isinstance(tracked_run.input, Input):
|
|
3049
2787
|
upload_input = tracked_run.input.data
|
|
3050
2788
|
|
|
3051
|
-
|
|
2789
|
+
# Actually uploads de input.
|
|
2790
|
+
self.upload_large_input(input=upload_input, upload_url=url_input, tar_file=input_tar_file)
|
|
3052
2791
|
|
|
2792
|
+
# Get the URL to upload the output to.
|
|
3053
2793
|
url_output = self.upload_url()
|
|
3054
2794
|
|
|
2795
|
+
# Handle the case where the output is being uploaded as files. We need
|
|
2796
|
+
# to tar them.
|
|
2797
|
+
output_tar_file = ""
|
|
2798
|
+
output_dir_path = tracked_run.output_dir_path
|
|
2799
|
+
if output_dir_path is not None and output_dir_path != "":
|
|
2800
|
+
if not os.path.exists(output_dir_path):
|
|
2801
|
+
raise ValueError(f"Directory {output_dir_path} does not exist.")
|
|
2802
|
+
|
|
2803
|
+
if not os.path.isdir(output_dir_path):
|
|
2804
|
+
raise ValueError(f"Path {output_dir_path} is not a directory.")
|
|
2805
|
+
|
|
2806
|
+
output_tar_file = self.__package_inputs(output_dir_path)
|
|
2807
|
+
|
|
2808
|
+
# Handle the case where the output is uploaded as Output or a dict.
|
|
3055
2809
|
upload_output = tracked_run.output
|
|
3056
|
-
if isinstance(tracked_run.output, Output):
|
|
2810
|
+
if upload_output is not None and isinstance(tracked_run.output, Output):
|
|
3057
2811
|
upload_output = tracked_run.output.to_dict()
|
|
3058
2812
|
|
|
3059
|
-
|
|
2813
|
+
# Actually uploads the output.
|
|
2814
|
+
self.upload_large_input(input=upload_output, upload_url=url_output, tar_file=output_tar_file)
|
|
3060
2815
|
|
|
2816
|
+
# Create the external run result and appends logs if required.
|
|
3061
2817
|
external_result = ExternalRunResult(
|
|
3062
2818
|
output_upload_id=url_output.upload_id,
|
|
3063
2819
|
status=tracked_run.status.value,
|
|
@@ -3076,14 +2832,18 @@ class Application:
|
|
|
3076
2832
|
upload_id=url_input.upload_id,
|
|
3077
2833
|
external_result=external_result,
|
|
3078
2834
|
instance_id=instance_id,
|
|
2835
|
+
name=tracked_run.name,
|
|
2836
|
+
description=tracked_run.description,
|
|
2837
|
+
configuration=configuration,
|
|
3079
2838
|
)
|
|
3080
2839
|
|
|
3081
2840
|
def track_run_with_result(
|
|
3082
2841
|
self,
|
|
3083
2842
|
tracked_run: TrackedRun,
|
|
3084
|
-
polling_options: PollingOptions =
|
|
2843
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
3085
2844
|
instance_id: Optional[str] = None,
|
|
3086
2845
|
output_dir_path: Optional[str] = ".",
|
|
2846
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]] = None,
|
|
3087
2847
|
) -> RunResult:
|
|
3088
2848
|
"""
|
|
3089
2849
|
Track an external run and poll for the result. This is a convenience
|
|
@@ -3104,6 +2864,11 @@ class Application:
|
|
|
3104
2864
|
Path to a directory where non-JSON output files will be saved. This is
|
|
3105
2865
|
required if the output is non-JSON. If the directory does not exist, it
|
|
3106
2866
|
will be created. Uses the current directory by default.
|
|
2867
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]]
|
|
2868
|
+
Configuration to use for the run. This can be a
|
|
2869
|
+
`cloud.RunConfiguration` object or a dict. If the object is used,
|
|
2870
|
+
then the `.to_dict()` method is applied to extract the
|
|
2871
|
+
configuration.
|
|
3107
2872
|
|
|
3108
2873
|
Returns
|
|
3109
2874
|
-------
|
|
@@ -3123,7 +2888,11 @@ class Application:
|
|
|
3123
2888
|
If the run does not succeed after the polling strategy is
|
|
3124
2889
|
exhausted based on number of tries.
|
|
3125
2890
|
"""
|
|
3126
|
-
run_id = self.track_run(
|
|
2891
|
+
run_id = self.track_run(
|
|
2892
|
+
tracked_run=tracked_run,
|
|
2893
|
+
instance_id=instance_id,
|
|
2894
|
+
configuration=configuration,
|
|
2895
|
+
)
|
|
3127
2896
|
|
|
3128
2897
|
return self.run_result_with_polling(
|
|
3129
2898
|
run_id=run_id,
|
|
@@ -3829,7 +3598,7 @@ class Application:
|
|
|
3829
3598
|
# If working with a list of managed inputs, we need to create an
|
|
3830
3599
|
# input set.
|
|
3831
3600
|
if scenario.scenario_input.scenario_input_type == ScenarioInputType.INPUT:
|
|
3832
|
-
name, id =
|
|
3601
|
+
name, id = safe_name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
3833
3602
|
input_set = self.new_input_set(
|
|
3834
3603
|
id=id,
|
|
3835
3604
|
name=name,
|
|
@@ -3849,7 +3618,7 @@ class Application:
|
|
|
3849
3618
|
for data in scenario.scenario_input.scenario_input_data:
|
|
3850
3619
|
upload_url = self.upload_url()
|
|
3851
3620
|
self.upload_large_input(input=data, upload_url=upload_url)
|
|
3852
|
-
name, id =
|
|
3621
|
+
name, id = safe_name_and_id(prefix="man-input", entity_id=scenario_id)
|
|
3853
3622
|
managed_input = self.new_managed_input(
|
|
3854
3623
|
id=id,
|
|
3855
3624
|
name=name,
|
|
@@ -3858,7 +3627,7 @@ class Application:
|
|
|
3858
3627
|
)
|
|
3859
3628
|
managed_inputs.append(managed_input)
|
|
3860
3629
|
|
|
3861
|
-
name, id =
|
|
3630
|
+
name, id = safe_name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
3862
3631
|
input_set = self.new_input_set(
|
|
3863
3632
|
id=id,
|
|
3864
3633
|
name=name,
|
|
@@ -3873,28 +3642,71 @@ class Application:
|
|
|
3873
3642
|
def __validate_input_dir_path_and_configuration(
|
|
3874
3643
|
self,
|
|
3875
3644
|
input_dir_path: Optional[str],
|
|
3876
|
-
configuration: Optional[RunConfiguration],
|
|
3645
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]],
|
|
3877
3646
|
) -> None:
|
|
3878
3647
|
"""
|
|
3879
3648
|
Auxiliary function to validate the directory path and configuration.
|
|
3880
3649
|
"""
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
or configuration.format is None
|
|
3884
|
-
or configuration.format.format_input is None
|
|
3885
|
-
or configuration.format.format_input.input_type is None
|
|
3886
|
-
):
|
|
3887
|
-
# No explicit input type set, so we cannot confirm it.
|
|
3650
|
+
|
|
3651
|
+
if input_dir_path is None or input_dir_path == "":
|
|
3888
3652
|
return
|
|
3889
3653
|
|
|
3890
|
-
|
|
3891
|
-
dir_types = (InputFormat.MULTI_FILE, InputFormat.CSV_ARCHIVE)
|
|
3892
|
-
if input_type in dir_types and not input_dir_path:
|
|
3654
|
+
if configuration is None:
|
|
3893
3655
|
raise ValueError(
|
|
3894
|
-
|
|
3895
|
-
"then input_dir_path must be provided.",
|
|
3656
|
+
"If dir_path is provided, a RunConfiguration must also be provided.",
|
|
3896
3657
|
)
|
|
3897
3658
|
|
|
3659
|
+
config_format = self.__extract_config_format(configuration)
|
|
3660
|
+
|
|
3661
|
+
if config_format is None:
|
|
3662
|
+
raise ValueError(
|
|
3663
|
+
"If dir_path is provided, RunConfiguration.format must also be provided.",
|
|
3664
|
+
)
|
|
3665
|
+
|
|
3666
|
+
input_type = self.__extract_input_type(config_format)
|
|
3667
|
+
|
|
3668
|
+
if input_type is None or input_type in (InputFormat.JSON, InputFormat.TEXT):
|
|
3669
|
+
raise ValueError(
|
|
3670
|
+
"If dir_path is provided, RunConfiguration.format.format_input.input_type must be set to a valid type. "
|
|
3671
|
+
f"Valid types are: {[InputFormat.CSV_ARCHIVE, InputFormat.MULTI_FILE]}",
|
|
3672
|
+
)
|
|
3673
|
+
|
|
3674
|
+
def __extract_config_format(self, configuration: Union[RunConfiguration, dict[str, Any]]) -> Any:
|
|
3675
|
+
"""Extract format from configuration, handling both RunConfiguration objects and dicts."""
|
|
3676
|
+
if isinstance(configuration, RunConfiguration):
|
|
3677
|
+
return configuration.format
|
|
3678
|
+
|
|
3679
|
+
if isinstance(configuration, dict):
|
|
3680
|
+
config_format = configuration.get("format")
|
|
3681
|
+
if config_format is not None and isinstance(config_format, dict):
|
|
3682
|
+
return Format.from_dict(config_format) if hasattr(Format, "from_dict") else config_format
|
|
3683
|
+
|
|
3684
|
+
return config_format
|
|
3685
|
+
|
|
3686
|
+
raise ValueError("Configuration must be a RunConfiguration object or a dict.")
|
|
3687
|
+
|
|
3688
|
+
def __extract_input_type(self, config_format: Any) -> Any:
|
|
3689
|
+
"""Extract input type from config format."""
|
|
3690
|
+
if isinstance(config_format, dict):
|
|
3691
|
+
format_input = config_format.get("format_input") or config_format.get("input")
|
|
3692
|
+
if format_input is None:
|
|
3693
|
+
raise ValueError(
|
|
3694
|
+
"If dir_path is provided, RunConfiguration.format.format_input must also be provided.",
|
|
3695
|
+
)
|
|
3696
|
+
|
|
3697
|
+
if isinstance(format_input, dict):
|
|
3698
|
+
return format_input.get("input_type") or format_input.get("type")
|
|
3699
|
+
|
|
3700
|
+
return getattr(format_input, "input_type", None)
|
|
3701
|
+
|
|
3702
|
+
# Handle Format object
|
|
3703
|
+
if config_format.format_input is None:
|
|
3704
|
+
raise ValueError(
|
|
3705
|
+
"If dir_path is provided, RunConfiguration.format.format_input must also be provided.",
|
|
3706
|
+
)
|
|
3707
|
+
|
|
3708
|
+
return config_format.format_input.input_type
|
|
3709
|
+
|
|
3898
3710
|
def __package_inputs(self, dir_path: str) -> str:
|
|
3899
3711
|
"""
|
|
3900
3712
|
This is an auxiliary function for packaging the inputs found in the
|
|
@@ -3956,153 +3768,72 @@ class Application:
|
|
|
3956
3768
|
|
|
3957
3769
|
return size_exceeds or non_json_payload
|
|
3958
3770
|
|
|
3771
|
+
def __extract_input_data(
|
|
3772
|
+
self,
|
|
3773
|
+
input: Union[Input, dict[str, Any], BaseModel, str] = None,
|
|
3774
|
+
) -> Optional[Union[dict[str, Any], str]]:
|
|
3775
|
+
"""
|
|
3776
|
+
Auxiliary function to extract the input data from the input, based on
|
|
3777
|
+
its type.
|
|
3778
|
+
"""
|
|
3959
3779
|
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
You can import the `poll` function directly from `cloud`:
|
|
3969
|
-
|
|
3970
|
-
```python
|
|
3971
|
-
from nextmv.cloud import poll
|
|
3972
|
-
```
|
|
3973
|
-
|
|
3974
|
-
This function implements a flexible polling strategy with exponential backoff
|
|
3975
|
-
and jitter. It calls the provided polling function repeatedly until it indicates
|
|
3976
|
-
success, the maximum number of tries is reached, or the maximum duration is exceeded.
|
|
3977
|
-
|
|
3978
|
-
The `polling_func` is a callable that must return a `tuple[Any, bool]`
|
|
3979
|
-
where the first element is the result of the polling and the second
|
|
3980
|
-
element is a boolean indicating if the polling was successful or should be
|
|
3981
|
-
retried.
|
|
3982
|
-
|
|
3983
|
-
Parameters
|
|
3984
|
-
----------
|
|
3985
|
-
polling_options : PollingOptions
|
|
3986
|
-
Options for configuring the polling behavior, including retry counts,
|
|
3987
|
-
delays, timeouts, and verbosity settings.
|
|
3988
|
-
polling_func : callable
|
|
3989
|
-
Function to call to check if the polling was successful. Must return a tuple
|
|
3990
|
-
where the first element is the result value and the second is a boolean
|
|
3991
|
-
indicating success (True) or need to retry (False).
|
|
3992
|
-
|
|
3993
|
-
Returns
|
|
3994
|
-
-------
|
|
3995
|
-
Any
|
|
3996
|
-
Result value from the polling function when successful.
|
|
3997
|
-
|
|
3998
|
-
Raises
|
|
3999
|
-
------
|
|
4000
|
-
TimeoutError
|
|
4001
|
-
If the polling exceeds the maximum duration specified in polling_options.
|
|
4002
|
-
RuntimeError
|
|
4003
|
-
If the maximum number of tries is exhausted without success.
|
|
4004
|
-
|
|
4005
|
-
Examples
|
|
4006
|
-
--------
|
|
4007
|
-
>>> from nextmv.cloud import PollingOptions, poll
|
|
4008
|
-
>>> import time
|
|
4009
|
-
>>>
|
|
4010
|
-
>>> # Define a polling function that succeeds after 3 tries
|
|
4011
|
-
>>> counter = 0
|
|
4012
|
-
>>> def check_completion() -> tuple[str, bool]:
|
|
4013
|
-
... global counter
|
|
4014
|
-
... counter += 1
|
|
4015
|
-
... if counter >= 3:
|
|
4016
|
-
... return "Success", True
|
|
4017
|
-
... return None, False
|
|
4018
|
-
...
|
|
4019
|
-
>>> # Configure polling options
|
|
4020
|
-
>>> options = PollingOptions(
|
|
4021
|
-
... max_tries=5,
|
|
4022
|
-
... delay=0.1,
|
|
4023
|
-
... backoff=0.2,
|
|
4024
|
-
... verbose=True
|
|
4025
|
-
... )
|
|
4026
|
-
>>>
|
|
4027
|
-
>>> # Poll until the function succeeds
|
|
4028
|
-
>>> result = poll(options, check_completion)
|
|
4029
|
-
>>> print(result)
|
|
4030
|
-
'Success'
|
|
4031
|
-
"""
|
|
4032
|
-
|
|
4033
|
-
# Start by sleeping for the duration specified as initial delay.
|
|
4034
|
-
if polling_options.verbose:
|
|
4035
|
-
log(f"polling | sleeping for initial delay: {polling_options.initial_delay}")
|
|
4036
|
-
|
|
4037
|
-
__sleep_func(polling_options.initial_delay)
|
|
3780
|
+
input_data = None
|
|
3781
|
+
if isinstance(input, BaseModel):
|
|
3782
|
+
input_data = input.to_dict()
|
|
3783
|
+
elif isinstance(input, dict) or isinstance(input, str):
|
|
3784
|
+
input_data = input
|
|
3785
|
+
elif isinstance(input, Input):
|
|
3786
|
+
input_data = input.data
|
|
4038
3787
|
|
|
4039
|
-
|
|
4040
|
-
stopped = False
|
|
3788
|
+
return input_data
|
|
4041
3789
|
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
3790
|
+
def __extract_options_dict(
|
|
3791
|
+
self,
|
|
3792
|
+
options: Optional[Union[Options, dict[str, str]]] = None,
|
|
3793
|
+
json_configurations: Optional[dict[str, Any]] = None,
|
|
3794
|
+
) -> dict[str, str]:
|
|
3795
|
+
"""
|
|
3796
|
+
Auxiliary function to extract the options that will be sent to the
|
|
3797
|
+
application for execution.
|
|
3798
|
+
"""
|
|
4050
3799
|
|
|
4051
|
-
|
|
4052
|
-
if
|
|
4053
|
-
|
|
3800
|
+
options_dict = {}
|
|
3801
|
+
if options is not None:
|
|
3802
|
+
if isinstance(options, Options):
|
|
3803
|
+
options_dict = options.to_dict_cloud()
|
|
4054
3804
|
|
|
4055
|
-
|
|
3805
|
+
elif isinstance(options, dict):
|
|
3806
|
+
for k, v in options.items():
|
|
3807
|
+
if isinstance(v, str):
|
|
3808
|
+
options_dict[k] = v
|
|
3809
|
+
continue
|
|
4056
3810
|
|
|
4057
|
-
|
|
4058
|
-
result, ok = polling_func()
|
|
4059
|
-
if polling_options.verbose:
|
|
4060
|
-
log(f"polling | try # {ix + 1}, ok: {ok}")
|
|
3811
|
+
options_dict[k] = deflated_serialize_json(v, json_configurations=json_configurations)
|
|
4061
3812
|
|
|
4062
|
-
|
|
4063
|
-
return result
|
|
3813
|
+
return options_dict
|
|
4064
3814
|
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
3815
|
+
def __extract_run_config(
|
|
3816
|
+
self,
|
|
3817
|
+
input: Union[Input, dict[str, Any], BaseModel, str] = None,
|
|
3818
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]] = None,
|
|
3819
|
+
dir_path: Optional[str] = None,
|
|
3820
|
+
) -> dict[str, Any]:
|
|
3821
|
+
"""
|
|
3822
|
+
Auxiliary function to extract the run configuration that will be sent
|
|
3823
|
+
to the application for execution.
|
|
3824
|
+
"""
|
|
4069
3825
|
|
|
4070
|
-
if
|
|
4071
|
-
|
|
4072
|
-
|
|
3826
|
+
if configuration is not None:
|
|
3827
|
+
configuration_dict = (
|
|
3828
|
+
configuration.to_dict() if isinstance(configuration, RunConfiguration) else configuration
|
|
4073
3829
|
)
|
|
3830
|
+
return configuration_dict
|
|
4074
3831
|
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
# delay to avoid overflows.
|
|
4079
|
-
delay = polling_options.max_delay
|
|
4080
|
-
delay += random.uniform(0, polling_options.jitter) # Add jitter.
|
|
4081
|
-
else:
|
|
4082
|
-
delay = polling_options.delay # Base
|
|
4083
|
-
delay += polling_options.backoff * (2**ix) # Add exponential backoff.
|
|
4084
|
-
delay += random.uniform(0, polling_options.jitter) # Add jitter.
|
|
4085
|
-
|
|
4086
|
-
# We cannot exceed the max delay.
|
|
4087
|
-
if delay >= polling_options.max_delay:
|
|
4088
|
-
max_reached = True
|
|
4089
|
-
delay = polling_options.max_delay
|
|
4090
|
-
|
|
4091
|
-
# Sleep for the calculated delay.
|
|
4092
|
-
sleep_duration = delay
|
|
4093
|
-
if polling_options.verbose:
|
|
4094
|
-
log(f"polling | sleeping for duration: {sleep_duration}")
|
|
4095
|
-
|
|
4096
|
-
__sleep_func(sleep_duration)
|
|
4097
|
-
|
|
4098
|
-
if stopped:
|
|
4099
|
-
log("polling | stop condition met, stopping polling")
|
|
4100
|
-
|
|
4101
|
-
return None
|
|
3832
|
+
configuration = RunConfiguration()
|
|
3833
|
+
configuration.resolve(input=input, dir_path=dir_path)
|
|
3834
|
+
configuration_dict = configuration.to_dict()
|
|
4102
3835
|
|
|
4103
|
-
|
|
4104
|
-
f"polling did not succeed after {polling_options.max_tries} tries",
|
|
4105
|
-
)
|
|
3836
|
+
return configuration_dict
|
|
4106
3837
|
|
|
4107
3838
|
|
|
4108
3839
|
def _is_not_exist_error(e: requests.HTTPError) -> bool:
|