nextmv 0.30.0__py3-none-any.whl → 0.32.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 +35 -0
- nextmv/cloud/__init__.py +38 -33
- nextmv/cloud/account.py +1 -1
- nextmv/cloud/application.py +249 -484
- nextmv/cloud/batch_experiment.py +7 -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/README.md +17 -2
- nextmv/default_app/app.yaml +1 -0
- nextmv/default_app/input.json +5 -0
- nextmv/default_app/main.py +37 -0
- nextmv/default_app/src/main.py +2 -1
- nextmv/input.py +11 -1
- nextmv/local/__init__.py +5 -0
- nextmv/local/application.py +1173 -0
- nextmv/local/executor.py +713 -0
- nextmv/local/geojson_handler.py +323 -0
- nextmv/local/local.py +97 -0
- nextmv/local/plotly_handler.py +61 -0
- nextmv/local/runner.py +274 -0
- nextmv/{cloud/manifest.py → manifest.py} +110 -69
- nextmv/output.py +41 -8
- nextmv/polling.py +287 -0
- nextmv/run.py +1460 -0
- nextmv/{cloud/safe.py → safe.py} +35 -3
- nextmv/{cloud/status.py → status.py} +9 -9
- {nextmv-0.30.0.dist-info → nextmv-0.32.0.dist-info}/METADATA +5 -1
- nextmv-0.32.0.dist-info/RECORD +49 -0
- nextmv/cloud/run.py +0 -755
- nextmv-0.30.0.dist-info/RECORD +0 -37
- {nextmv-0.30.0.dist-info → nextmv-0.32.0.dist-info}/WHEEL +0 -0
- {nextmv-0.30.0.dist-info → nextmv-0.32.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,28 +48,31 @@ 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,
|
|
60
66
|
FormatOutput,
|
|
67
|
+
Run,
|
|
61
68
|
RunConfiguration,
|
|
62
69
|
RunInformation,
|
|
63
70
|
RunLog,
|
|
64
71
|
RunResult,
|
|
65
72
|
TrackedRun,
|
|
66
73
|
)
|
|
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
|
|
74
|
+
from nextmv.safe import safe_id, safe_name_and_id
|
|
75
|
+
from nextmv.status import StatusV2
|
|
77
76
|
|
|
78
77
|
# Maximum size of the run input/output in bytes. This constant defines the
|
|
79
78
|
# maximum allowed size for run inputs and outputs. When the size exceeds this
|
|
@@ -82,180 +81,6 @@ from nextmv.output import Output, OutputFormat
|
|
|
82
81
|
_MAX_RUN_SIZE: int = 5 * 1024 * 1024
|
|
83
82
|
|
|
84
83
|
|
|
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
84
|
@dataclass
|
|
260
85
|
class Application:
|
|
261
86
|
"""
|
|
@@ -305,15 +130,6 @@ class Application:
|
|
|
305
130
|
experiments_endpoint: str = "{base}/experiments"
|
|
306
131
|
"""Base endpoint for the experiments in the application."""
|
|
307
132
|
|
|
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
133
|
def __post_init__(self):
|
|
318
134
|
"""Initialize the endpoint and experiments_endpoint attributes.
|
|
319
135
|
|
|
@@ -323,92 +139,6 @@ class Application:
|
|
|
323
139
|
self.endpoint = self.endpoint.format(id=self.id)
|
|
324
140
|
self.experiments_endpoint = self.experiments_endpoint.format(base=self.endpoint)
|
|
325
141
|
|
|
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
142
|
@classmethod
|
|
413
143
|
def new(
|
|
414
144
|
cls,
|
|
@@ -510,7 +240,7 @@ class Application:
|
|
|
510
240
|
def acceptance_test_with_polling(
|
|
511
241
|
self,
|
|
512
242
|
acceptance_test_id: str,
|
|
513
|
-
polling_options: PollingOptions =
|
|
243
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
514
244
|
) -> AcceptanceTest:
|
|
515
245
|
"""
|
|
516
246
|
Retrieve details of an acceptance test using polling.
|
|
@@ -560,7 +290,8 @@ class Application:
|
|
|
560
290
|
|
|
561
291
|
def batch_experiment(self, batch_id: str) -> BatchExperiment:
|
|
562
292
|
"""
|
|
563
|
-
Get a batch experiment.
|
|
293
|
+
Get a batch experiment. This method also returns the runs of the batch
|
|
294
|
+
experiment under the `.runs` attribute.
|
|
564
295
|
|
|
565
296
|
Parameters
|
|
566
297
|
----------
|
|
@@ -589,7 +320,17 @@ class Application:
|
|
|
589
320
|
endpoint=f"{self.experiments_endpoint}/batch/{batch_id}",
|
|
590
321
|
)
|
|
591
322
|
|
|
592
|
-
|
|
323
|
+
exp = BatchExperiment.from_dict(response.json())
|
|
324
|
+
|
|
325
|
+
runs_response = self.client.request(
|
|
326
|
+
method="GET",
|
|
327
|
+
endpoint=f"{self.experiments_endpoint}/batch/{batch_id}/runs",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
runs = [Run.from_dict(run) for run in runs_response.json().get("runs", [])]
|
|
331
|
+
exp.runs = runs
|
|
332
|
+
|
|
333
|
+
return exp
|
|
593
334
|
|
|
594
335
|
def batch_experiment_metadata(self, batch_id: str) -> BatchExperimentMetadata:
|
|
595
336
|
"""
|
|
@@ -627,7 +368,7 @@ class Application:
|
|
|
627
368
|
def batch_experiment_with_polling(
|
|
628
369
|
self,
|
|
629
370
|
batch_id: str,
|
|
630
|
-
polling_options: PollingOptions =
|
|
371
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
631
372
|
) -> BatchExperiment:
|
|
632
373
|
"""
|
|
633
374
|
Get a batch experiment with polling.
|
|
@@ -1089,6 +830,28 @@ class Application:
|
|
|
1089
830
|
|
|
1090
831
|
return [ManagedInput.from_dict(managed_input) for managed_input in response.json()]
|
|
1091
832
|
|
|
833
|
+
def list_runs(self) -> list[Run]:
|
|
834
|
+
"""
|
|
835
|
+
List all runs.
|
|
836
|
+
|
|
837
|
+
Returns
|
|
838
|
+
-------
|
|
839
|
+
list[Run]
|
|
840
|
+
List of runs.
|
|
841
|
+
|
|
842
|
+
Raises
|
|
843
|
+
------
|
|
844
|
+
requests.HTTPError
|
|
845
|
+
If the response status code is not 2xx.
|
|
846
|
+
"""
|
|
847
|
+
|
|
848
|
+
response = self.client.request(
|
|
849
|
+
method="GET",
|
|
850
|
+
endpoint=f"{self.endpoint}/runs",
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
return [Run.from_dict(run) for run in response.json().get("runs", [])]
|
|
854
|
+
|
|
1092
855
|
def list_scenario_tests(self) -> list[BatchExperimentMetadata]:
|
|
1093
856
|
"""
|
|
1094
857
|
List all batch scenario tests. Scenario tests are based on the batch
|
|
@@ -1327,7 +1090,7 @@ class Application:
|
|
|
1327
1090
|
name: str,
|
|
1328
1091
|
input_set_id: Optional[str] = None,
|
|
1329
1092
|
description: Optional[str] = None,
|
|
1330
|
-
polling_options: PollingOptions =
|
|
1093
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
1331
1094
|
) -> AcceptanceTest:
|
|
1332
1095
|
"""
|
|
1333
1096
|
Create a new acceptance test and poll for the result.
|
|
@@ -1491,7 +1254,7 @@ class Application:
|
|
|
1491
1254
|
option_sets: Optional[dict[str, dict[str, str]]] = None,
|
|
1492
1255
|
runs: Optional[list[Union[BatchExperimentRun, dict[str, Any]]]] = None,
|
|
1493
1256
|
type: Optional[str] = "batch",
|
|
1494
|
-
polling_options: PollingOptions =
|
|
1257
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
1495
1258
|
) -> BatchExperiment:
|
|
1496
1259
|
"""
|
|
1497
1260
|
Convenience method to create a new batch experiment and poll for the
|
|
@@ -1931,13 +1694,7 @@ class Application:
|
|
|
1931
1694
|
|
|
1932
1695
|
tar_file = self.__package_inputs(input_dir_path)
|
|
1933
1696
|
|
|
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
|
|
1697
|
+
input_data = self.__extract_input_data(input)
|
|
1941
1698
|
|
|
1942
1699
|
input_size = 0
|
|
1943
1700
|
if input_data is not None:
|
|
@@ -1950,20 +1707,10 @@ class Application:
|
|
|
1950
1707
|
upload_id = upload_url.upload_id
|
|
1951
1708
|
upload_id_used = True
|
|
1952
1709
|
|
|
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)
|
|
1710
|
+
options_dict = self.__extract_options_dict(options, json_configurations)
|
|
1966
1711
|
|
|
1712
|
+
# Builds the payload progressively based on the different arguments
|
|
1713
|
+
# that must be provided.
|
|
1967
1714
|
payload = {}
|
|
1968
1715
|
if upload_id_used:
|
|
1969
1716
|
payload["upload_id"] = upload_id
|
|
@@ -1980,15 +1727,7 @@ class Application:
|
|
|
1980
1727
|
raise ValueError(f"options must be dict[str,str], option {k} has type {type(v)} instead.")
|
|
1981
1728
|
payload["options"] = options_dict
|
|
1982
1729
|
|
|
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
|
-
|
|
1730
|
+
configuration_dict = self.__extract_run_config(input, configuration, input_dir_path)
|
|
1992
1731
|
payload["configuration"] = configuration_dict
|
|
1993
1732
|
|
|
1994
1733
|
if batch_experiment_id is not None:
|
|
@@ -2020,7 +1759,7 @@ class Application:
|
|
|
2020
1759
|
description: Optional[str] = None,
|
|
2021
1760
|
upload_id: Optional[str] = None,
|
|
2022
1761
|
run_options: Optional[Union[Options, dict[str, str]]] = None,
|
|
2023
|
-
polling_options: PollingOptions =
|
|
1762
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2024
1763
|
configuration: Optional[Union[RunConfiguration, dict[str, Any]]] = None,
|
|
2025
1764
|
batch_experiment_id: Optional[str] = None,
|
|
2026
1765
|
external_result: Optional[Union[ExternalRunResult, dict[str, Any]]] = None,
|
|
@@ -2289,7 +2028,7 @@ class Application:
|
|
|
2289
2028
|
scenarios: list[Scenario],
|
|
2290
2029
|
description: Optional[str] = None,
|
|
2291
2030
|
repetitions: Optional[int] = 0,
|
|
2292
|
-
polling_options: PollingOptions =
|
|
2031
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2293
2032
|
) -> BatchExperiment:
|
|
2294
2033
|
"""
|
|
2295
2034
|
Convenience method to create a new scenario test and poll for the
|
|
@@ -2496,7 +2235,7 @@ class Application:
|
|
|
2496
2235
|
return self.version(version_id=id)
|
|
2497
2236
|
|
|
2498
2237
|
if id is None:
|
|
2499
|
-
id =
|
|
2238
|
+
id = safe_id(prefix="version")
|
|
2500
2239
|
|
|
2501
2240
|
payload = {
|
|
2502
2241
|
"id": id,
|
|
@@ -2827,7 +2566,7 @@ class Application:
|
|
|
2827
2566
|
def run_result_with_polling(
|
|
2828
2567
|
self,
|
|
2829
2568
|
run_id: str,
|
|
2830
|
-
polling_options: PollingOptions =
|
|
2569
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2831
2570
|
output_dir_path: Optional[str] = ".",
|
|
2832
2571
|
) -> RunResult:
|
|
2833
2572
|
"""
|
|
@@ -2965,7 +2704,7 @@ class Application:
|
|
|
2965
2704
|
def scenario_test_with_polling(
|
|
2966
2705
|
self,
|
|
2967
2706
|
scenario_test_id: str,
|
|
2968
|
-
polling_options: PollingOptions =
|
|
2707
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2969
2708
|
) -> BatchExperiment:
|
|
2970
2709
|
"""
|
|
2971
2710
|
Get a scenario test with polling.
|
|
@@ -3004,7 +2743,12 @@ class Application:
|
|
|
3004
2743
|
|
|
3005
2744
|
return self.batch_experiment_with_polling(batch_id=scenario_test_id, polling_options=polling_options)
|
|
3006
2745
|
|
|
3007
|
-
def track_run(
|
|
2746
|
+
def track_run( # noqa: C901
|
|
2747
|
+
self,
|
|
2748
|
+
tracked_run: TrackedRun,
|
|
2749
|
+
instance_id: Optional[str] = None,
|
|
2750
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]] = None,
|
|
2751
|
+
) -> str:
|
|
3008
2752
|
"""
|
|
3009
2753
|
Track an external run.
|
|
3010
2754
|
|
|
@@ -3013,6 +2757,14 @@ class Application:
|
|
|
3013
2757
|
information about a run in Nextmv is useful for things like
|
|
3014
2758
|
experimenting and testing.
|
|
3015
2759
|
|
|
2760
|
+
Please read the documentation on the `TrackedRun` class carefully, as
|
|
2761
|
+
there are important considerations to take into account when using this
|
|
2762
|
+
method. For example, if you intend to upload JSON input/output, use the
|
|
2763
|
+
`input`/`output` attributes of the `TrackedRun` class. On the other
|
|
2764
|
+
hand, if you intend to track files-based input/output, use the
|
|
2765
|
+
`input_dir_path`/`output_dir_path` attributes of the `TrackedRun`
|
|
2766
|
+
class.
|
|
2767
|
+
|
|
3016
2768
|
Parameters
|
|
3017
2769
|
----------
|
|
3018
2770
|
tracked_run : TrackedRun
|
|
@@ -3020,6 +2772,11 @@ class Application:
|
|
|
3020
2772
|
instance_id : Optional[str], default=None
|
|
3021
2773
|
Optional instance ID if you want to associate your tracked run with
|
|
3022
2774
|
an instance.
|
|
2775
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]]
|
|
2776
|
+
Configuration to use for the run. This can be a
|
|
2777
|
+
`cloud.RunConfiguration` object or a dict. If the object is used,
|
|
2778
|
+
then the `.to_dict()` method is applied to extract the
|
|
2779
|
+
configuration.
|
|
3023
2780
|
|
|
3024
2781
|
Returns
|
|
3025
2782
|
-------
|
|
@@ -3036,28 +2793,61 @@ class Application:
|
|
|
3036
2793
|
Examples
|
|
3037
2794
|
--------
|
|
3038
2795
|
>>> from nextmv.cloud import Application
|
|
3039
|
-
>>> from nextmv
|
|
2796
|
+
>>> from nextmv import TrackedRun
|
|
3040
2797
|
>>> app = Application(id="app_123")
|
|
3041
2798
|
>>> tracked_run = TrackedRun(input={"data": [...]}, output={"solution": [...]})
|
|
3042
2799
|
>>> run_id = app.track_run(tracked_run)
|
|
3043
2800
|
"""
|
|
3044
2801
|
|
|
2802
|
+
# Get the URL to upload the input to.
|
|
3045
2803
|
url_input = self.upload_url()
|
|
3046
2804
|
|
|
2805
|
+
# Handle the case where the input is being uploaded as files. We need
|
|
2806
|
+
# to tar them.
|
|
2807
|
+
input_tar_file = ""
|
|
2808
|
+
input_dir_path = tracked_run.input_dir_path
|
|
2809
|
+
if input_dir_path is not None and input_dir_path != "":
|
|
2810
|
+
if not os.path.exists(input_dir_path):
|
|
2811
|
+
raise ValueError(f"Directory {input_dir_path} does not exist.")
|
|
2812
|
+
|
|
2813
|
+
if not os.path.isdir(input_dir_path):
|
|
2814
|
+
raise ValueError(f"Path {input_dir_path} is not a directory.")
|
|
2815
|
+
|
|
2816
|
+
input_tar_file = self.__package_inputs(input_dir_path)
|
|
2817
|
+
|
|
2818
|
+
# Handle the case where the input is uploaded as Input or a dict.
|
|
3047
2819
|
upload_input = tracked_run.input
|
|
3048
|
-
if isinstance(tracked_run.input, Input):
|
|
2820
|
+
if upload_input is not None and isinstance(tracked_run.input, Input):
|
|
3049
2821
|
upload_input = tracked_run.input.data
|
|
3050
2822
|
|
|
3051
|
-
|
|
2823
|
+
# Actually uploads de input.
|
|
2824
|
+
self.upload_large_input(input=upload_input, upload_url=url_input, tar_file=input_tar_file)
|
|
3052
2825
|
|
|
2826
|
+
# Get the URL to upload the output to.
|
|
3053
2827
|
url_output = self.upload_url()
|
|
3054
2828
|
|
|
2829
|
+
# Handle the case where the output is being uploaded as files. We need
|
|
2830
|
+
# to tar them.
|
|
2831
|
+
output_tar_file = ""
|
|
2832
|
+
output_dir_path = tracked_run.output_dir_path
|
|
2833
|
+
if output_dir_path is not None and output_dir_path != "":
|
|
2834
|
+
if not os.path.exists(output_dir_path):
|
|
2835
|
+
raise ValueError(f"Directory {output_dir_path} does not exist.")
|
|
2836
|
+
|
|
2837
|
+
if not os.path.isdir(output_dir_path):
|
|
2838
|
+
raise ValueError(f"Path {output_dir_path} is not a directory.")
|
|
2839
|
+
|
|
2840
|
+
output_tar_file = self.__package_inputs(output_dir_path)
|
|
2841
|
+
|
|
2842
|
+
# Handle the case where the output is uploaded as Output or a dict.
|
|
3055
2843
|
upload_output = tracked_run.output
|
|
3056
|
-
if isinstance(tracked_run.output, Output):
|
|
2844
|
+
if upload_output is not None and isinstance(tracked_run.output, Output):
|
|
3057
2845
|
upload_output = tracked_run.output.to_dict()
|
|
3058
2846
|
|
|
3059
|
-
|
|
2847
|
+
# Actually uploads the output.
|
|
2848
|
+
self.upload_large_input(input=upload_output, upload_url=url_output, tar_file=output_tar_file)
|
|
3060
2849
|
|
|
2850
|
+
# Create the external run result and appends logs if required.
|
|
3061
2851
|
external_result = ExternalRunResult(
|
|
3062
2852
|
output_upload_id=url_output.upload_id,
|
|
3063
2853
|
status=tracked_run.status.value,
|
|
@@ -3076,14 +2866,18 @@ class Application:
|
|
|
3076
2866
|
upload_id=url_input.upload_id,
|
|
3077
2867
|
external_result=external_result,
|
|
3078
2868
|
instance_id=instance_id,
|
|
2869
|
+
name=tracked_run.name,
|
|
2870
|
+
description=tracked_run.description,
|
|
2871
|
+
configuration=configuration,
|
|
3079
2872
|
)
|
|
3080
2873
|
|
|
3081
2874
|
def track_run_with_result(
|
|
3082
2875
|
self,
|
|
3083
2876
|
tracked_run: TrackedRun,
|
|
3084
|
-
polling_options: PollingOptions =
|
|
2877
|
+
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
3085
2878
|
instance_id: Optional[str] = None,
|
|
3086
2879
|
output_dir_path: Optional[str] = ".",
|
|
2880
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]] = None,
|
|
3087
2881
|
) -> RunResult:
|
|
3088
2882
|
"""
|
|
3089
2883
|
Track an external run and poll for the result. This is a convenience
|
|
@@ -3104,6 +2898,11 @@ class Application:
|
|
|
3104
2898
|
Path to a directory where non-JSON output files will be saved. This is
|
|
3105
2899
|
required if the output is non-JSON. If the directory does not exist, it
|
|
3106
2900
|
will be created. Uses the current directory by default.
|
|
2901
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]]
|
|
2902
|
+
Configuration to use for the run. This can be a
|
|
2903
|
+
`cloud.RunConfiguration` object or a dict. If the object is used,
|
|
2904
|
+
then the `.to_dict()` method is applied to extract the
|
|
2905
|
+
configuration.
|
|
3107
2906
|
|
|
3108
2907
|
Returns
|
|
3109
2908
|
-------
|
|
@@ -3123,7 +2922,11 @@ class Application:
|
|
|
3123
2922
|
If the run does not succeed after the polling strategy is
|
|
3124
2923
|
exhausted based on number of tries.
|
|
3125
2924
|
"""
|
|
3126
|
-
run_id = self.track_run(
|
|
2925
|
+
run_id = self.track_run(
|
|
2926
|
+
tracked_run=tracked_run,
|
|
2927
|
+
instance_id=instance_id,
|
|
2928
|
+
configuration=configuration,
|
|
2929
|
+
)
|
|
3127
2930
|
|
|
3128
2931
|
return self.run_result_with_polling(
|
|
3129
2932
|
run_id=run_id,
|
|
@@ -3829,7 +3632,7 @@ class Application:
|
|
|
3829
3632
|
# If working with a list of managed inputs, we need to create an
|
|
3830
3633
|
# input set.
|
|
3831
3634
|
if scenario.scenario_input.scenario_input_type == ScenarioInputType.INPUT:
|
|
3832
|
-
name, id =
|
|
3635
|
+
name, id = safe_name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
3833
3636
|
input_set = self.new_input_set(
|
|
3834
3637
|
id=id,
|
|
3835
3638
|
name=name,
|
|
@@ -3849,7 +3652,7 @@ class Application:
|
|
|
3849
3652
|
for data in scenario.scenario_input.scenario_input_data:
|
|
3850
3653
|
upload_url = self.upload_url()
|
|
3851
3654
|
self.upload_large_input(input=data, upload_url=upload_url)
|
|
3852
|
-
name, id =
|
|
3655
|
+
name, id = safe_name_and_id(prefix="man-input", entity_id=scenario_id)
|
|
3853
3656
|
managed_input = self.new_managed_input(
|
|
3854
3657
|
id=id,
|
|
3855
3658
|
name=name,
|
|
@@ -3858,7 +3661,7 @@ class Application:
|
|
|
3858
3661
|
)
|
|
3859
3662
|
managed_inputs.append(managed_input)
|
|
3860
3663
|
|
|
3861
|
-
name, id =
|
|
3664
|
+
name, id = safe_name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
3862
3665
|
input_set = self.new_input_set(
|
|
3863
3666
|
id=id,
|
|
3864
3667
|
name=name,
|
|
@@ -3873,28 +3676,71 @@ class Application:
|
|
|
3873
3676
|
def __validate_input_dir_path_and_configuration(
|
|
3874
3677
|
self,
|
|
3875
3678
|
input_dir_path: Optional[str],
|
|
3876
|
-
configuration: Optional[RunConfiguration],
|
|
3679
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]],
|
|
3877
3680
|
) -> None:
|
|
3878
3681
|
"""
|
|
3879
3682
|
Auxiliary function to validate the directory path and configuration.
|
|
3880
3683
|
"""
|
|
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.
|
|
3684
|
+
|
|
3685
|
+
if input_dir_path is None or input_dir_path == "":
|
|
3888
3686
|
return
|
|
3889
3687
|
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3688
|
+
if configuration is None:
|
|
3689
|
+
raise ValueError(
|
|
3690
|
+
"If dir_path is provided, a RunConfiguration must also be provided.",
|
|
3691
|
+
)
|
|
3692
|
+
|
|
3693
|
+
config_format = self.__extract_config_format(configuration)
|
|
3694
|
+
|
|
3695
|
+
if config_format is None:
|
|
3696
|
+
raise ValueError(
|
|
3697
|
+
"If dir_path is provided, RunConfiguration.format must also be provided.",
|
|
3698
|
+
)
|
|
3699
|
+
|
|
3700
|
+
input_type = self.__extract_input_type(config_format)
|
|
3701
|
+
|
|
3702
|
+
if input_type is None or input_type in (InputFormat.JSON, InputFormat.TEXT):
|
|
3703
|
+
raise ValueError(
|
|
3704
|
+
"If dir_path is provided, RunConfiguration.format.format_input.input_type must be set to a valid type. "
|
|
3705
|
+
f"Valid types are: {[InputFormat.CSV_ARCHIVE, InputFormat.MULTI_FILE]}",
|
|
3706
|
+
)
|
|
3707
|
+
|
|
3708
|
+
def __extract_config_format(self, configuration: Union[RunConfiguration, dict[str, Any]]) -> Any:
|
|
3709
|
+
"""Extract format from configuration, handling both RunConfiguration objects and dicts."""
|
|
3710
|
+
if isinstance(configuration, RunConfiguration):
|
|
3711
|
+
return configuration.format
|
|
3712
|
+
|
|
3713
|
+
if isinstance(configuration, dict):
|
|
3714
|
+
config_format = configuration.get("format")
|
|
3715
|
+
if config_format is not None and isinstance(config_format, dict):
|
|
3716
|
+
return Format.from_dict(config_format) if hasattr(Format, "from_dict") else config_format
|
|
3717
|
+
|
|
3718
|
+
return config_format
|
|
3719
|
+
|
|
3720
|
+
raise ValueError("Configuration must be a RunConfiguration object or a dict.")
|
|
3721
|
+
|
|
3722
|
+
def __extract_input_type(self, config_format: Any) -> Any:
|
|
3723
|
+
"""Extract input type from config format."""
|
|
3724
|
+
if isinstance(config_format, dict):
|
|
3725
|
+
format_input = config_format.get("format_input") or config_format.get("input")
|
|
3726
|
+
if format_input is None:
|
|
3727
|
+
raise ValueError(
|
|
3728
|
+
"If dir_path is provided, RunConfiguration.format.format_input must also be provided.",
|
|
3729
|
+
)
|
|
3730
|
+
|
|
3731
|
+
if isinstance(format_input, dict):
|
|
3732
|
+
return format_input.get("input_type") or format_input.get("type")
|
|
3733
|
+
|
|
3734
|
+
return getattr(format_input, "input_type", None)
|
|
3735
|
+
|
|
3736
|
+
# Handle Format object
|
|
3737
|
+
if config_format.format_input is None:
|
|
3893
3738
|
raise ValueError(
|
|
3894
|
-
|
|
3895
|
-
"then input_dir_path must be provided.",
|
|
3739
|
+
"If dir_path is provided, RunConfiguration.format.format_input must also be provided.",
|
|
3896
3740
|
)
|
|
3897
3741
|
|
|
3742
|
+
return config_format.format_input.input_type
|
|
3743
|
+
|
|
3898
3744
|
def __package_inputs(self, dir_path: str) -> str:
|
|
3899
3745
|
"""
|
|
3900
3746
|
This is an auxiliary function for packaging the inputs found in the
|
|
@@ -3956,153 +3802,72 @@ class Application:
|
|
|
3956
3802
|
|
|
3957
3803
|
return size_exceeds or non_json_payload
|
|
3958
3804
|
|
|
3805
|
+
def __extract_input_data(
|
|
3806
|
+
self,
|
|
3807
|
+
input: Union[Input, dict[str, Any], BaseModel, str] = None,
|
|
3808
|
+
) -> Optional[Union[dict[str, Any], str]]:
|
|
3809
|
+
"""
|
|
3810
|
+
Auxiliary function to extract the input data from the input, based on
|
|
3811
|
+
its type.
|
|
3812
|
+
"""
|
|
3959
3813
|
|
|
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)
|
|
3814
|
+
input_data = None
|
|
3815
|
+
if isinstance(input, BaseModel):
|
|
3816
|
+
input_data = input.to_dict()
|
|
3817
|
+
elif isinstance(input, dict) or isinstance(input, str):
|
|
3818
|
+
input_data = input
|
|
3819
|
+
elif isinstance(input, Input):
|
|
3820
|
+
input_data = input.data
|
|
4038
3821
|
|
|
4039
|
-
|
|
4040
|
-
stopped = False
|
|
3822
|
+
return input_data
|
|
4041
3823
|
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
3824
|
+
def __extract_options_dict(
|
|
3825
|
+
self,
|
|
3826
|
+
options: Optional[Union[Options, dict[str, str]]] = None,
|
|
3827
|
+
json_configurations: Optional[dict[str, Any]] = None,
|
|
3828
|
+
) -> dict[str, str]:
|
|
3829
|
+
"""
|
|
3830
|
+
Auxiliary function to extract the options that will be sent to the
|
|
3831
|
+
application for execution.
|
|
3832
|
+
"""
|
|
4050
3833
|
|
|
4051
|
-
|
|
4052
|
-
if
|
|
4053
|
-
|
|
3834
|
+
options_dict = {}
|
|
3835
|
+
if options is not None:
|
|
3836
|
+
if isinstance(options, Options):
|
|
3837
|
+
options_dict = options.to_dict_cloud()
|
|
4054
3838
|
|
|
4055
|
-
|
|
3839
|
+
elif isinstance(options, dict):
|
|
3840
|
+
for k, v in options.items():
|
|
3841
|
+
if isinstance(v, str):
|
|
3842
|
+
options_dict[k] = v
|
|
3843
|
+
continue
|
|
4056
3844
|
|
|
4057
|
-
|
|
4058
|
-
result, ok = polling_func()
|
|
4059
|
-
if polling_options.verbose:
|
|
4060
|
-
log(f"polling | try # {ix + 1}, ok: {ok}")
|
|
3845
|
+
options_dict[k] = deflated_serialize_json(v, json_configurations=json_configurations)
|
|
4061
3846
|
|
|
4062
|
-
|
|
4063
|
-
return result
|
|
3847
|
+
return options_dict
|
|
4064
3848
|
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
3849
|
+
def __extract_run_config(
|
|
3850
|
+
self,
|
|
3851
|
+
input: Union[Input, dict[str, Any], BaseModel, str] = None,
|
|
3852
|
+
configuration: Optional[Union[RunConfiguration, dict[str, Any]]] = None,
|
|
3853
|
+
dir_path: Optional[str] = None,
|
|
3854
|
+
) -> dict[str, Any]:
|
|
3855
|
+
"""
|
|
3856
|
+
Auxiliary function to extract the run configuration that will be sent
|
|
3857
|
+
to the application for execution.
|
|
3858
|
+
"""
|
|
4069
3859
|
|
|
4070
|
-
if
|
|
4071
|
-
|
|
4072
|
-
|
|
3860
|
+
if configuration is not None:
|
|
3861
|
+
configuration_dict = (
|
|
3862
|
+
configuration.to_dict() if isinstance(configuration, RunConfiguration) else configuration
|
|
4073
3863
|
)
|
|
3864
|
+
return configuration_dict
|
|
4074
3865
|
|
|
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
|
|
3866
|
+
configuration = RunConfiguration()
|
|
3867
|
+
configuration.resolve(input=input, dir_path=dir_path)
|
|
3868
|
+
configuration_dict = configuration.to_dict()
|
|
4102
3869
|
|
|
4103
|
-
|
|
4104
|
-
f"polling did not succeed after {polling_options.max_tries} tries",
|
|
4105
|
-
)
|
|
3870
|
+
return configuration_dict
|
|
4106
3871
|
|
|
4107
3872
|
|
|
4108
3873
|
def _is_not_exist_error(e: requests.HTTPError) -> bool:
|