nextmv 0.29.5.dev1__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 +39 -30
- nextmv/cloud/acceptance_test.py +2 -51
- nextmv/cloud/account.py +1 -1
- nextmv/cloud/application.py +599 -516
- nextmv/cloud/batch_experiment.py +73 -1
- 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} +258 -54
- 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.29.5.dev1.dist-info → nextmv-0.31.0.dist-info}/METADATA +5 -1
- nextmv-0.31.0.dist-info/RECORD +46 -0
- nextmv-0.29.5.dev1.dist-info/RECORD +0 -37
- {nextmv-0.29.5.dev1.dist-info → nextmv-0.31.0.dist-info}/WHEEL +0 -0
- {nextmv-0.29.5.dev1.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
|
|
@@ -40,19 +36,30 @@ import requests
|
|
|
40
36
|
from nextmv._serialization import deflated_serialize_json
|
|
41
37
|
from nextmv.base_model import BaseModel
|
|
42
38
|
from nextmv.cloud import package
|
|
43
|
-
from nextmv.cloud.acceptance_test import AcceptanceTest,
|
|
39
|
+
from nextmv.cloud.acceptance_test import AcceptanceTest, Metric
|
|
44
40
|
from nextmv.cloud.batch_experiment import (
|
|
45
41
|
BatchExperiment,
|
|
46
42
|
BatchExperimentInformation,
|
|
47
43
|
BatchExperimentMetadata,
|
|
48
44
|
BatchExperimentRun,
|
|
45
|
+
ExperimentStatus,
|
|
49
46
|
to_runs,
|
|
50
47
|
)
|
|
51
48
|
from nextmv.cloud.client import Client, get_size
|
|
52
49
|
from nextmv.cloud.input_set import InputSet, ManagedInput
|
|
53
50
|
from nextmv.cloud.instance import Instance, InstanceConfiguration
|
|
54
|
-
from nextmv.cloud.
|
|
55
|
-
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 (
|
|
56
63
|
ExternalRunResult,
|
|
57
64
|
Format,
|
|
58
65
|
FormatInput,
|
|
@@ -63,16 +70,8 @@ from nextmv.cloud.run import (
|
|
|
63
70
|
RunResult,
|
|
64
71
|
TrackedRun,
|
|
65
72
|
)
|
|
66
|
-
from nextmv.
|
|
67
|
-
from nextmv.
|
|
68
|
-
from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
|
|
69
|
-
from nextmv.cloud.status import StatusV2
|
|
70
|
-
from nextmv.cloud.version import Version
|
|
71
|
-
from nextmv.input import Input, InputFormat
|
|
72
|
-
from nextmv.logger import log
|
|
73
|
-
from nextmv.model import Model, ModelConfiguration
|
|
74
|
-
from nextmv.options import Options
|
|
75
|
-
from nextmv.output import Output, OutputFormat
|
|
73
|
+
from nextmv.safe import safe_id, safe_name_and_id
|
|
74
|
+
from nextmv.status import StatusV2
|
|
76
75
|
|
|
77
76
|
# Maximum size of the run input/output in bytes. This constant defines the
|
|
78
77
|
# maximum allowed size for run inputs and outputs. When the size exceeds this
|
|
@@ -81,180 +80,6 @@ from nextmv.output import Output, OutputFormat
|
|
|
81
80
|
_MAX_RUN_SIZE: int = 5 * 1024 * 1024
|
|
82
81
|
|
|
83
82
|
|
|
84
|
-
class DownloadURL(BaseModel):
|
|
85
|
-
"""
|
|
86
|
-
Result of getting a download URL.
|
|
87
|
-
|
|
88
|
-
You can import the `DownloadURL` class directly from `cloud`:
|
|
89
|
-
|
|
90
|
-
```python
|
|
91
|
-
from nextmv.cloud import DownloadURL
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
This class represents a download URL that can be used to fetch content
|
|
95
|
-
from Nextmv Cloud, typically used for downloading large run results.
|
|
96
|
-
|
|
97
|
-
Attributes
|
|
98
|
-
----------
|
|
99
|
-
url : str
|
|
100
|
-
URL to use for downloading the file.
|
|
101
|
-
|
|
102
|
-
Examples
|
|
103
|
-
--------
|
|
104
|
-
>>> download_url = DownloadURL(url="https://example.com/download")
|
|
105
|
-
>>> response = requests.get(download_url.url)
|
|
106
|
-
"""
|
|
107
|
-
|
|
108
|
-
url: str
|
|
109
|
-
"""URL to use for downloading the file."""
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@dataclass
|
|
113
|
-
class PollingOptions:
|
|
114
|
-
"""
|
|
115
|
-
Options to use when polling for a run result.
|
|
116
|
-
|
|
117
|
-
You can import the `PollingOptions` class directly from `cloud`:
|
|
118
|
-
|
|
119
|
-
```python
|
|
120
|
-
from nextmv.cloud import PollingOptions
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
The Cloud API will be polled for the result. The polling stops if:
|
|
124
|
-
|
|
125
|
-
* The maximum number of polls (tries) are exhausted. This is specified by
|
|
126
|
-
the `max_tries` parameter.
|
|
127
|
-
* The maximum duration of the polling strategy is reached. This is
|
|
128
|
-
specified by the `max_duration` parameter.
|
|
129
|
-
|
|
130
|
-
Before conducting the first poll, the `initial_delay` is used to sleep.
|
|
131
|
-
After each poll, a sleep duration is calculated using the following
|
|
132
|
-
strategy, based on exponential backoff with jitter:
|
|
133
|
-
|
|
134
|
-
```
|
|
135
|
-
sleep_duration = min(`max_delay`, `delay` + `backoff` * 2 ** i + Uniform(0, `jitter`))
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Where:
|
|
139
|
-
* i is the retry (poll) number.
|
|
140
|
-
* Uniform is the uniform distribution.
|
|
141
|
-
|
|
142
|
-
Note that the sleep duration is capped by the `max_delay` parameter.
|
|
143
|
-
|
|
144
|
-
Parameters
|
|
145
|
-
----------
|
|
146
|
-
backoff : float, default=0.9
|
|
147
|
-
Exponential backoff factor, in seconds, to use between polls.
|
|
148
|
-
delay : float, default=0.1
|
|
149
|
-
Base delay to use between polls, in seconds.
|
|
150
|
-
initial_delay : float, default=1.0
|
|
151
|
-
Initial delay to use before starting the polling strategy, in seconds.
|
|
152
|
-
max_delay : float, default=20.0
|
|
153
|
-
Maximum delay to use between polls, in seconds.
|
|
154
|
-
max_duration : float, default=300.0
|
|
155
|
-
Maximum duration of the polling strategy, in seconds.
|
|
156
|
-
max_tries : int, default=100
|
|
157
|
-
Maximum number of tries to use.
|
|
158
|
-
jitter : float, default=1.0
|
|
159
|
-
Jitter to use for the polling strategy. A uniform distribution is sampled
|
|
160
|
-
between 0 and this number. The resulting random number is added to the
|
|
161
|
-
delay for each poll, adding a random noise. Set this to 0 to avoid using
|
|
162
|
-
random jitter.
|
|
163
|
-
verbose : bool, default=False
|
|
164
|
-
Whether to log the polling strategy. This is useful for debugging.
|
|
165
|
-
stop : callable, default=None
|
|
166
|
-
Function to call to check if the polling should stop. This is useful for
|
|
167
|
-
stopping the polling based on external conditions. The function should
|
|
168
|
-
return True to stop the polling and False to continue. The function does
|
|
169
|
-
not receive any arguments. The function is called before each poll.
|
|
170
|
-
|
|
171
|
-
Examples
|
|
172
|
-
--------
|
|
173
|
-
>>> from nextmv.cloud import PollingOptions
|
|
174
|
-
>>> # Create polling options with custom settings
|
|
175
|
-
>>> polling_options = PollingOptions(
|
|
176
|
-
... max_tries=50,
|
|
177
|
-
... max_duration=600,
|
|
178
|
-
... verbose=True
|
|
179
|
-
... )
|
|
180
|
-
"""
|
|
181
|
-
|
|
182
|
-
backoff: float = 0.9
|
|
183
|
-
"""
|
|
184
|
-
Exponential backoff factor, in seconds, to use between polls.
|
|
185
|
-
"""
|
|
186
|
-
delay: float = 0.1
|
|
187
|
-
"""Base delay to use between polls, in seconds."""
|
|
188
|
-
initial_delay: float = 1
|
|
189
|
-
"""
|
|
190
|
-
Initial delay to use before starting the polling strategy, in seconds.
|
|
191
|
-
"""
|
|
192
|
-
max_delay: float = 20
|
|
193
|
-
"""Maximum delay to use between polls, in seconds."""
|
|
194
|
-
max_duration: float = -1
|
|
195
|
-
"""
|
|
196
|
-
Maximum duration of the polling strategy, in seconds. A negative value means no limit.
|
|
197
|
-
"""
|
|
198
|
-
max_tries: int = -1
|
|
199
|
-
"""Maximum number of tries to use. A negative value means no limit."""
|
|
200
|
-
jitter: float = 1
|
|
201
|
-
"""
|
|
202
|
-
Jitter to use for the polling strategy. A uniform distribution is sampled
|
|
203
|
-
between 0 and this number. The resulting random number is added to the
|
|
204
|
-
delay for each poll, adding a random noise. Set this to 0 to avoid using
|
|
205
|
-
random jitter.
|
|
206
|
-
"""
|
|
207
|
-
verbose: bool = False
|
|
208
|
-
"""Whether to log the polling strategy. This is useful for debugging."""
|
|
209
|
-
stop: Optional[Callable[[], bool]] = None
|
|
210
|
-
"""
|
|
211
|
-
Function to call to check if the polling should stop. This is useful for
|
|
212
|
-
stopping the polling based on external conditions. The function should
|
|
213
|
-
return True to stop the polling and False to continue. The function does
|
|
214
|
-
not receive any arguments. The function is called before each poll.
|
|
215
|
-
"""
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
# Default polling options to use when polling for a run result. This constant
|
|
219
|
-
# provides the default values for `PollingOptions` used across the module.
|
|
220
|
-
# Using these defaults is recommended for most use cases unless specific timing
|
|
221
|
-
# needs are required.
|
|
222
|
-
_DEFAULT_POLLING_OPTIONS: PollingOptions = PollingOptions()
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
class UploadURL(BaseModel):
|
|
226
|
-
"""
|
|
227
|
-
Result of getting an upload URL.
|
|
228
|
-
|
|
229
|
-
You can import the `UploadURL` class directly from `cloud`:
|
|
230
|
-
|
|
231
|
-
```python
|
|
232
|
-
from nextmv.cloud import UploadURL
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
This class represents an upload URL that can be used to send data to
|
|
236
|
-
Nextmv Cloud, typically used for uploading large inputs for runs.
|
|
237
|
-
|
|
238
|
-
Attributes
|
|
239
|
-
----------
|
|
240
|
-
upload_id : str
|
|
241
|
-
ID of the upload, used to reference the uploaded content.
|
|
242
|
-
upload_url : str
|
|
243
|
-
URL to use for uploading the file.
|
|
244
|
-
|
|
245
|
-
Examples
|
|
246
|
-
--------
|
|
247
|
-
>>> upload_url = UploadURL(upload_id="123", upload_url="https://example.com/upload")
|
|
248
|
-
>>> with open("large_input.json", "rb") as f:
|
|
249
|
-
... requests.put(upload_url.upload_url, data=f)
|
|
250
|
-
"""
|
|
251
|
-
|
|
252
|
-
upload_id: str
|
|
253
|
-
"""ID of the upload."""
|
|
254
|
-
upload_url: str
|
|
255
|
-
"""URL to use for uploading the file."""
|
|
256
|
-
|
|
257
|
-
|
|
258
83
|
@dataclass
|
|
259
84
|
class Application:
|
|
260
85
|
"""
|
|
@@ -304,15 +129,6 @@ class Application:
|
|
|
304
129
|
experiments_endpoint: str = "{base}/experiments"
|
|
305
130
|
"""Base endpoint for the experiments in the application."""
|
|
306
131
|
|
|
307
|
-
# Local experience parameters.
|
|
308
|
-
src: Optional[str] = None
|
|
309
|
-
"""
|
|
310
|
-
Source of the application, if initialized locally. This is the path
|
|
311
|
-
to the application's source code.
|
|
312
|
-
"""
|
|
313
|
-
description: Optional[str] = None
|
|
314
|
-
"""Description of the application."""
|
|
315
|
-
|
|
316
132
|
def __post_init__(self):
|
|
317
133
|
"""Initialize the endpoint and experiments_endpoint attributes.
|
|
318
134
|
|
|
@@ -322,92 +138,6 @@ class Application:
|
|
|
322
138
|
self.endpoint = self.endpoint.format(id=self.id)
|
|
323
139
|
self.experiments_endpoint = self.experiments_endpoint.format(base=self.endpoint)
|
|
324
140
|
|
|
325
|
-
@classmethod
|
|
326
|
-
def initialize(
|
|
327
|
-
cls,
|
|
328
|
-
name: str,
|
|
329
|
-
id: Optional[str] = None,
|
|
330
|
-
description: Optional[str] = None,
|
|
331
|
-
destination: Optional[str] = None,
|
|
332
|
-
client: Optional[Client] = None,
|
|
333
|
-
) -> "Application":
|
|
334
|
-
"""
|
|
335
|
-
Initialize a Nextmv application, locally.
|
|
336
|
-
|
|
337
|
-
This method will create a new application in the local file system. The
|
|
338
|
-
application is a folder with the name given by `name`, under the
|
|
339
|
-
location given by `destination`. If the `destination` parameter is not
|
|
340
|
-
specified, the current working directory is used as default. This
|
|
341
|
-
method will scaffold the application with the necessary files and
|
|
342
|
-
directories to have an opinionated structure for your decision model.
|
|
343
|
-
Once the application is initialized, you are encouraged to complete it
|
|
344
|
-
with the decision model itself, so that the application can be run,
|
|
345
|
-
locally or remotely.
|
|
346
|
-
|
|
347
|
-
This method differs from the `Application.new` method in that it
|
|
348
|
-
creates the application locally rather than in the Cloud.
|
|
349
|
-
|
|
350
|
-
Although not required, you are encouraged to specify the `client`
|
|
351
|
-
parameter, so that the application can be pushed and synced remotely,
|
|
352
|
-
with the Nextmv Cloud. If you don't specify the `client`, and intend to
|
|
353
|
-
interact with the Nextmv Cloud, you will encounter an error. Make sure
|
|
354
|
-
you set the `client` parameter on the `Application` instance after
|
|
355
|
-
initialization, if you don't provide it here.
|
|
356
|
-
|
|
357
|
-
Use the `destination` parameter to specify where you want the app to be
|
|
358
|
-
initialized, using the current working directory by default.
|
|
359
|
-
|
|
360
|
-
Parameters
|
|
361
|
-
----------
|
|
362
|
-
name : str
|
|
363
|
-
Name of the application.
|
|
364
|
-
id : str, optional
|
|
365
|
-
ID of the application. Will be generated if not provided.
|
|
366
|
-
description : str, optional
|
|
367
|
-
Description of the application.
|
|
368
|
-
destination : str, optional
|
|
369
|
-
Destination directory where the application will be initialized. If
|
|
370
|
-
not provided, the current working directory will be used.
|
|
371
|
-
client : Client, optional
|
|
372
|
-
Client to use for interacting with the Nextmv Cloud API.
|
|
373
|
-
|
|
374
|
-
Returns
|
|
375
|
-
-------
|
|
376
|
-
Application
|
|
377
|
-
The initialized application instance.
|
|
378
|
-
"""
|
|
379
|
-
|
|
380
|
-
destination_dir = os.getcwd() if destination is None else destination
|
|
381
|
-
app_id = id if id is not None else str(uuid.uuid4())
|
|
382
|
-
|
|
383
|
-
# Create the new directory with the given name.
|
|
384
|
-
src = os.path.join(destination_dir, name)
|
|
385
|
-
os.makedirs(src, exist_ok=True)
|
|
386
|
-
|
|
387
|
-
# Get the path to the initial app structure template.
|
|
388
|
-
current_file_dir = os.path.dirname(os.path.abspath(__file__))
|
|
389
|
-
initial_app_structure_path = os.path.join(current_file_dir, "..", "default_app")
|
|
390
|
-
initial_app_structure_path = os.path.normpath(initial_app_structure_path)
|
|
391
|
-
|
|
392
|
-
# Copy everything from initial_app_structure to the new directory.
|
|
393
|
-
if os.path.exists(initial_app_structure_path):
|
|
394
|
-
for item in os.listdir(initial_app_structure_path):
|
|
395
|
-
source_path = os.path.join(initial_app_structure_path, item)
|
|
396
|
-
dest_path = os.path.join(src, item)
|
|
397
|
-
|
|
398
|
-
if os.path.isdir(source_path):
|
|
399
|
-
shutil.copytree(source_path, dest_path, dirs_exist_ok=True)
|
|
400
|
-
continue
|
|
401
|
-
|
|
402
|
-
shutil.copy2(source_path, dest_path)
|
|
403
|
-
|
|
404
|
-
return cls(
|
|
405
|
-
id=app_id,
|
|
406
|
-
client=client,
|
|
407
|
-
src=src,
|
|
408
|
-
description=description,
|
|
409
|
-
)
|
|
410
|
-
|
|
411
141
|
@classmethod
|
|
412
142
|
def new(
|
|
413
143
|
cls,
|
|
@@ -506,6 +236,57 @@ class Application:
|
|
|
506
236
|
|
|
507
237
|
return AcceptanceTest.from_dict(response.json())
|
|
508
238
|
|
|
239
|
+
def acceptance_test_with_polling(
|
|
240
|
+
self,
|
|
241
|
+
acceptance_test_id: str,
|
|
242
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
243
|
+
) -> AcceptanceTest:
|
|
244
|
+
"""
|
|
245
|
+
Retrieve details of an acceptance test using polling.
|
|
246
|
+
|
|
247
|
+
Retrieves the result of an acceptance test. This method polls for the
|
|
248
|
+
result until the test finishes executing or the polling strategy is
|
|
249
|
+
exhausted.
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
acceptance_test_id : str
|
|
254
|
+
ID of the acceptance test to retrieve.
|
|
255
|
+
|
|
256
|
+
Returns
|
|
257
|
+
-------
|
|
258
|
+
AcceptanceTest
|
|
259
|
+
The requested acceptance test details.
|
|
260
|
+
|
|
261
|
+
Raises
|
|
262
|
+
------
|
|
263
|
+
requests.HTTPError
|
|
264
|
+
If the response status code is not 2xx.
|
|
265
|
+
|
|
266
|
+
Examples
|
|
267
|
+
--------
|
|
268
|
+
>>> test = app.acceptance_test_with_polling("test-123")
|
|
269
|
+
>>> print(test.name)
|
|
270
|
+
'My Test'
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
def polling_func() -> tuple[Any, bool]:
|
|
274
|
+
acceptance_test_result = self.acceptance_test(acceptance_test_id=acceptance_test_id)
|
|
275
|
+
if acceptance_test_result.status in {
|
|
276
|
+
ExperimentStatus.COMPLETED,
|
|
277
|
+
ExperimentStatus.FAILED,
|
|
278
|
+
ExperimentStatus.DRAFT,
|
|
279
|
+
ExperimentStatus.CANCELED,
|
|
280
|
+
ExperimentStatus.DELETE_FAILED,
|
|
281
|
+
}:
|
|
282
|
+
return acceptance_test_result, True
|
|
283
|
+
|
|
284
|
+
return None, False
|
|
285
|
+
|
|
286
|
+
acceptance_test = poll(polling_options=polling_options, polling_func=polling_func)
|
|
287
|
+
|
|
288
|
+
return self.acceptance_test(acceptance_test_id=acceptance_test.id)
|
|
289
|
+
|
|
509
290
|
def batch_experiment(self, batch_id: str) -> BatchExperiment:
|
|
510
291
|
"""
|
|
511
292
|
Get a batch experiment.
|
|
@@ -539,6 +320,90 @@ class Application:
|
|
|
539
320
|
|
|
540
321
|
return BatchExperiment.from_dict(response.json())
|
|
541
322
|
|
|
323
|
+
def batch_experiment_metadata(self, batch_id: str) -> BatchExperimentMetadata:
|
|
324
|
+
"""
|
|
325
|
+
Get metadata for a batch experiment.
|
|
326
|
+
|
|
327
|
+
Parameters
|
|
328
|
+
----------
|
|
329
|
+
batch_id : str
|
|
330
|
+
ID of the batch experiment.
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
-------
|
|
334
|
+
BatchExperimentMetadata
|
|
335
|
+
The requested batch experiment metadata.
|
|
336
|
+
|
|
337
|
+
Raises
|
|
338
|
+
------
|
|
339
|
+
requests.HTTPError
|
|
340
|
+
If the response status code is not 2xx.
|
|
341
|
+
|
|
342
|
+
Examples
|
|
343
|
+
--------
|
|
344
|
+
>>> metadata = app.batch_experiment_metadata("batch-123")
|
|
345
|
+
>>> print(metadata.name)
|
|
346
|
+
'My Batch Experiment'
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
response = self.client.request(
|
|
350
|
+
method="GET",
|
|
351
|
+
endpoint=f"{self.experiments_endpoint}/batch/{batch_id}/metadata",
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
return BatchExperimentMetadata.from_dict(response.json())
|
|
355
|
+
|
|
356
|
+
def batch_experiment_with_polling(
|
|
357
|
+
self,
|
|
358
|
+
batch_id: str,
|
|
359
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
360
|
+
) -> BatchExperiment:
|
|
361
|
+
"""
|
|
362
|
+
Get a batch experiment with polling.
|
|
363
|
+
|
|
364
|
+
Retrieves the result of an experiment. This method polls for the result
|
|
365
|
+
until the experiment finishes executing or the polling strategy is
|
|
366
|
+
exhausted.
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
batch_id : str
|
|
371
|
+
ID of the batch experiment.
|
|
372
|
+
|
|
373
|
+
Returns
|
|
374
|
+
-------
|
|
375
|
+
BatchExperiment
|
|
376
|
+
The requested batch experiment details.
|
|
377
|
+
|
|
378
|
+
Raises
|
|
379
|
+
------
|
|
380
|
+
requests.HTTPError
|
|
381
|
+
If the response status code is not 2xx.
|
|
382
|
+
|
|
383
|
+
Examples
|
|
384
|
+
--------
|
|
385
|
+
>>> batch_exp = app.batch_experiment_with_polling("batch-123")
|
|
386
|
+
>>> print(batch_exp.name)
|
|
387
|
+
'My Batch Experiment'
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
def polling_func() -> tuple[Any, bool]:
|
|
391
|
+
batch_metadata = self.batch_experiment_metadata(batch_id=batch_id)
|
|
392
|
+
if batch_metadata.status in {
|
|
393
|
+
ExperimentStatus.COMPLETED,
|
|
394
|
+
ExperimentStatus.FAILED,
|
|
395
|
+
ExperimentStatus.DRAFT,
|
|
396
|
+
ExperimentStatus.CANCELED,
|
|
397
|
+
ExperimentStatus.DELETE_FAILED,
|
|
398
|
+
}:
|
|
399
|
+
return batch_metadata, True
|
|
400
|
+
|
|
401
|
+
return None, False
|
|
402
|
+
|
|
403
|
+
batch_information = poll(polling_options=polling_options, polling_func=polling_func)
|
|
404
|
+
|
|
405
|
+
return self.batch_experiment(batch_id=batch_information.id)
|
|
406
|
+
|
|
542
407
|
def cancel_run(self, run_id: str) -> None:
|
|
543
408
|
"""
|
|
544
409
|
Cancel a run.
|
|
@@ -1130,7 +995,7 @@ class Application:
|
|
|
1130
995
|
else:
|
|
1131
996
|
# Get all input IDs from the input set.
|
|
1132
997
|
input_set = self.input_set(input_set_id=input_set_id)
|
|
1133
|
-
if
|
|
998
|
+
if not input_set.input_ids:
|
|
1134
999
|
raise ValueError(f"input set {input_set_id} does not contain any inputs")
|
|
1135
1000
|
runs = []
|
|
1136
1001
|
for input_id in input_set.input_ids:
|
|
@@ -1191,7 +1056,7 @@ class Application:
|
|
|
1191
1056
|
name: str,
|
|
1192
1057
|
input_set_id: Optional[str] = None,
|
|
1193
1058
|
description: Optional[str] = None,
|
|
1194
|
-
polling_options: PollingOptions =
|
|
1059
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
1195
1060
|
) -> AcceptanceTest:
|
|
1196
1061
|
"""
|
|
1197
1062
|
Create a new acceptance test and poll for the result.
|
|
@@ -1248,7 +1113,8 @@ class Application:
|
|
|
1248
1113
|
>>> print(test.status)
|
|
1249
1114
|
'completed'
|
|
1250
1115
|
"""
|
|
1251
|
-
|
|
1116
|
+
|
|
1117
|
+
acceptance_test = self.new_acceptance_test(
|
|
1252
1118
|
candidate_instance_id=candidate_instance_id,
|
|
1253
1119
|
baseline_instance_id=baseline_instance_id,
|
|
1254
1120
|
id=id,
|
|
@@ -1258,20 +1124,10 @@ class Application:
|
|
|
1258
1124
|
description=description,
|
|
1259
1125
|
)
|
|
1260
1126
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
ExperimentStatus.failed,
|
|
1266
|
-
ExperimentStatus.canceled,
|
|
1267
|
-
]:
|
|
1268
|
-
return test_information, True
|
|
1269
|
-
|
|
1270
|
-
return None, False
|
|
1271
|
-
|
|
1272
|
-
test_information = poll(polling_options=polling_options, polling_func=polling_func)
|
|
1273
|
-
|
|
1274
|
-
return test_information
|
|
1127
|
+
return self.acceptance_test_with_polling(
|
|
1128
|
+
acceptance_test_id=acceptance_test.id,
|
|
1129
|
+
polling_options=polling_options,
|
|
1130
|
+
)
|
|
1275
1131
|
|
|
1276
1132
|
def new_batch_experiment(
|
|
1277
1133
|
self,
|
|
@@ -1354,6 +1210,76 @@ class Application:
|
|
|
1354
1210
|
|
|
1355
1211
|
return response.json()["id"]
|
|
1356
1212
|
|
|
1213
|
+
def new_batch_experiment_with_result(
|
|
1214
|
+
self,
|
|
1215
|
+
name: str,
|
|
1216
|
+
input_set_id: Optional[str] = None,
|
|
1217
|
+
instance_ids: Optional[list[str]] = None,
|
|
1218
|
+
description: Optional[str] = None,
|
|
1219
|
+
id: Optional[str] = None,
|
|
1220
|
+
option_sets: Optional[dict[str, dict[str, str]]] = None,
|
|
1221
|
+
runs: Optional[list[Union[BatchExperimentRun, dict[str, Any]]]] = None,
|
|
1222
|
+
type: Optional[str] = "batch",
|
|
1223
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
1224
|
+
) -> BatchExperiment:
|
|
1225
|
+
"""
|
|
1226
|
+
Convenience method to create a new batch experiment and poll for the
|
|
1227
|
+
result.
|
|
1228
|
+
|
|
1229
|
+
This method combines the `new_batch_experiment` and
|
|
1230
|
+
`batch_experiment_with_polling` methods, applying polling logic to
|
|
1231
|
+
check when the experiment succeeded.
|
|
1232
|
+
|
|
1233
|
+
Parameters
|
|
1234
|
+
----------
|
|
1235
|
+
name: str
|
|
1236
|
+
Name of the batch experiment.
|
|
1237
|
+
input_set_id: str
|
|
1238
|
+
ID of the input set to use for the batch experiment.
|
|
1239
|
+
instance_ids: list[str]
|
|
1240
|
+
List of instance IDs to use for the batch experiment. This argument
|
|
1241
|
+
is deprecated, use `runs` instead.
|
|
1242
|
+
description: Optional[str]
|
|
1243
|
+
Optional description of the batch experiment.
|
|
1244
|
+
id: Optional[str]
|
|
1245
|
+
ID of the batch experiment. Will be generated if not provided.
|
|
1246
|
+
option_sets: Optional[dict[str, dict[str, str]]]
|
|
1247
|
+
Option sets to use for the batch experiment. This is a dictionary
|
|
1248
|
+
where the keys are option set IDs and the values are dictionaries
|
|
1249
|
+
with the actual options.
|
|
1250
|
+
runs: Optional[list[BatchExperimentRun]]
|
|
1251
|
+
List of runs to use for the batch experiment.
|
|
1252
|
+
type: Optional[str]
|
|
1253
|
+
Type of the batch experiment. This is used to determine the
|
|
1254
|
+
experiment type. The default value is "batch". If you want to
|
|
1255
|
+
create a scenario test, set this to "scenario".
|
|
1256
|
+
polling_options : PollingOptions, default=_DEFAULT_POLLING_OPTIONS
|
|
1257
|
+
Options to use when polling for the batch experiment result.
|
|
1258
|
+
|
|
1259
|
+
Returns
|
|
1260
|
+
-------
|
|
1261
|
+
BatchExperiment
|
|
1262
|
+
The completed batch experiment with results.
|
|
1263
|
+
|
|
1264
|
+
Raises
|
|
1265
|
+
------
|
|
1266
|
+
requests.HTTPError
|
|
1267
|
+
If the response status code is not 2xx.
|
|
1268
|
+
"""
|
|
1269
|
+
|
|
1270
|
+
batch_id = self.new_batch_experiment(
|
|
1271
|
+
name=name,
|
|
1272
|
+
input_set_id=input_set_id,
|
|
1273
|
+
instance_ids=instance_ids,
|
|
1274
|
+
description=description,
|
|
1275
|
+
id=id,
|
|
1276
|
+
option_sets=option_sets,
|
|
1277
|
+
runs=runs,
|
|
1278
|
+
type=type,
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
return self.batch_experiment_with_polling(batch_id=batch_id, polling_options=polling_options)
|
|
1282
|
+
|
|
1357
1283
|
def new_input_set(
|
|
1358
1284
|
self,
|
|
1359
1285
|
id: str,
|
|
@@ -1722,7 +1648,7 @@ class Application:
|
|
|
1722
1648
|
not `JSON`. If the final `options` are not of type `dict[str,str]`.
|
|
1723
1649
|
"""
|
|
1724
1650
|
|
|
1725
|
-
self.
|
|
1651
|
+
self.__validate_input_dir_path_and_configuration(input_dir_path, configuration)
|
|
1726
1652
|
|
|
1727
1653
|
tar_file = ""
|
|
1728
1654
|
if input_dir_path is not None and input_dir_path != "":
|
|
@@ -1734,13 +1660,7 @@ class Application:
|
|
|
1734
1660
|
|
|
1735
1661
|
tar_file = self.__package_inputs(input_dir_path)
|
|
1736
1662
|
|
|
1737
|
-
input_data =
|
|
1738
|
-
if isinstance(input, BaseModel):
|
|
1739
|
-
input_data = input.to_dict()
|
|
1740
|
-
elif isinstance(input, dict) or isinstance(input, str):
|
|
1741
|
-
input_data = input
|
|
1742
|
-
elif isinstance(input, Input):
|
|
1743
|
-
input_data = input.data
|
|
1663
|
+
input_data = self.__extract_input_data(input)
|
|
1744
1664
|
|
|
1745
1665
|
input_size = 0
|
|
1746
1666
|
if input_data is not None:
|
|
@@ -1753,20 +1673,10 @@ class Application:
|
|
|
1753
1673
|
upload_id = upload_url.upload_id
|
|
1754
1674
|
upload_id_used = True
|
|
1755
1675
|
|
|
1756
|
-
options_dict =
|
|
1757
|
-
if isinstance(input, Input) and input.options is not None:
|
|
1758
|
-
options_dict = input.options.to_dict_cloud()
|
|
1759
|
-
|
|
1760
|
-
if options is not None:
|
|
1761
|
-
if isinstance(options, Options):
|
|
1762
|
-
options_dict = options.to_dict_cloud()
|
|
1763
|
-
elif isinstance(options, dict):
|
|
1764
|
-
for k, v in options.items():
|
|
1765
|
-
if isinstance(v, str):
|
|
1766
|
-
options_dict[k] = v
|
|
1767
|
-
else:
|
|
1768
|
-
options_dict[k] = deflated_serialize_json(v, json_configurations=json_configurations)
|
|
1676
|
+
options_dict = self.__extract_options_dict(options, json_configurations)
|
|
1769
1677
|
|
|
1678
|
+
# Builds the payload progressively based on the different arguments
|
|
1679
|
+
# that must be provided.
|
|
1770
1680
|
payload = {}
|
|
1771
1681
|
if upload_id_used:
|
|
1772
1682
|
payload["upload_id"] = upload_id
|
|
@@ -1779,19 +1689,11 @@ class Application:
|
|
|
1779
1689
|
payload["description"] = description
|
|
1780
1690
|
if len(options_dict) > 0:
|
|
1781
1691
|
for k, v in options_dict.items():
|
|
1782
|
-
if not isinstance(v, str):
|
|
1783
|
-
raise ValueError(f"options must be dict[str,str], option {k} has type {type(v)} instead.")
|
|
1784
|
-
payload["options"] = options_dict
|
|
1785
|
-
|
|
1786
|
-
if configuration is not None:
|
|
1787
|
-
configuration_dict = (
|
|
1788
|
-
configuration.to_dict() if isinstance(configuration, RunConfiguration) else configuration
|
|
1789
|
-
)
|
|
1790
|
-
else:
|
|
1791
|
-
configuration = RunConfiguration()
|
|
1792
|
-
configuration.resolve(input=input, dir_path=input_dir_path)
|
|
1793
|
-
configuration_dict = configuration.to_dict()
|
|
1692
|
+
if not isinstance(v, str):
|
|
1693
|
+
raise ValueError(f"options must be dict[str,str], option {k} has type {type(v)} instead.")
|
|
1694
|
+
payload["options"] = options_dict
|
|
1794
1695
|
|
|
1696
|
+
configuration_dict = self.__extract_run_config(input, configuration, input_dir_path)
|
|
1795
1697
|
payload["configuration"] = configuration_dict
|
|
1796
1698
|
|
|
1797
1699
|
if batch_experiment_id is not None:
|
|
@@ -1823,7 +1725,7 @@ class Application:
|
|
|
1823
1725
|
description: Optional[str] = None,
|
|
1824
1726
|
upload_id: Optional[str] = None,
|
|
1825
1727
|
run_options: Optional[Union[Options, dict[str, str]]] = None,
|
|
1826
|
-
polling_options: PollingOptions =
|
|
1728
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
1827
1729
|
configuration: Optional[Union[RunConfiguration, dict[str, Any]]] = None,
|
|
1828
1730
|
batch_experiment_id: Optional[str] = None,
|
|
1829
1731
|
external_result: Optional[Union[ExternalRunResult, dict[str, Any]]] = None,
|
|
@@ -2085,6 +1987,69 @@ class Application:
|
|
|
2085
1987
|
runs=runs,
|
|
2086
1988
|
)
|
|
2087
1989
|
|
|
1990
|
+
def new_scenario_test_with_result(
|
|
1991
|
+
self,
|
|
1992
|
+
id: str,
|
|
1993
|
+
name: str,
|
|
1994
|
+
scenarios: list[Scenario],
|
|
1995
|
+
description: Optional[str] = None,
|
|
1996
|
+
repetitions: Optional[int] = 0,
|
|
1997
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
1998
|
+
) -> BatchExperiment:
|
|
1999
|
+
"""
|
|
2000
|
+
Convenience method to create a new scenario test and poll for the
|
|
2001
|
+
result.
|
|
2002
|
+
|
|
2003
|
+
This method combines the `new_scenario_test` and
|
|
2004
|
+
`scenario_test_with_polling` methods, applying polling logic to
|
|
2005
|
+
check when the test succeeded.
|
|
2006
|
+
|
|
2007
|
+
The scenario tests uses the batch experiments API under the hood.
|
|
2008
|
+
|
|
2009
|
+
Parameters
|
|
2010
|
+
----------
|
|
2011
|
+
id: str
|
|
2012
|
+
ID of the scenario test.
|
|
2013
|
+
name: str
|
|
2014
|
+
Name of the scenario test.
|
|
2015
|
+
scenarios: list[Scenario]
|
|
2016
|
+
List of scenarios to use for the scenario test. At least one
|
|
2017
|
+
scenario should be provided.
|
|
2018
|
+
description: Optional[str]
|
|
2019
|
+
Optional description of the scenario test.
|
|
2020
|
+
repetitions: Optional[int]
|
|
2021
|
+
Number of repetitions to use for the scenario test. 0
|
|
2022
|
+
repetitions means that the tests will be executed once. 1
|
|
2023
|
+
repetition means that the test will be repeated once, i.e.: it
|
|
2024
|
+
will be executed twice. 2 repetitions equals 3 executions, so on,
|
|
2025
|
+
and so forth.
|
|
2026
|
+
|
|
2027
|
+
Returns
|
|
2028
|
+
-------
|
|
2029
|
+
BatchExperiment
|
|
2030
|
+
The completed scenario test as a BatchExperiment.
|
|
2031
|
+
|
|
2032
|
+
Raises
|
|
2033
|
+
------
|
|
2034
|
+
requests.HTTPError
|
|
2035
|
+
If the response status code is not 2xx.
|
|
2036
|
+
ValueError
|
|
2037
|
+
If no scenarios are provided.
|
|
2038
|
+
"""
|
|
2039
|
+
|
|
2040
|
+
test_id = self.new_scenario_test(
|
|
2041
|
+
id=id,
|
|
2042
|
+
name=name,
|
|
2043
|
+
scenarios=scenarios,
|
|
2044
|
+
description=description,
|
|
2045
|
+
repetitions=repetitions,
|
|
2046
|
+
)
|
|
2047
|
+
|
|
2048
|
+
return self.scenario_test_with_polling(
|
|
2049
|
+
scenario_test_id=test_id,
|
|
2050
|
+
polling_options=polling_options,
|
|
2051
|
+
)
|
|
2052
|
+
|
|
2088
2053
|
def new_secrets_collection(
|
|
2089
2054
|
self,
|
|
2090
2055
|
secrets: list[Secret],
|
|
@@ -2236,7 +2201,7 @@ class Application:
|
|
|
2236
2201
|
return self.version(version_id=id)
|
|
2237
2202
|
|
|
2238
2203
|
if id is None:
|
|
2239
|
-
id =
|
|
2204
|
+
id = safe_id(prefix="version")
|
|
2240
2205
|
|
|
2241
2206
|
payload = {
|
|
2242
2207
|
"id": id,
|
|
@@ -2567,7 +2532,7 @@ class Application:
|
|
|
2567
2532
|
def run_result_with_polling(
|
|
2568
2533
|
self,
|
|
2569
2534
|
run_id: str,
|
|
2570
|
-
polling_options: PollingOptions =
|
|
2535
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2571
2536
|
output_dir_path: Optional[str] = ".",
|
|
2572
2537
|
) -> RunResult:
|
|
2573
2538
|
"""
|
|
@@ -2638,9 +2603,9 @@ class Application:
|
|
|
2638
2603
|
"""
|
|
2639
2604
|
Get a scenario test.
|
|
2640
2605
|
|
|
2641
|
-
Retrieves a scenario test by ID. Scenario tests are based on batch
|
|
2642
|
-
so this function returns the corresponding batch
|
|
2643
|
-
the scenario test.
|
|
2606
|
+
Retrieves a scenario test by ID. Scenario tests are based on batch
|
|
2607
|
+
experiments, so this function returns the corresponding batch
|
|
2608
|
+
experiment associated with the scenario test.
|
|
2644
2609
|
|
|
2645
2610
|
Parameters
|
|
2646
2611
|
----------
|
|
@@ -2668,7 +2633,88 @@ class Application:
|
|
|
2668
2633
|
|
|
2669
2634
|
return self.batch_experiment(batch_id=scenario_test_id)
|
|
2670
2635
|
|
|
2671
|
-
def
|
|
2636
|
+
def scenario_test_metadata(self, scenario_test_id: str) -> BatchExperimentMetadata:
|
|
2637
|
+
"""
|
|
2638
|
+
Get the metadata for a scenario test, given its ID.
|
|
2639
|
+
|
|
2640
|
+
Scenario tests are based on batch experiments, so this function returns
|
|
2641
|
+
the corresponding batch experiment metadata associated with the
|
|
2642
|
+
scenario test.
|
|
2643
|
+
|
|
2644
|
+
Parameters
|
|
2645
|
+
----------
|
|
2646
|
+
scenario_test_id : str
|
|
2647
|
+
ID of the scenario test to retrieve.
|
|
2648
|
+
|
|
2649
|
+
Returns
|
|
2650
|
+
-------
|
|
2651
|
+
BatchExperimentMetadata
|
|
2652
|
+
The scenario test metadata as a batch experiment.
|
|
2653
|
+
|
|
2654
|
+
Raises
|
|
2655
|
+
------
|
|
2656
|
+
requests.HTTPError
|
|
2657
|
+
If the response status code is not 2xx.
|
|
2658
|
+
|
|
2659
|
+
Examples
|
|
2660
|
+
--------
|
|
2661
|
+
>>> metadata = app.scenario_test_metadata("scenario-123")
|
|
2662
|
+
>>> print(metadata.name)
|
|
2663
|
+
'My Scenario Test'
|
|
2664
|
+
>>> print(metadata.type)
|
|
2665
|
+
'scenario'
|
|
2666
|
+
"""
|
|
2667
|
+
|
|
2668
|
+
return self.batch_experiment_metadata(batch_id=scenario_test_id)
|
|
2669
|
+
|
|
2670
|
+
def scenario_test_with_polling(
|
|
2671
|
+
self,
|
|
2672
|
+
scenario_test_id: str,
|
|
2673
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2674
|
+
) -> BatchExperiment:
|
|
2675
|
+
"""
|
|
2676
|
+
Get a scenario test with polling.
|
|
2677
|
+
|
|
2678
|
+
Retrieves the result of a scenario test. This method polls for the
|
|
2679
|
+
result until the test finishes executing or the polling strategy is
|
|
2680
|
+
exhausted.
|
|
2681
|
+
|
|
2682
|
+
The scenario tests uses the batch experiments API under the hood.
|
|
2683
|
+
|
|
2684
|
+
Parameters
|
|
2685
|
+
----------
|
|
2686
|
+
scenario_test_id : str
|
|
2687
|
+
ID of the scenario test to retrieve.
|
|
2688
|
+
polling_options : PollingOptions, default=_DEFAULT_POLLING_OPTIONS
|
|
2689
|
+
Options to use when polling for the scenario test result.
|
|
2690
|
+
|
|
2691
|
+
Returns
|
|
2692
|
+
-------
|
|
2693
|
+
BatchExperiment
|
|
2694
|
+
The scenario test details as a batch experiment.
|
|
2695
|
+
|
|
2696
|
+
Raises
|
|
2697
|
+
------
|
|
2698
|
+
requests.HTTPError
|
|
2699
|
+
If the response status code is not 2xx.
|
|
2700
|
+
|
|
2701
|
+
Examples
|
|
2702
|
+
--------
|
|
2703
|
+
>>> test = app.scenario_test_with_polling("scenario-123")
|
|
2704
|
+
>>> print(test.name)
|
|
2705
|
+
'My Scenario Test'
|
|
2706
|
+
>>> print(test.type)
|
|
2707
|
+
'scenario'
|
|
2708
|
+
"""
|
|
2709
|
+
|
|
2710
|
+
return self.batch_experiment_with_polling(batch_id=scenario_test_id, polling_options=polling_options)
|
|
2711
|
+
|
|
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:
|
|
2672
2718
|
"""
|
|
2673
2719
|
Track an external run.
|
|
2674
2720
|
|
|
@@ -2677,6 +2723,14 @@ class Application:
|
|
|
2677
2723
|
information about a run in Nextmv is useful for things like
|
|
2678
2724
|
experimenting and testing.
|
|
2679
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
|
+
|
|
2680
2734
|
Parameters
|
|
2681
2735
|
----------
|
|
2682
2736
|
tracked_run : TrackedRun
|
|
@@ -2684,6 +2738,11 @@ class Application:
|
|
|
2684
2738
|
instance_id : Optional[str], default=None
|
|
2685
2739
|
Optional instance ID if you want to associate your tracked run with
|
|
2686
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.
|
|
2687
2746
|
|
|
2688
2747
|
Returns
|
|
2689
2748
|
-------
|
|
@@ -2706,22 +2765,55 @@ class Application:
|
|
|
2706
2765
|
>>> run_id = app.track_run(tracked_run)
|
|
2707
2766
|
"""
|
|
2708
2767
|
|
|
2768
|
+
# Get the URL to upload the input to.
|
|
2709
2769
|
url_input = self.upload_url()
|
|
2710
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.
|
|
2711
2785
|
upload_input = tracked_run.input
|
|
2712
|
-
if isinstance(tracked_run.input, Input):
|
|
2786
|
+
if upload_input is not None and isinstance(tracked_run.input, Input):
|
|
2713
2787
|
upload_input = tracked_run.input.data
|
|
2714
2788
|
|
|
2715
|
-
|
|
2789
|
+
# Actually uploads de input.
|
|
2790
|
+
self.upload_large_input(input=upload_input, upload_url=url_input, tar_file=input_tar_file)
|
|
2716
2791
|
|
|
2792
|
+
# Get the URL to upload the output to.
|
|
2717
2793
|
url_output = self.upload_url()
|
|
2718
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.
|
|
2719
2809
|
upload_output = tracked_run.output
|
|
2720
|
-
if isinstance(tracked_run.output, Output):
|
|
2810
|
+
if upload_output is not None and isinstance(tracked_run.output, Output):
|
|
2721
2811
|
upload_output = tracked_run.output.to_dict()
|
|
2722
2812
|
|
|
2723
|
-
|
|
2813
|
+
# Actually uploads the output.
|
|
2814
|
+
self.upload_large_input(input=upload_output, upload_url=url_output, tar_file=output_tar_file)
|
|
2724
2815
|
|
|
2816
|
+
# Create the external run result and appends logs if required.
|
|
2725
2817
|
external_result = ExternalRunResult(
|
|
2726
2818
|
output_upload_id=url_output.upload_id,
|
|
2727
2819
|
status=tracked_run.status.value,
|
|
@@ -2740,14 +2832,18 @@ class Application:
|
|
|
2740
2832
|
upload_id=url_input.upload_id,
|
|
2741
2833
|
external_result=external_result,
|
|
2742
2834
|
instance_id=instance_id,
|
|
2835
|
+
name=tracked_run.name,
|
|
2836
|
+
description=tracked_run.description,
|
|
2837
|
+
configuration=configuration,
|
|
2743
2838
|
)
|
|
2744
2839
|
|
|
2745
2840
|
def track_run_with_result(
|
|
2746
2841
|
self,
|
|
2747
2842
|
tracked_run: TrackedRun,
|
|
2748
|
-
polling_options: PollingOptions =
|
|
2843
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2749
2844
|
instance_id: Optional[str] = None,
|
|
2750
2845
|
output_dir_path: Optional[str] = ".",
|
|
2846
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]] = None,
|
|
2751
2847
|
) -> RunResult:
|
|
2752
2848
|
"""
|
|
2753
2849
|
Track an external run and poll for the result. This is a convenience
|
|
@@ -2768,6 +2864,11 @@ class Application:
|
|
|
2768
2864
|
Path to a directory where non-JSON output files will be saved. This is
|
|
2769
2865
|
required if the output is non-JSON. If the directory does not exist, it
|
|
2770
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.
|
|
2771
2872
|
|
|
2772
2873
|
Returns
|
|
2773
2874
|
-------
|
|
@@ -2787,7 +2888,11 @@ class Application:
|
|
|
2787
2888
|
If the run does not succeed after the polling strategy is
|
|
2788
2889
|
exhausted based on number of tries.
|
|
2789
2890
|
"""
|
|
2790
|
-
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
|
+
)
|
|
2791
2896
|
|
|
2792
2897
|
return self.run_result_with_polling(
|
|
2793
2898
|
run_id=run_id,
|
|
@@ -3393,6 +3498,47 @@ class Application:
|
|
|
3393
3498
|
|
|
3394
3499
|
return result
|
|
3395
3500
|
|
|
3501
|
+
@staticmethod
|
|
3502
|
+
def __convert_manifest_to_payload(manifest: Manifest) -> dict[str, Any]:
|
|
3503
|
+
"""Converts a manifest to a payload dictionary for the API."""
|
|
3504
|
+
|
|
3505
|
+
activation_request = {
|
|
3506
|
+
"requirements": {
|
|
3507
|
+
"executable_type": manifest.type,
|
|
3508
|
+
"runtime": manifest.runtime,
|
|
3509
|
+
},
|
|
3510
|
+
}
|
|
3511
|
+
|
|
3512
|
+
if manifest.configuration is not None and manifest.configuration.content is not None:
|
|
3513
|
+
content = manifest.configuration.content
|
|
3514
|
+
io_config = {
|
|
3515
|
+
"format": content.format,
|
|
3516
|
+
}
|
|
3517
|
+
if content.multi_file is not None:
|
|
3518
|
+
multi_config = io_config["multi_file"] = {}
|
|
3519
|
+
if content.multi_file.input is not None:
|
|
3520
|
+
multi_config["input_path"] = content.multi_file.input.path
|
|
3521
|
+
if content.multi_file.output is not None:
|
|
3522
|
+
output_config = multi_config["output_configuration"] = {}
|
|
3523
|
+
if content.multi_file.output.statistics:
|
|
3524
|
+
output_config["statistics_path"] = content.multi_file.output.statistics
|
|
3525
|
+
if content.multi_file.output.assets:
|
|
3526
|
+
output_config["assets_path"] = content.multi_file.output.assets
|
|
3527
|
+
if content.multi_file.output.solutions:
|
|
3528
|
+
output_config["solutions_path"] = content.multi_file.output.solutions
|
|
3529
|
+
activation_request["requirements"]["io_configuration"] = io_config
|
|
3530
|
+
|
|
3531
|
+
if manifest.configuration is not None and manifest.configuration.options is not None:
|
|
3532
|
+
options = manifest.configuration.options.to_dict()
|
|
3533
|
+
if "format" in options and isinstance(options["format"], list):
|
|
3534
|
+
# the endpoint expects a dictionary with a template key having a list of strings
|
|
3535
|
+
# the app.yaml however defines format as a list of strings, so we need to convert it here
|
|
3536
|
+
options["format"] = {
|
|
3537
|
+
"template": options["format"],
|
|
3538
|
+
}
|
|
3539
|
+
activation_request["requirements"]["options"] = options
|
|
3540
|
+
return activation_request
|
|
3541
|
+
|
|
3396
3542
|
def __update_app_binary(
|
|
3397
3543
|
self,
|
|
3398
3544
|
tar_file: str,
|
|
@@ -3419,27 +3565,10 @@ class Application:
|
|
|
3419
3565
|
headers={"Content-Type": "application/octet-stream"},
|
|
3420
3566
|
)
|
|
3421
3567
|
|
|
3422
|
-
activation_request = {
|
|
3423
|
-
"requirements": {
|
|
3424
|
-
"executable_type": manifest.type,
|
|
3425
|
-
"runtime": manifest.runtime,
|
|
3426
|
-
},
|
|
3427
|
-
}
|
|
3428
|
-
|
|
3429
|
-
if manifest.configuration is not None and manifest.configuration.options is not None:
|
|
3430
|
-
options = manifest.configuration.options.to_dict()
|
|
3431
|
-
if "format" in options and isinstance(options["format"], list):
|
|
3432
|
-
# the endpoint expects a dictionary with a template key having a list of strings
|
|
3433
|
-
# the app.yaml however defines format as a list of strings, so we need to convert it here
|
|
3434
|
-
options["format"] = {
|
|
3435
|
-
"template": options["format"],
|
|
3436
|
-
}
|
|
3437
|
-
activation_request["requirements"]["options"] = options
|
|
3438
|
-
|
|
3439
3568
|
response = self.client.request(
|
|
3440
3569
|
method="PUT",
|
|
3441
3570
|
endpoint=endpoint,
|
|
3442
|
-
payload=
|
|
3571
|
+
payload=Application.__convert_manifest_to_payload(manifest=manifest),
|
|
3443
3572
|
)
|
|
3444
3573
|
|
|
3445
3574
|
if verbose:
|
|
@@ -3469,7 +3598,7 @@ class Application:
|
|
|
3469
3598
|
# If working with a list of managed inputs, we need to create an
|
|
3470
3599
|
# input set.
|
|
3471
3600
|
if scenario.scenario_input.scenario_input_type == ScenarioInputType.INPUT:
|
|
3472
|
-
name, id =
|
|
3601
|
+
name, id = safe_name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
3473
3602
|
input_set = self.new_input_set(
|
|
3474
3603
|
id=id,
|
|
3475
3604
|
name=name,
|
|
@@ -3489,7 +3618,7 @@ class Application:
|
|
|
3489
3618
|
for data in scenario.scenario_input.scenario_input_data:
|
|
3490
3619
|
upload_url = self.upload_url()
|
|
3491
3620
|
self.upload_large_input(input=data, upload_url=upload_url)
|
|
3492
|
-
name, id =
|
|
3621
|
+
name, id = safe_name_and_id(prefix="man-input", entity_id=scenario_id)
|
|
3493
3622
|
managed_input = self.new_managed_input(
|
|
3494
3623
|
id=id,
|
|
3495
3624
|
name=name,
|
|
@@ -3498,7 +3627,7 @@ class Application:
|
|
|
3498
3627
|
)
|
|
3499
3628
|
managed_inputs.append(managed_input)
|
|
3500
3629
|
|
|
3501
|
-
name, id =
|
|
3630
|
+
name, id = safe_name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
3502
3631
|
input_set = self.new_input_set(
|
|
3503
3632
|
id=id,
|
|
3504
3633
|
name=name,
|
|
@@ -3510,15 +3639,16 @@ class Application:
|
|
|
3510
3639
|
|
|
3511
3640
|
raise ValueError(f"Unknown scenario input type: {scenario.scenario_input.scenario_input_type}")
|
|
3512
3641
|
|
|
3513
|
-
def
|
|
3642
|
+
def __validate_input_dir_path_and_configuration(
|
|
3514
3643
|
self,
|
|
3515
|
-
|
|
3516
|
-
configuration: Optional[RunConfiguration],
|
|
3644
|
+
input_dir_path: Optional[str],
|
|
3645
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]],
|
|
3517
3646
|
) -> None:
|
|
3518
3647
|
"""
|
|
3519
3648
|
Auxiliary function to validate the directory path and configuration.
|
|
3520
3649
|
"""
|
|
3521
|
-
|
|
3650
|
+
|
|
3651
|
+
if input_dir_path is None or input_dir_path == "":
|
|
3522
3652
|
return
|
|
3523
3653
|
|
|
3524
3654
|
if configuration is None:
|
|
@@ -3526,23 +3656,57 @@ class Application:
|
|
|
3526
3656
|
"If dir_path is provided, a RunConfiguration must also be provided.",
|
|
3527
3657
|
)
|
|
3528
3658
|
|
|
3529
|
-
|
|
3659
|
+
config_format = self.__extract_config_format(configuration)
|
|
3660
|
+
|
|
3661
|
+
if config_format is None:
|
|
3530
3662
|
raise ValueError(
|
|
3531
3663
|
"If dir_path is provided, RunConfiguration.format must also be provided.",
|
|
3532
3664
|
)
|
|
3533
3665
|
|
|
3534
|
-
|
|
3535
|
-
raise ValueError(
|
|
3536
|
-
"If dir_path is provided, RunConfiguration.format.format_input must also be provided.",
|
|
3537
|
-
)
|
|
3666
|
+
input_type = self.__extract_input_type(config_format)
|
|
3538
3667
|
|
|
3539
|
-
input_type = configuration.format.format_input.input_type
|
|
3540
3668
|
if input_type is None or input_type in (InputFormat.JSON, InputFormat.TEXT):
|
|
3541
3669
|
raise ValueError(
|
|
3542
|
-
"If dir_path is provided, RunConfiguration.format.format_input.input_type must be set to a valid type."
|
|
3670
|
+
"If dir_path is provided, RunConfiguration.format.format_input.input_type must be set to a valid type. "
|
|
3543
3671
|
f"Valid types are: {[InputFormat.CSV_ARCHIVE, InputFormat.MULTI_FILE]}",
|
|
3544
3672
|
)
|
|
3545
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
|
+
|
|
3546
3710
|
def __package_inputs(self, dir_path: str) -> str:
|
|
3547
3711
|
"""
|
|
3548
3712
|
This is an auxiliary function for packaging the inputs found in the
|
|
@@ -3604,153 +3768,72 @@ class Application:
|
|
|
3604
3768
|
|
|
3605
3769
|
return size_exceeds or non_json_payload
|
|
3606
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
|
+
"""
|
|
3607
3779
|
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
You can import the `poll` function directly from `cloud`:
|
|
3617
|
-
|
|
3618
|
-
```python
|
|
3619
|
-
from nextmv.cloud import poll
|
|
3620
|
-
```
|
|
3621
|
-
|
|
3622
|
-
This function implements a flexible polling strategy with exponential backoff
|
|
3623
|
-
and jitter. It calls the provided polling function repeatedly until it indicates
|
|
3624
|
-
success, the maximum number of tries is reached, or the maximum duration is exceeded.
|
|
3625
|
-
|
|
3626
|
-
The `polling_func` is a callable that must return a `tuple[Any, bool]`
|
|
3627
|
-
where the first element is the result of the polling and the second
|
|
3628
|
-
element is a boolean indicating if the polling was successful or should be
|
|
3629
|
-
retried.
|
|
3630
|
-
|
|
3631
|
-
Parameters
|
|
3632
|
-
----------
|
|
3633
|
-
polling_options : PollingOptions
|
|
3634
|
-
Options for configuring the polling behavior, including retry counts,
|
|
3635
|
-
delays, timeouts, and verbosity settings.
|
|
3636
|
-
polling_func : callable
|
|
3637
|
-
Function to call to check if the polling was successful. Must return a tuple
|
|
3638
|
-
where the first element is the result value and the second is a boolean
|
|
3639
|
-
indicating success (True) or need to retry (False).
|
|
3640
|
-
|
|
3641
|
-
Returns
|
|
3642
|
-
-------
|
|
3643
|
-
Any
|
|
3644
|
-
Result value from the polling function when successful.
|
|
3645
|
-
|
|
3646
|
-
Raises
|
|
3647
|
-
------
|
|
3648
|
-
TimeoutError
|
|
3649
|
-
If the polling exceeds the maximum duration specified in polling_options.
|
|
3650
|
-
RuntimeError
|
|
3651
|
-
If the maximum number of tries is exhausted without success.
|
|
3652
|
-
|
|
3653
|
-
Examples
|
|
3654
|
-
--------
|
|
3655
|
-
>>> from nextmv.cloud import PollingOptions, poll
|
|
3656
|
-
>>> import time
|
|
3657
|
-
>>>
|
|
3658
|
-
>>> # Define a polling function that succeeds after 3 tries
|
|
3659
|
-
>>> counter = 0
|
|
3660
|
-
>>> def check_completion() -> tuple[str, bool]:
|
|
3661
|
-
... global counter
|
|
3662
|
-
... counter += 1
|
|
3663
|
-
... if counter >= 3:
|
|
3664
|
-
... return "Success", True
|
|
3665
|
-
... return None, False
|
|
3666
|
-
...
|
|
3667
|
-
>>> # Configure polling options
|
|
3668
|
-
>>> options = PollingOptions(
|
|
3669
|
-
... max_tries=5,
|
|
3670
|
-
... delay=0.1,
|
|
3671
|
-
... backoff=0.2,
|
|
3672
|
-
... verbose=True
|
|
3673
|
-
... )
|
|
3674
|
-
>>>
|
|
3675
|
-
>>> # Poll until the function succeeds
|
|
3676
|
-
>>> result = poll(options, check_completion)
|
|
3677
|
-
>>> print(result)
|
|
3678
|
-
'Success'
|
|
3679
|
-
"""
|
|
3680
|
-
|
|
3681
|
-
# Start by sleeping for the duration specified as initial delay.
|
|
3682
|
-
if polling_options.verbose:
|
|
3683
|
-
log(f"polling | sleeping for initial delay: {polling_options.initial_delay}")
|
|
3684
|
-
|
|
3685
|
-
__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
|
|
3686
3787
|
|
|
3687
|
-
|
|
3688
|
-
stopped = False
|
|
3788
|
+
return input_data
|
|
3689
3789
|
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
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
|
+
"""
|
|
3698
3799
|
|
|
3699
|
-
|
|
3700
|
-
if
|
|
3701
|
-
|
|
3800
|
+
options_dict = {}
|
|
3801
|
+
if options is not None:
|
|
3802
|
+
if isinstance(options, Options):
|
|
3803
|
+
options_dict = options.to_dict_cloud()
|
|
3702
3804
|
|
|
3703
|
-
|
|
3805
|
+
elif isinstance(options, dict):
|
|
3806
|
+
for k, v in options.items():
|
|
3807
|
+
if isinstance(v, str):
|
|
3808
|
+
options_dict[k] = v
|
|
3809
|
+
continue
|
|
3704
3810
|
|
|
3705
|
-
|
|
3706
|
-
result, ok = polling_func()
|
|
3707
|
-
if polling_options.verbose:
|
|
3708
|
-
log(f"polling | try # {ix + 1}, ok: {ok}")
|
|
3811
|
+
options_dict[k] = deflated_serialize_json(v, json_configurations=json_configurations)
|
|
3709
3812
|
|
|
3710
|
-
|
|
3711
|
-
return result
|
|
3813
|
+
return options_dict
|
|
3712
3814
|
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
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
|
+
"""
|
|
3717
3825
|
|
|
3718
|
-
if
|
|
3719
|
-
|
|
3720
|
-
|
|
3826
|
+
if configuration is not None:
|
|
3827
|
+
configuration_dict = (
|
|
3828
|
+
configuration.to_dict() if isinstance(configuration, RunConfiguration) else configuration
|
|
3721
3829
|
)
|
|
3830
|
+
return configuration_dict
|
|
3722
3831
|
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
# delay to avoid overflows.
|
|
3727
|
-
delay = polling_options.max_delay
|
|
3728
|
-
delay += random.uniform(0, polling_options.jitter) # Add jitter.
|
|
3729
|
-
else:
|
|
3730
|
-
delay = polling_options.delay # Base
|
|
3731
|
-
delay += polling_options.backoff * (2**ix) # Add exponential backoff.
|
|
3732
|
-
delay += random.uniform(0, polling_options.jitter) # Add jitter.
|
|
3733
|
-
|
|
3734
|
-
# We cannot exceed the max delay.
|
|
3735
|
-
if delay >= polling_options.max_delay:
|
|
3736
|
-
max_reached = True
|
|
3737
|
-
delay = polling_options.max_delay
|
|
3738
|
-
|
|
3739
|
-
# Sleep for the calculated delay.
|
|
3740
|
-
sleep_duration = delay
|
|
3741
|
-
if polling_options.verbose:
|
|
3742
|
-
log(f"polling | sleeping for duration: {sleep_duration}")
|
|
3743
|
-
|
|
3744
|
-
__sleep_func(sleep_duration)
|
|
3745
|
-
|
|
3746
|
-
if stopped:
|
|
3747
|
-
log("polling | stop condition met, stopping polling")
|
|
3748
|
-
|
|
3749
|
-
return None
|
|
3832
|
+
configuration = RunConfiguration()
|
|
3833
|
+
configuration.resolve(input=input, dir_path=dir_path)
|
|
3834
|
+
configuration_dict = configuration.to_dict()
|
|
3750
3835
|
|
|
3751
|
-
|
|
3752
|
-
f"polling did not succeed after {polling_options.max_tries} tries",
|
|
3753
|
-
)
|
|
3836
|
+
return configuration_dict
|
|
3754
3837
|
|
|
3755
3838
|
|
|
3756
3839
|
def _is_not_exist_error(e: requests.HTTPError) -> bool:
|