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/output.py
CHANGED
|
@@ -40,6 +40,21 @@ Functions
|
|
|
40
40
|
---------
|
|
41
41
|
write
|
|
42
42
|
Write the output to the specified destination.
|
|
43
|
+
|
|
44
|
+
Constants
|
|
45
|
+
---------
|
|
46
|
+
ASSETS_KEY
|
|
47
|
+
Assets key constant used for identifying assets in the run output.
|
|
48
|
+
STATISTICS_KEY
|
|
49
|
+
Statistics key constant used for identifying statistics in the run output.
|
|
50
|
+
SOLUTIONS_KEY
|
|
51
|
+
Solutions key constant used for identifying solutions in the run output.
|
|
52
|
+
OUTPUTS_KEY
|
|
53
|
+
Outputs key constant used for identifying outputs in the run output.
|
|
54
|
+
LOGS_FILE
|
|
55
|
+
Constant used for identifying the file used for logging.
|
|
56
|
+
DEFAULT_OUTPUT_JSON_FILE
|
|
57
|
+
Constant for the default output JSON file name.
|
|
43
58
|
"""
|
|
44
59
|
|
|
45
60
|
import copy
|
|
@@ -59,6 +74,39 @@ from nextmv.deprecated import deprecated
|
|
|
59
74
|
from nextmv.logger import reset_stdout
|
|
60
75
|
from nextmv.options import Options
|
|
61
76
|
|
|
77
|
+
ASSETS_KEY = "assets"
|
|
78
|
+
"""
|
|
79
|
+
Assets key constant used for identifying assets in the run output.
|
|
80
|
+
"""
|
|
81
|
+
STATISTICS_KEY = "statistics"
|
|
82
|
+
"""
|
|
83
|
+
Statistics key constant used for identifying statistics in the run output.
|
|
84
|
+
"""
|
|
85
|
+
SOLUTIONS_KEY = "solutions"
|
|
86
|
+
"""
|
|
87
|
+
Solutions key constant used for identifying solutions in the run output.
|
|
88
|
+
"""
|
|
89
|
+
OUTPUTS_KEY = "outputs"
|
|
90
|
+
"""
|
|
91
|
+
Outputs key constant used for identifying outputs in the run output.
|
|
92
|
+
"""
|
|
93
|
+
OUTPUT_KEY = "output"
|
|
94
|
+
"""
|
|
95
|
+
Output key constant used for identifying output in the run output.
|
|
96
|
+
"""
|
|
97
|
+
LOGS_KEY = "logs"
|
|
98
|
+
"""
|
|
99
|
+
Logs key constant used for identifying logs in the run output.
|
|
100
|
+
"""
|
|
101
|
+
LOGS_FILE = "stderr.log"
|
|
102
|
+
"""
|
|
103
|
+
Constant used for identifying the file used for logging.
|
|
104
|
+
"""
|
|
105
|
+
DEFAULT_OUTPUT_JSON_FILE = "solution.json"
|
|
106
|
+
"""
|
|
107
|
+
Constant for the default output JSON file name.
|
|
108
|
+
"""
|
|
109
|
+
|
|
62
110
|
|
|
63
111
|
class RunStatistics(BaseModel):
|
|
64
112
|
"""
|
|
@@ -495,6 +543,8 @@ class OutputFormat(str, Enum):
|
|
|
495
543
|
CSV archive format: multiple CSV files.
|
|
496
544
|
MULTI_FILE : str
|
|
497
545
|
Multi-file format: multiple files in a directory.
|
|
546
|
+
TEXT : str
|
|
547
|
+
Text format, utf-8 encoded.
|
|
498
548
|
"""
|
|
499
549
|
|
|
500
550
|
JSON = "json"
|
|
@@ -503,6 +553,8 @@ class OutputFormat(str, Enum):
|
|
|
503
553
|
"""CSV archive format: multiple CSV files."""
|
|
504
554
|
MULTI_FILE = "multi-file"
|
|
505
555
|
"""Multi-file format: multiple files in a directory."""
|
|
556
|
+
TEXT = "text"
|
|
557
|
+
"""Text format, utf-8 encoded."""
|
|
506
558
|
|
|
507
559
|
|
|
508
560
|
@dataclass
|
|
@@ -1072,8 +1124,8 @@ class Output:
|
|
|
1072
1124
|
output_dict = {
|
|
1073
1125
|
"options": options,
|
|
1074
1126
|
"solution": self.solution if self.solution is not None else {},
|
|
1075
|
-
|
|
1076
|
-
|
|
1127
|
+
STATISTICS_KEY: statistics,
|
|
1128
|
+
ASSETS_KEY: assets,
|
|
1077
1129
|
}
|
|
1078
1130
|
|
|
1079
1131
|
# Add the auxiliary configurations to the output dictionary if they are
|
|
@@ -1232,8 +1284,8 @@ class LocalOutputWriter(OutputWriter):
|
|
|
1232
1284
|
serialized = serialize_json(
|
|
1233
1285
|
{
|
|
1234
1286
|
"options": output_dict.get("options", {}),
|
|
1235
|
-
|
|
1236
|
-
|
|
1287
|
+
STATISTICS_KEY: output_dict.get(STATISTICS_KEY, {}),
|
|
1288
|
+
ASSETS_KEY: output_dict.get(ASSETS_KEY, []),
|
|
1237
1289
|
},
|
|
1238
1290
|
json_configurations=json_configurations,
|
|
1239
1291
|
)
|
|
@@ -1281,7 +1333,7 @@ class LocalOutputWriter(OutputWriter):
|
|
|
1281
1333
|
ValueError
|
|
1282
1334
|
If the path is an existing file instead of a directory.
|
|
1283
1335
|
"""
|
|
1284
|
-
dir_path =
|
|
1336
|
+
dir_path = OUTPUTS_KEY
|
|
1285
1337
|
if path is not None and path != "":
|
|
1286
1338
|
if os.path.isfile(path):
|
|
1287
1339
|
raise ValueError(f"The path refers to an existing file: {path}")
|
|
@@ -1299,13 +1351,13 @@ class LocalOutputWriter(OutputWriter):
|
|
|
1299
1351
|
parent_dir=dir_path,
|
|
1300
1352
|
json_configurations=json_configurations,
|
|
1301
1353
|
output_dict=output_dict,
|
|
1302
|
-
element_key=
|
|
1354
|
+
element_key=STATISTICS_KEY,
|
|
1303
1355
|
)
|
|
1304
1356
|
self._write_multi_file_element(
|
|
1305
1357
|
parent_dir=dir_path,
|
|
1306
1358
|
json_configurations=json_configurations,
|
|
1307
1359
|
output_dict=output_dict,
|
|
1308
|
-
element_key=
|
|
1360
|
+
element_key=ASSETS_KEY,
|
|
1309
1361
|
)
|
|
1310
1362
|
self._write_multi_file_solution(dir_path=dir_path, output=output)
|
|
1311
1363
|
|
|
@@ -1350,7 +1402,7 @@ class LocalOutputWriter(OutputWriter):
|
|
|
1350
1402
|
if output.solution_files is None:
|
|
1351
1403
|
return
|
|
1352
1404
|
|
|
1353
|
-
solutions_dir = os.path.join(dir_path,
|
|
1405
|
+
solutions_dir = os.path.join(dir_path, SOLUTIONS_KEY)
|
|
1354
1406
|
|
|
1355
1407
|
if not os.path.exists(solutions_dir):
|
|
1356
1408
|
os.makedirs(solutions_dir)
|
|
@@ -1381,6 +1433,7 @@ class LocalOutputWriter(OutputWriter):
|
|
|
1381
1433
|
OutputFormat.JSON: _write_json,
|
|
1382
1434
|
OutputFormat.CSV_ARCHIVE: _write_archive,
|
|
1383
1435
|
OutputFormat.MULTI_FILE: _write_multi_file,
|
|
1436
|
+
OutputFormat.TEXT: _write_json,
|
|
1384
1437
|
}
|
|
1385
1438
|
"""Dictionary mapping output formats to writer functions."""
|
|
1386
1439
|
|
nextmv/polling.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Polling module containing logic to poll for a run result.
|
|
3
|
+
|
|
4
|
+
Polling can be used with both Cloud and local applications.
|
|
5
|
+
|
|
6
|
+
Classes
|
|
7
|
+
-------
|
|
8
|
+
PollingOptions
|
|
9
|
+
Options to use when polling for a run result.
|
|
10
|
+
|
|
11
|
+
Functions
|
|
12
|
+
---------
|
|
13
|
+
poll
|
|
14
|
+
Function to poll a function until it succeeds or the polling strategy is
|
|
15
|
+
exhausted.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import random
|
|
19
|
+
import time
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any, Optional
|
|
23
|
+
|
|
24
|
+
from nextmv.logger import log
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class PollingOptions:
|
|
29
|
+
"""
|
|
30
|
+
Options to use when polling for a run result.
|
|
31
|
+
|
|
32
|
+
You can import the `PollingOptions` class directly from `nextmv`:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from nextmv import PollingOptions
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The Cloud API will be polled for the result. The polling stops if:
|
|
39
|
+
|
|
40
|
+
* The maximum number of polls (tries) are exhausted. This is specified by
|
|
41
|
+
the `max_tries` parameter.
|
|
42
|
+
* The maximum duration of the polling strategy is reached. This is
|
|
43
|
+
specified by the `max_duration` parameter.
|
|
44
|
+
|
|
45
|
+
Before conducting the first poll, the `initial_delay` is used to sleep.
|
|
46
|
+
After each poll, a sleep duration is calculated using the following
|
|
47
|
+
strategy, based on exponential backoff with jitter:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
sleep_duration = min(`max_delay`, `delay` + `backoff` * 2 ** i + Uniform(0, `jitter`))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Where:
|
|
54
|
+
* i is the retry (poll) number.
|
|
55
|
+
* Uniform is the uniform distribution.
|
|
56
|
+
|
|
57
|
+
Note that the sleep duration is capped by the `max_delay` parameter.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
backoff : float, default=0.9
|
|
62
|
+
Exponential backoff factor, in seconds, to use between polls.
|
|
63
|
+
delay : float, default=0.1
|
|
64
|
+
Base delay to use between polls, in seconds.
|
|
65
|
+
initial_delay : float, default=1.0
|
|
66
|
+
Initial delay to use before starting the polling strategy, in seconds.
|
|
67
|
+
max_delay : float, default=20.0
|
|
68
|
+
Maximum delay to use between polls, in seconds.
|
|
69
|
+
max_duration : float, default=300.0
|
|
70
|
+
Maximum duration of the polling strategy, in seconds.
|
|
71
|
+
max_tries : int, default=100
|
|
72
|
+
Maximum number of tries to use.
|
|
73
|
+
jitter : float, default=1.0
|
|
74
|
+
Jitter to use for the polling strategy. A uniform distribution is sampled
|
|
75
|
+
between 0 and this number. The resulting random number is added to the
|
|
76
|
+
delay for each poll, adding a random noise. Set this to 0 to avoid using
|
|
77
|
+
random jitter.
|
|
78
|
+
verbose : bool, default=False
|
|
79
|
+
Whether to log the polling strategy. This is useful for debugging.
|
|
80
|
+
stop : callable, default=None
|
|
81
|
+
Function to call to check if the polling should stop. This is useful for
|
|
82
|
+
stopping the polling based on external conditions. The function should
|
|
83
|
+
return True to stop the polling and False to continue. The function does
|
|
84
|
+
not receive any arguments. The function is called before each poll.
|
|
85
|
+
|
|
86
|
+
Examples
|
|
87
|
+
--------
|
|
88
|
+
>>> from nextmv.cloud import PollingOptions
|
|
89
|
+
>>> # Create polling options with custom settings
|
|
90
|
+
>>> polling_options = PollingOptions(
|
|
91
|
+
... max_tries=50,
|
|
92
|
+
... max_duration=600,
|
|
93
|
+
... verbose=True
|
|
94
|
+
... )
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
backoff: float = 0.9
|
|
98
|
+
"""
|
|
99
|
+
Exponential backoff factor, in seconds, to use between polls.
|
|
100
|
+
"""
|
|
101
|
+
delay: float = 0.1
|
|
102
|
+
"""Base delay to use between polls, in seconds."""
|
|
103
|
+
initial_delay: float = 1
|
|
104
|
+
"""
|
|
105
|
+
Initial delay to use before starting the polling strategy, in seconds.
|
|
106
|
+
"""
|
|
107
|
+
max_delay: float = 20
|
|
108
|
+
"""Maximum delay to use between polls, in seconds."""
|
|
109
|
+
max_duration: float = -1
|
|
110
|
+
"""
|
|
111
|
+
Maximum duration of the polling strategy, in seconds. A negative value means no limit.
|
|
112
|
+
"""
|
|
113
|
+
max_tries: int = -1
|
|
114
|
+
"""Maximum number of tries to use. A negative value means no limit."""
|
|
115
|
+
jitter: float = 1
|
|
116
|
+
"""
|
|
117
|
+
Jitter to use for the polling strategy. A uniform distribution is sampled
|
|
118
|
+
between 0 and this number. The resulting random number is added to the
|
|
119
|
+
delay for each poll, adding a random noise. Set this to 0 to avoid using
|
|
120
|
+
random jitter.
|
|
121
|
+
"""
|
|
122
|
+
verbose: bool = False
|
|
123
|
+
"""Whether to log the polling strategy. This is useful for debugging."""
|
|
124
|
+
stop: Optional[Callable[[], bool]] = None
|
|
125
|
+
"""
|
|
126
|
+
Function to call to check if the polling should stop. This is useful for
|
|
127
|
+
stopping the polling based on external conditions. The function should
|
|
128
|
+
return True to stop the polling and False to continue. The function does
|
|
129
|
+
not receive any arguments. The function is called before each poll.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
DEFAULT_POLLING_OPTIONS: PollingOptions = PollingOptions()
|
|
134
|
+
"""
|
|
135
|
+
Default polling options to use when polling for a run result. This constant
|
|
136
|
+
provides the default values for `PollingOptions` used across the module.
|
|
137
|
+
Using these defaults is recommended for most use cases unless specific timing
|
|
138
|
+
needs are required.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def poll( # noqa: C901
|
|
143
|
+
polling_options: PollingOptions,
|
|
144
|
+
polling_func: Callable[[], tuple[Any, bool]],
|
|
145
|
+
__sleep_func: Callable[[float], None] = time.sleep,
|
|
146
|
+
) -> Any:
|
|
147
|
+
"""
|
|
148
|
+
Poll a function until it succeeds or the polling strategy is exhausted.
|
|
149
|
+
|
|
150
|
+
You can import the `poll` function directly from `nextmv`:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from nextmv import poll
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
This function implements a flexible polling strategy with exponential backoff
|
|
157
|
+
and jitter. It calls the provided polling function repeatedly until it indicates
|
|
158
|
+
success, the maximum number of tries is reached, or the maximum duration is exceeded.
|
|
159
|
+
|
|
160
|
+
The `polling_func` is a callable that must return a `tuple[Any, bool]`
|
|
161
|
+
where the first element is the result of the polling and the second
|
|
162
|
+
element is a boolean indicating if the polling was successful or should be
|
|
163
|
+
retried.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
polling_options : PollingOptions
|
|
168
|
+
Options for configuring the polling behavior, including retry counts,
|
|
169
|
+
delays, timeouts, and verbosity settings.
|
|
170
|
+
polling_func : callable
|
|
171
|
+
Function to call to check if the polling was successful. Must return a tuple
|
|
172
|
+
where the first element is the result value and the second is a boolean
|
|
173
|
+
indicating success (True) or need to retry (False).
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
Any
|
|
178
|
+
Result value from the polling function when successful.
|
|
179
|
+
|
|
180
|
+
Raises
|
|
181
|
+
------
|
|
182
|
+
TimeoutError
|
|
183
|
+
If the polling exceeds the maximum duration specified in polling_options.
|
|
184
|
+
RuntimeError
|
|
185
|
+
If the maximum number of tries is exhausted without success.
|
|
186
|
+
|
|
187
|
+
Examples
|
|
188
|
+
--------
|
|
189
|
+
>>> from nextmv.cloud import PollingOptions, poll
|
|
190
|
+
>>> import time
|
|
191
|
+
>>>
|
|
192
|
+
>>> # Define a polling function that succeeds after 3 tries
|
|
193
|
+
>>> counter = 0
|
|
194
|
+
>>> def check_completion() -> tuple[str, bool]:
|
|
195
|
+
... global counter
|
|
196
|
+
... counter += 1
|
|
197
|
+
... if counter >= 3:
|
|
198
|
+
... return "Success", True
|
|
199
|
+
... return None, False
|
|
200
|
+
...
|
|
201
|
+
>>> # Configure polling options
|
|
202
|
+
>>> options = PollingOptions(
|
|
203
|
+
... max_tries=5,
|
|
204
|
+
... delay=0.1,
|
|
205
|
+
... backoff=0.2,
|
|
206
|
+
... verbose=True
|
|
207
|
+
... )
|
|
208
|
+
>>>
|
|
209
|
+
>>> # Poll until the function succeeds
|
|
210
|
+
>>> result = poll(options, check_completion)
|
|
211
|
+
>>> print(result)
|
|
212
|
+
'Success'
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
# Start by sleeping for the duration specified as initial delay.
|
|
216
|
+
if polling_options.verbose:
|
|
217
|
+
log(f"polling | sleeping for initial delay: {polling_options.initial_delay}")
|
|
218
|
+
|
|
219
|
+
__sleep_func(polling_options.initial_delay)
|
|
220
|
+
|
|
221
|
+
start_time = time.time()
|
|
222
|
+
stopped = False
|
|
223
|
+
|
|
224
|
+
# Begin the polling process.
|
|
225
|
+
max_reached = False
|
|
226
|
+
ix = 0
|
|
227
|
+
while True:
|
|
228
|
+
# Check if we reached the maximum number of tries. Break if so.
|
|
229
|
+
if ix >= polling_options.max_tries and polling_options.max_tries >= 0:
|
|
230
|
+
break
|
|
231
|
+
ix += 1
|
|
232
|
+
|
|
233
|
+
# Check is we should stop polling according to the stop callback.
|
|
234
|
+
if polling_options.stop is not None and polling_options.stop():
|
|
235
|
+
stopped = True
|
|
236
|
+
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
# We check if we can stop polling.
|
|
240
|
+
result, ok = polling_func()
|
|
241
|
+
if polling_options.verbose:
|
|
242
|
+
log(f"polling | try # {ix + 1}, ok: {ok}")
|
|
243
|
+
|
|
244
|
+
if ok:
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
# An exit condition happens if we exceed the allowed duration.
|
|
248
|
+
passed = time.time() - start_time
|
|
249
|
+
if polling_options.verbose:
|
|
250
|
+
log(f"polling | elapsed time: {passed}")
|
|
251
|
+
|
|
252
|
+
if passed >= polling_options.max_duration and polling_options.max_duration >= 0:
|
|
253
|
+
raise TimeoutError(
|
|
254
|
+
f"polling did not succeed after {passed} seconds, exceeds max duration: {polling_options.max_duration}",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Calculate the delay.
|
|
258
|
+
if max_reached:
|
|
259
|
+
# If we already reached the maximum, we don't want to further calculate the
|
|
260
|
+
# delay to avoid overflows.
|
|
261
|
+
delay = polling_options.max_delay
|
|
262
|
+
delay += random.uniform(0, polling_options.jitter) # Add jitter.
|
|
263
|
+
else:
|
|
264
|
+
delay = polling_options.delay # Base
|
|
265
|
+
delay += polling_options.backoff * (2**ix) # Add exponential backoff.
|
|
266
|
+
delay += random.uniform(0, polling_options.jitter) # Add jitter.
|
|
267
|
+
|
|
268
|
+
# We cannot exceed the max delay.
|
|
269
|
+
if delay >= polling_options.max_delay:
|
|
270
|
+
max_reached = True
|
|
271
|
+
delay = polling_options.max_delay
|
|
272
|
+
|
|
273
|
+
# Sleep for the calculated delay.
|
|
274
|
+
sleep_duration = delay
|
|
275
|
+
if polling_options.verbose:
|
|
276
|
+
log(f"polling | sleeping for duration: {sleep_duration}")
|
|
277
|
+
|
|
278
|
+
__sleep_func(sleep_duration)
|
|
279
|
+
|
|
280
|
+
if stopped:
|
|
281
|
+
log("polling | stop condition met, stopping polling")
|
|
282
|
+
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
raise RuntimeError(
|
|
286
|
+
f"polling did not succeed after {polling_options.max_tries} tries",
|
|
287
|
+
)
|