nextmv 0.18.2.dev0__tar.gz → 0.19.0.dev0__tar.gz
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-0.18.2.dev0 → nextmv-0.19.0.dev0}/PKG-INFO +1 -1
- nextmv-0.19.0.dev0/nextmv/__about__.py +1 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/application.py +146 -53
- nextmv-0.19.0.dev0/tests/cloud/test_application.py +23 -0
- nextmv-0.18.2.dev0/nextmv/__about__.py +0 -1
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/.gitignore +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/LICENSE +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/README.md +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/__entrypoint__.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/__init__.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/base_model.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/__init__.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/acceptance_test.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/account.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/batch_experiment.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/client.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/input_set.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/instance.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/manifest.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/package.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/run.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/secrets.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/status.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/cloud/version.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/input.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/logger.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/model.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/options.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/nextmv/output.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/pyproject.toml +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/requirements.txt +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/__init__.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/cloud/__init__.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/cloud/app.yaml +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/cloud/test_client.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/cloud/test_manifest.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/cloud/test_package.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/scripts/__init__.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/scripts/options1.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/scripts/options2.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/scripts/options3.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/scripts/options4.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/scripts/options5.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/scripts/options6.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/scripts/options7.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/test_base_model.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/test_entrypoint/__init__.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/test_entrypoint/test_entrypoint.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/test_input.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/test_logger.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/test_model.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/test_options.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/test_output.py +0 -0
- {nextmv-0.18.2.dev0 → nextmv-0.19.0.dev0}/tests/test_version.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "v0.19.0.dev0"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""This module contains the application class."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import random
|
|
4
5
|
import shutil
|
|
5
6
|
import time
|
|
6
7
|
from dataclasses import dataclass
|
|
@@ -37,24 +38,56 @@ class DownloadURL(BaseModel):
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
class PollingOptions(BaseModel):
|
|
40
|
-
"""
|
|
41
|
+
"""
|
|
42
|
+
Options to use when polling for a run result.
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
The Cloud API will be polled for the result. The polling stops if:
|
|
45
|
+
|
|
46
|
+
* The maximum number of polls (tries) are exhausted. This is specified by
|
|
47
|
+
the `max_tries` parameter.
|
|
48
|
+
* The maximum duration of the polling strategy is reached. This is
|
|
49
|
+
specified by the `max_duration` parameter.
|
|
50
|
+
|
|
51
|
+
Before conducting the first poll, the `initial_delay` is used to sleep.
|
|
52
|
+
After each poll, a sleep duration is calculated using the following
|
|
53
|
+
strategy, based on exponential backoff with jitter:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
sleep_duration = min(`max_delay`, `delay` + `backoff` * 2 ** i + Uniform(0, `jitter`))
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Where:
|
|
60
|
+
* i is the retry (poll) number.
|
|
61
|
+
* Uniform is the uniform distribution.
|
|
62
|
+
|
|
63
|
+
Note that the sleep duration is capped by the `max_delay` parameter.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
backoff: float = 0.9
|
|
67
|
+
"""
|
|
68
|
+
Exponential backoff factor, in seconds, to use between polls.
|
|
69
|
+
"""
|
|
70
|
+
delay: float = 0.1
|
|
71
|
+
"""Base delay to use between polls, in seconds."""
|
|
47
72
|
initial_delay: float = 1
|
|
48
|
-
"""
|
|
49
|
-
seconds.
|
|
73
|
+
"""
|
|
74
|
+
Initial delay to use before starting the polling strategy, in seconds.
|
|
75
|
+
"""
|
|
50
76
|
max_delay: float = 20
|
|
51
|
-
"""Maximum delay to use between polls, in seconds.
|
|
52
|
-
activated when the backoff parameter is greater than 1, such that the delay
|
|
53
|
-
is increasing after each poll."""
|
|
77
|
+
"""Maximum delay to use between polls, in seconds."""
|
|
54
78
|
max_duration: float = 300
|
|
55
79
|
"""Maximum duration of the polling strategy, in seconds."""
|
|
56
|
-
max_tries: int =
|
|
80
|
+
max_tries: int = 100
|
|
57
81
|
"""Maximum number of tries to use."""
|
|
82
|
+
jitter: float = 1
|
|
83
|
+
"""
|
|
84
|
+
Jitter to use for the polling strategy. A uniform distribution is sampled
|
|
85
|
+
between 0 and this number. The resulting random number is added to the
|
|
86
|
+
delay for each poll, adding a random noise. Set this to 0 to avoid using
|
|
87
|
+
random jitter.
|
|
88
|
+
"""
|
|
89
|
+
verbose: bool = False
|
|
90
|
+
"""Whether to log the polling strategy. This is useful for debugging."""
|
|
58
91
|
|
|
59
92
|
|
|
60
93
|
_DEFAULT_POLLING_OPTIONS: PollingOptions = PollingOptions()
|
|
@@ -573,32 +606,18 @@ class Application:
|
|
|
573
606
|
description=description,
|
|
574
607
|
)
|
|
575
608
|
|
|
576
|
-
|
|
577
|
-
delay = polling_options.delay
|
|
578
|
-
polling_ok = False
|
|
579
|
-
for _ in range(polling_options.max_tries):
|
|
609
|
+
def polling_func() -> tuple[AcceptanceTest, bool]:
|
|
580
610
|
test_information = self.acceptance_test(acceptance_test_id=id)
|
|
581
611
|
if test_information.status in [
|
|
582
612
|
ExperimentStatus.completed,
|
|
583
613
|
ExperimentStatus.failed,
|
|
584
614
|
ExperimentStatus.canceled,
|
|
585
615
|
]:
|
|
586
|
-
|
|
587
|
-
break
|
|
588
|
-
|
|
589
|
-
if delay > polling_options.max_duration:
|
|
590
|
-
raise TimeoutError(
|
|
591
|
-
f"acceptance_test {id} did not succeed after {delay} seconds",
|
|
592
|
-
)
|
|
616
|
+
return test_information, True
|
|
593
617
|
|
|
594
|
-
|
|
595
|
-
time.sleep(sleep_duration)
|
|
596
|
-
delay *= polling_options.backoff
|
|
618
|
+
return None, False
|
|
597
619
|
|
|
598
|
-
|
|
599
|
-
raise RuntimeError(
|
|
600
|
-
f"acceptance_test {id} did not succeed after {polling_options.max_tries} tries",
|
|
601
|
-
)
|
|
620
|
+
test_information = poll(polling_options=polling_options, polling_func=polling_func)
|
|
602
621
|
|
|
603
622
|
return test_information
|
|
604
623
|
|
|
@@ -785,6 +804,10 @@ class Application:
|
|
|
785
804
|
upload_id: ID to use when running a large input.
|
|
786
805
|
options: Options to use for the run.
|
|
787
806
|
configuration: Configuration to use for the run.
|
|
807
|
+
batch_experiment_id: ID of a batch experiment to associate the run
|
|
808
|
+
with.
|
|
809
|
+
external_result: External result to use for the run, if this is an
|
|
810
|
+
external run.
|
|
788
811
|
|
|
789
812
|
Returns:
|
|
790
813
|
ID of the submitted run.
|
|
@@ -875,6 +898,10 @@ class Application:
|
|
|
875
898
|
run_options: Options to use for the run.
|
|
876
899
|
polling_options: Options to use when polling for the run result.
|
|
877
900
|
configuration: Configuration to use for the run.
|
|
901
|
+
batch_experimemt_id: ID of a batch experiment to associate the run
|
|
902
|
+
with.
|
|
903
|
+
external_result: External result to use for the run, if this is an
|
|
904
|
+
external run
|
|
878
905
|
|
|
879
906
|
Returns:
|
|
880
907
|
Result of the run.
|
|
@@ -912,14 +939,23 @@ class Application:
|
|
|
912
939
|
description: Optional[str] = None,
|
|
913
940
|
) -> SecretsCollectionSummary:
|
|
914
941
|
"""
|
|
915
|
-
Create a new secrets collection.
|
|
942
|
+
Create a new secrets collection. If no secrets are provided, a
|
|
943
|
+
ValueError is raised.
|
|
944
|
+
|
|
945
|
+
Args:
|
|
946
|
+
secrets: List of secrets to use for the secrets collection. id: ID
|
|
947
|
+
of the secrets collection. Will be generated if not provided.
|
|
948
|
+
name: Name of the secrets collection. Will be generated if not
|
|
949
|
+
provided.
|
|
950
|
+
description: Description of the secrets collection. Will be
|
|
951
|
+
generated if not provided.
|
|
916
952
|
|
|
917
953
|
Returns:
|
|
918
954
|
SecretsCollectionSummary: Summary of the secrets collection.
|
|
919
955
|
|
|
920
956
|
Raises:
|
|
921
|
-
ValueError: If no secrets are provided.
|
|
922
|
-
|
|
957
|
+
ValueError: If no secrets are provided. requests.HTTPError: If the
|
|
958
|
+
response status code is not 2xx.
|
|
923
959
|
"""
|
|
924
960
|
|
|
925
961
|
if len(secrets) == 0:
|
|
@@ -1233,32 +1269,18 @@ class Application:
|
|
|
1233
1269
|
requests.HTTPError: If the response status code is not 2xx.
|
|
1234
1270
|
"""
|
|
1235
1271
|
|
|
1236
|
-
|
|
1237
|
-
delay = polling_options.delay
|
|
1238
|
-
polling_ok = False
|
|
1239
|
-
for _ in range(polling_options.max_tries):
|
|
1272
|
+
def polling_func() -> tuple[any, bool]:
|
|
1240
1273
|
run_information = self.run_metadata(run_id=run_id)
|
|
1241
|
-
if run_information.metadata.status_v2 in
|
|
1274
|
+
if run_information.metadata.status_v2 in {
|
|
1242
1275
|
StatusV2.succeeded,
|
|
1243
1276
|
StatusV2.failed,
|
|
1244
1277
|
StatusV2.canceled,
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
break
|
|
1278
|
+
}:
|
|
1279
|
+
return run_information, True
|
|
1248
1280
|
|
|
1249
|
-
|
|
1250
|
-
raise TimeoutError(
|
|
1251
|
-
f"run {run_id} did not succeed after {delay} seconds",
|
|
1252
|
-
)
|
|
1253
|
-
|
|
1254
|
-
sleep_duration = min(delay, polling_options.max_delay)
|
|
1255
|
-
time.sleep(sleep_duration)
|
|
1256
|
-
delay *= polling_options.backoff
|
|
1281
|
+
return None, False
|
|
1257
1282
|
|
|
1258
|
-
|
|
1259
|
-
raise RuntimeError(
|
|
1260
|
-
f"run {run_id} did not succeed after {polling_options.max_tries} tries",
|
|
1261
|
-
)
|
|
1283
|
+
run_information = poll(polling_options=polling_options, polling_func=polling_func)
|
|
1262
1284
|
|
|
1263
1285
|
return self.__run_result(run_id=run_id, run_information=run_information)
|
|
1264
1286
|
|
|
@@ -1525,3 +1547,74 @@ class Application:
|
|
|
1525
1547
|
indent=2,
|
|
1526
1548
|
)
|
|
1527
1549
|
)
|
|
1550
|
+
|
|
1551
|
+
|
|
1552
|
+
def poll(polling_options: PollingOptions, polling_func: callable) -> any:
|
|
1553
|
+
"""
|
|
1554
|
+
Auxiliary function for polling.
|
|
1555
|
+
|
|
1556
|
+
The `polling_func` is a callable that must return a `tuple[any, bool]`
|
|
1557
|
+
where the first element is the result of the polling and the second
|
|
1558
|
+
element is a boolean indicating if the polling was successful or should be
|
|
1559
|
+
retried.
|
|
1560
|
+
|
|
1561
|
+
This function will return the result of the `polling_func` if the polling
|
|
1562
|
+
process is successful, otherwise it will raise a `TimeoutError` or
|
|
1563
|
+
`RuntimeError` depending on the situation.
|
|
1564
|
+
|
|
1565
|
+
Parameters
|
|
1566
|
+
----------
|
|
1567
|
+
polling_options : PollingOptions
|
|
1568
|
+
Options for the polling process.
|
|
1569
|
+
polling_func : callable
|
|
1570
|
+
Function to call to check if the polling was successful.
|
|
1571
|
+
|
|
1572
|
+
Returns
|
|
1573
|
+
-------
|
|
1574
|
+
any
|
|
1575
|
+
Result of the polling function.
|
|
1576
|
+
"""
|
|
1577
|
+
|
|
1578
|
+
# Start by sleeping for the duration specified as initial delay.
|
|
1579
|
+
if polling_options.verbose:
|
|
1580
|
+
log(f"polling | sleeping for initial delay: {polling_options.initial_delay}")
|
|
1581
|
+
|
|
1582
|
+
time.sleep(polling_options.initial_delay)
|
|
1583
|
+
|
|
1584
|
+
start_time = time.time()
|
|
1585
|
+
|
|
1586
|
+
# Begin the polling process.
|
|
1587
|
+
for ix in range(polling_options.max_tries):
|
|
1588
|
+
# We check if we can stop polling.
|
|
1589
|
+
result, ok = polling_func()
|
|
1590
|
+
if polling_options.verbose:
|
|
1591
|
+
log(f"polling | try # {ix + 1}, ok: {ok}")
|
|
1592
|
+
|
|
1593
|
+
if ok:
|
|
1594
|
+
return result
|
|
1595
|
+
|
|
1596
|
+
# An exit condition happens if we exceed the allowed duration.
|
|
1597
|
+
passed = time.time() - start_time
|
|
1598
|
+
if polling_options.verbose:
|
|
1599
|
+
log(f"polling | elapsed time: {passed}")
|
|
1600
|
+
|
|
1601
|
+
if passed >= polling_options.max_duration:
|
|
1602
|
+
raise TimeoutError(
|
|
1603
|
+
f"polling did not succeed after {passed} seconds, exceeds max duration: {polling_options.max_duration}",
|
|
1604
|
+
)
|
|
1605
|
+
|
|
1606
|
+
# Calculate the delay.
|
|
1607
|
+
delay = polling_options.delay # Base
|
|
1608
|
+
delay += polling_options.backoff * (2**ix) # Add exponential backoff.
|
|
1609
|
+
delay += random.uniform(0, polling_options.jitter) # Add jitter.
|
|
1610
|
+
|
|
1611
|
+
# Sleep for the calculated delay. We cannot exceed the max delay.
|
|
1612
|
+
sleep_duration = min(delay, polling_options.max_delay)
|
|
1613
|
+
if polling_options.verbose:
|
|
1614
|
+
log(f"polling | sleeping for duration: {sleep_duration}")
|
|
1615
|
+
|
|
1616
|
+
time.sleep(sleep_duration)
|
|
1617
|
+
|
|
1618
|
+
raise RuntimeError(
|
|
1619
|
+
f"polling did not succeed after {polling_options.max_tries} tries",
|
|
1620
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from nextmv.cloud.application import PollingOptions, poll
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestApplication(unittest.TestCase):
|
|
7
|
+
def test_poll(self):
|
|
8
|
+
counter = 0
|
|
9
|
+
|
|
10
|
+
def polling_func() -> tuple[any, bool]:
|
|
11
|
+
nonlocal counter
|
|
12
|
+
counter += 1
|
|
13
|
+
|
|
14
|
+
if counter < 4:
|
|
15
|
+
return "result", False
|
|
16
|
+
|
|
17
|
+
return "result", True
|
|
18
|
+
|
|
19
|
+
polling_options = PollingOptions(verbose=True)
|
|
20
|
+
|
|
21
|
+
result = poll(polling_options, polling_func)
|
|
22
|
+
|
|
23
|
+
self.assertEqual(result, "result")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "v0.18.2.dev0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|