nextmv 0.18.2.dev0__py3-none-any.whl → 0.19.0.dev0__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "v0.18.2.dev0"
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
- """Options to use when polling for a run result."""
41
+ """
42
+ Options to use when polling for a run result.
41
43
 
42
- backoff: float = 1
43
- """Backoff factor to use between polls. Leave this at 1 to poll at a
44
- constant rate."""
45
- delay: float = 1
46
- """Delay to use between polls, in seconds."""
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
- """Initial delay to use before starting the polling strategy, in
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. This parameter is
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 = 20
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
- time.sleep(polling_options.initial_delay)
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
- polling_ok = True
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
- sleep_duration = min(delay, polling_options.max_delay)
595
- time.sleep(sleep_duration)
596
- delay *= polling_options.backoff
618
+ return None, False
597
619
 
598
- if not polling_ok:
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
- requests.HTTPError: If the response status code is not 2xx.
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
- time.sleep(polling_options.initial_delay)
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
- polling_ok = True
1247
- break
1278
+ }:
1279
+ return run_information, True
1248
1280
 
1249
- if delay > polling_options.max_duration:
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
- if not polling_ok:
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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextmv
3
- Version: 0.18.2.dev0
3
+ Version: 0.19.0.dev0
4
4
  Summary: The all-purpose Python SDK for Nextmv
5
5
  Project-URL: Homepage, https://www.nextmv.io
6
6
  Project-URL: Documentation, https://www.nextmv.io/docs/python-sdks/nextmv/installation
@@ -1,4 +1,4 @@
1
- nextmv/__about__.py,sha256=dTLG-gGXb4zJkUa2P9Q65xpjXlUegJnA_vKyCCBpXpk,29
1
+ nextmv/__about__.py,sha256=L9niS75ngWJodUjl2bPjdLJssQdgs8hxUR8gldU5otE,29
2
2
  nextmv/__entrypoint__.py,sha256=o6xYGBBUgCY_htteW-qNJfp-S3Th8dzS4RreJcDCnzM,1333
3
3
  nextmv/__init__.py,sha256=rz9oP7JGyFQJdUtYX4j9xx4Gv4LMGfNvXoEID5qUqig,1354
4
4
  nextmv/base_model.py,sha256=mdaBe-epNK1cFgP4TxbOtn3So4pCi1vMTOrIBkCBp7A,1050
@@ -10,7 +10,7 @@ nextmv/output.py,sha256=YgRs9I7SEhpKzJPALfy09ew9wdVRX-uhwBsVsnB0bL4,19276
10
10
  nextmv/cloud/__init__.py,sha256=inttQhr3SEibNebMDNJzfVrvOLc4_fAR6zmwabjb7QQ,3209
11
11
  nextmv/cloud/acceptance_test.py,sha256=NtqGhj-UYibxGBbU2kfjr-lYcngojb_5VMvK2WZwibI,6620
12
12
  nextmv/cloud/account.py,sha256=mZUGzV-uMGBA5BC_FPtsiCMFuz5jxEZ3O1BbELZIm18,1841
13
- nextmv/cloud/application.py,sha256=1Dwf971RfY1R5h4XNnvXDtNvAW4G4BFGft1QwKm8eG0,49041
13
+ nextmv/cloud/application.py,sha256=c9YmE1mtr0ST9xWQ_vSyxKmJya8a9IGNSUxpz_K2PL4,52400
14
14
  nextmv/cloud/batch_experiment.py,sha256=UxrMNAm2c7-RZ9TWRhZBtiVyEeUG1pjsLNhYkoT5Jog,2236
15
15
  nextmv/cloud/client.py,sha256=F9P49f7eIbbmiGQxIuz8UxnZRN1Nb34awskMRliUMvw,9034
16
16
  nextmv/cloud/input_set.py,sha256=ovkP17-jYs0yWrbqTM6Nl5ubWQabD_UrDqAHNo8aE2s,672
@@ -21,7 +21,7 @@ nextmv/cloud/run.py,sha256=93o5SJMvN2b-Qd2ZGoXuelAHafKsGRt-DyOtE4srTsQ,4672
21
21
  nextmv/cloud/secrets.py,sha256=kqlN4ceww_L4kVTrAU8BZykRzXINO3zhMT_BLYea6tk,1764
22
22
  nextmv/cloud/status.py,sha256=C-ax8cLw0jPeh7CPsJkCa0s4ImRyFI4NDJJxI0_1sr4,602
23
23
  nextmv/cloud/version.py,sha256=sjVRNRtohHA97j6IuyM33_DSSsXYkZPusYgpb6hlcrc,1244
24
- nextmv-0.18.2.dev0.dist-info/METADATA,sha256=FKlU1MCoOWcF_1A0ovQyAa59pNc8McdiEJr2s1TQ3tc,14562
25
- nextmv-0.18.2.dev0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
- nextmv-0.18.2.dev0.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
27
- nextmv-0.18.2.dev0.dist-info/RECORD,,
24
+ nextmv-0.19.0.dev0.dist-info/METADATA,sha256=povkE3goU4oMaOLgVgCpa0shQhodvTsK0spsPJhJhYA,14562
25
+ nextmv-0.19.0.dev0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
+ nextmv-0.19.0.dev0.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
27
+ nextmv-0.19.0.dev0.dist-info/RECORD,,