nextmv 0.18.0__py3-none-any.whl → 1.0.0.dev2__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.
Files changed (175) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +8 -13
  3. nextmv/__init__.py +53 -0
  4. nextmv/_serialization.py +96 -0
  5. nextmv/base_model.py +54 -9
  6. nextmv/cli/CONTRIBUTING.md +511 -0
  7. nextmv/cli/__init__.py +0 -0
  8. nextmv/cli/cloud/__init__.py +47 -0
  9. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  10. nextmv/cli/cloud/acceptance/create.py +393 -0
  11. nextmv/cli/cloud/acceptance/delete.py +68 -0
  12. nextmv/cli/cloud/acceptance/get.py +104 -0
  13. nextmv/cli/cloud/acceptance/list.py +62 -0
  14. nextmv/cli/cloud/acceptance/update.py +95 -0
  15. nextmv/cli/cloud/account/__init__.py +28 -0
  16. nextmv/cli/cloud/account/create.py +83 -0
  17. nextmv/cli/cloud/account/delete.py +60 -0
  18. nextmv/cli/cloud/account/get.py +66 -0
  19. nextmv/cli/cloud/account/update.py +70 -0
  20. nextmv/cli/cloud/app/__init__.py +35 -0
  21. nextmv/cli/cloud/app/create.py +141 -0
  22. nextmv/cli/cloud/app/delete.py +58 -0
  23. nextmv/cli/cloud/app/exists.py +44 -0
  24. nextmv/cli/cloud/app/get.py +66 -0
  25. nextmv/cli/cloud/app/list.py +61 -0
  26. nextmv/cli/cloud/app/push.py +137 -0
  27. nextmv/cli/cloud/app/update.py +124 -0
  28. nextmv/cli/cloud/batch/__init__.py +29 -0
  29. nextmv/cli/cloud/batch/create.py +454 -0
  30. nextmv/cli/cloud/batch/delete.py +68 -0
  31. nextmv/cli/cloud/batch/get.py +104 -0
  32. nextmv/cli/cloud/batch/list.py +63 -0
  33. nextmv/cli/cloud/batch/metadata.py +66 -0
  34. nextmv/cli/cloud/batch/update.py +95 -0
  35. nextmv/cli/cloud/data/__init__.py +26 -0
  36. nextmv/cli/cloud/data/upload.py +162 -0
  37. nextmv/cli/cloud/ensemble/__init__.py +31 -0
  38. nextmv/cli/cloud/ensemble/create.py +414 -0
  39. nextmv/cli/cloud/ensemble/delete.py +67 -0
  40. nextmv/cli/cloud/ensemble/get.py +65 -0
  41. nextmv/cli/cloud/ensemble/update.py +103 -0
  42. nextmv/cli/cloud/input_set/__init__.py +30 -0
  43. nextmv/cli/cloud/input_set/create.py +170 -0
  44. nextmv/cli/cloud/input_set/get.py +63 -0
  45. nextmv/cli/cloud/input_set/list.py +63 -0
  46. nextmv/cli/cloud/input_set/update.py +123 -0
  47. nextmv/cli/cloud/instance/__init__.py +35 -0
  48. nextmv/cli/cloud/instance/create.py +290 -0
  49. nextmv/cli/cloud/instance/delete.py +62 -0
  50. nextmv/cli/cloud/instance/exists.py +39 -0
  51. nextmv/cli/cloud/instance/get.py +62 -0
  52. nextmv/cli/cloud/instance/list.py +60 -0
  53. nextmv/cli/cloud/instance/update.py +216 -0
  54. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  55. nextmv/cli/cloud/managed_input/create.py +146 -0
  56. nextmv/cli/cloud/managed_input/delete.py +65 -0
  57. nextmv/cli/cloud/managed_input/get.py +63 -0
  58. nextmv/cli/cloud/managed_input/list.py +60 -0
  59. nextmv/cli/cloud/managed_input/update.py +97 -0
  60. nextmv/cli/cloud/run/__init__.py +37 -0
  61. nextmv/cli/cloud/run/cancel.py +37 -0
  62. nextmv/cli/cloud/run/create.py +530 -0
  63. nextmv/cli/cloud/run/get.py +199 -0
  64. nextmv/cli/cloud/run/input.py +86 -0
  65. nextmv/cli/cloud/run/list.py +80 -0
  66. nextmv/cli/cloud/run/logs.py +167 -0
  67. nextmv/cli/cloud/run/metadata.py +67 -0
  68. nextmv/cli/cloud/run/track.py +501 -0
  69. nextmv/cli/cloud/scenario/__init__.py +29 -0
  70. nextmv/cli/cloud/scenario/create.py +451 -0
  71. nextmv/cli/cloud/scenario/delete.py +65 -0
  72. nextmv/cli/cloud/scenario/get.py +102 -0
  73. nextmv/cli/cloud/scenario/list.py +63 -0
  74. nextmv/cli/cloud/scenario/metadata.py +67 -0
  75. nextmv/cli/cloud/scenario/update.py +93 -0
  76. nextmv/cli/cloud/secrets/__init__.py +33 -0
  77. nextmv/cli/cloud/secrets/create.py +206 -0
  78. nextmv/cli/cloud/secrets/delete.py +67 -0
  79. nextmv/cli/cloud/secrets/get.py +66 -0
  80. nextmv/cli/cloud/secrets/list.py +60 -0
  81. nextmv/cli/cloud/secrets/update.py +147 -0
  82. nextmv/cli/cloud/shadow/__init__.py +33 -0
  83. nextmv/cli/cloud/shadow/create.py +184 -0
  84. nextmv/cli/cloud/shadow/delete.py +68 -0
  85. nextmv/cli/cloud/shadow/get.py +61 -0
  86. nextmv/cli/cloud/shadow/list.py +63 -0
  87. nextmv/cli/cloud/shadow/metadata.py +66 -0
  88. nextmv/cli/cloud/shadow/start.py +43 -0
  89. nextmv/cli/cloud/shadow/stop.py +43 -0
  90. nextmv/cli/cloud/shadow/update.py +95 -0
  91. nextmv/cli/cloud/upload/__init__.py +22 -0
  92. nextmv/cli/cloud/upload/create.py +39 -0
  93. nextmv/cli/cloud/version/__init__.py +33 -0
  94. nextmv/cli/cloud/version/create.py +97 -0
  95. nextmv/cli/cloud/version/delete.py +62 -0
  96. nextmv/cli/cloud/version/exists.py +39 -0
  97. nextmv/cli/cloud/version/get.py +62 -0
  98. nextmv/cli/cloud/version/list.py +60 -0
  99. nextmv/cli/cloud/version/update.py +92 -0
  100. nextmv/cli/community/__init__.py +24 -0
  101. nextmv/cli/community/clone.py +270 -0
  102. nextmv/cli/community/list.py +265 -0
  103. nextmv/cli/configuration/__init__.py +23 -0
  104. nextmv/cli/configuration/config.py +195 -0
  105. nextmv/cli/configuration/create.py +94 -0
  106. nextmv/cli/configuration/delete.py +67 -0
  107. nextmv/cli/configuration/list.py +77 -0
  108. nextmv/cli/main.py +188 -0
  109. nextmv/cli/message.py +153 -0
  110. nextmv/cli/options.py +206 -0
  111. nextmv/cli/version.py +38 -0
  112. nextmv/cloud/__init__.py +71 -17
  113. nextmv/cloud/acceptance_test.py +757 -51
  114. nextmv/cloud/account.py +406 -17
  115. nextmv/cloud/application/__init__.py +957 -0
  116. nextmv/cloud/application/_acceptance.py +419 -0
  117. nextmv/cloud/application/_batch_scenario.py +860 -0
  118. nextmv/cloud/application/_ensemble.py +251 -0
  119. nextmv/cloud/application/_input_set.py +227 -0
  120. nextmv/cloud/application/_instance.py +289 -0
  121. nextmv/cloud/application/_managed_input.py +227 -0
  122. nextmv/cloud/application/_run.py +1393 -0
  123. nextmv/cloud/application/_secrets.py +294 -0
  124. nextmv/cloud/application/_shadow.py +314 -0
  125. nextmv/cloud/application/_utils.py +54 -0
  126. nextmv/cloud/application/_version.py +303 -0
  127. nextmv/cloud/assets.py +48 -0
  128. nextmv/cloud/batch_experiment.py +294 -33
  129. nextmv/cloud/client.py +307 -66
  130. nextmv/cloud/ensemble.py +247 -0
  131. nextmv/cloud/input_set.py +120 -2
  132. nextmv/cloud/instance.py +133 -8
  133. nextmv/cloud/integration.py +533 -0
  134. nextmv/cloud/package.py +168 -53
  135. nextmv/cloud/scenario.py +410 -0
  136. nextmv/cloud/secrets.py +234 -0
  137. nextmv/cloud/shadow.py +190 -0
  138. nextmv/cloud/url.py +73 -0
  139. nextmv/cloud/version.py +132 -4
  140. nextmv/default_app/.gitignore +1 -0
  141. nextmv/default_app/README.md +32 -0
  142. nextmv/default_app/app.yaml +12 -0
  143. nextmv/default_app/input.json +5 -0
  144. nextmv/default_app/main.py +37 -0
  145. nextmv/default_app/requirements.txt +2 -0
  146. nextmv/default_app/src/__init__.py +0 -0
  147. nextmv/default_app/src/visuals.py +36 -0
  148. nextmv/deprecated.py +47 -0
  149. nextmv/input.py +861 -90
  150. nextmv/local/__init__.py +5 -0
  151. nextmv/local/application.py +1251 -0
  152. nextmv/local/executor.py +1042 -0
  153. nextmv/local/geojson_handler.py +323 -0
  154. nextmv/local/local.py +97 -0
  155. nextmv/local/plotly_handler.py +61 -0
  156. nextmv/local/runner.py +274 -0
  157. nextmv/logger.py +80 -9
  158. nextmv/manifest.py +1466 -0
  159. nextmv/model.py +241 -66
  160. nextmv/options.py +708 -115
  161. nextmv/output.py +1301 -274
  162. nextmv/polling.py +325 -0
  163. nextmv/run.py +1702 -0
  164. nextmv/safe.py +145 -0
  165. nextmv/status.py +122 -0
  166. nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
  167. nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
  168. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
  169. nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
  170. nextmv/cloud/application.py +0 -1405
  171. nextmv/cloud/manifest.py +0 -234
  172. nextmv/cloud/status.py +0 -29
  173. nextmv-0.18.0.dist-info/METADATA +0 -770
  174. nextmv-0.18.0.dist-info/RECORD +0 -25
  175. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
nextmv/polling.py ADDED
@@ -0,0 +1,325 @@
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
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: Callable[[], bool] | None = 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
+ sleep_duration_func: Callable[[], float] | None = None
132
+ """
133
+ Optional function to calculate the sleep duration between polls. If provided,
134
+ this function will be called to determine how long to sleep instead of using
135
+ the default exponential backoff calculation. The function should return a
136
+ float representing the sleep duration in seconds. The function does not
137
+ receive any arguments and is called before each sleep.
138
+ """
139
+
140
+
141
+ DEFAULT_POLLING_OPTIONS: PollingOptions = PollingOptions()
142
+ """
143
+ !!! warning
144
+ `DEFAULT_POLLING_OPTIONS` is a mutable global variable. Use the `default_polling_options`
145
+ function to obtain a fresh instance of `PollingOptions` with default settings.
146
+
147
+ Default polling options to use when polling for a run result. This constant
148
+ provides the default values for `PollingOptions` used across the module.
149
+ """
150
+
151
+
152
+ def default_polling_options() -> PollingOptions:
153
+ """
154
+ Returns a new instance of PollingOptions with default settings.
155
+
156
+ This function can be used to obtain a fresh set of default polling options
157
+ that can be modified as needed without affecting the global defaults.
158
+
159
+ You can import the `default_polling_options` function directly from `nextmv`:
160
+
161
+ ```python
162
+ from nextmv import default_polling_options
163
+ ```
164
+
165
+ Returns
166
+ -------
167
+ PollingOptions
168
+ A new instance of PollingOptions with default values.
169
+ """
170
+
171
+ return PollingOptions()
172
+
173
+
174
+ def poll( # noqa: C901
175
+ polling_options: PollingOptions,
176
+ polling_func: Callable[[], tuple[Any, bool]],
177
+ __sleep_func: Callable[[float], None] = time.sleep,
178
+ ) -> Any:
179
+ """
180
+ Poll a function until it succeeds or the polling strategy is exhausted.
181
+
182
+ You can import the `poll` function directly from `nextmv`:
183
+
184
+ ```python
185
+ from nextmv import poll
186
+ ```
187
+
188
+ This function implements a flexible polling strategy with exponential backoff
189
+ and jitter. It calls the provided polling function repeatedly until it indicates
190
+ success, the maximum number of tries is reached, or the maximum duration is exceeded.
191
+
192
+ The `polling_func` is a callable that must return a `tuple[Any, bool]`
193
+ where the first element is the result of the polling and the second
194
+ element is a boolean indicating if the polling was successful or should be
195
+ retried.
196
+
197
+ Parameters
198
+ ----------
199
+ polling_options : PollingOptions
200
+ Options for configuring the polling behavior, including retry counts,
201
+ delays, timeouts, and verbosity settings.
202
+ polling_func : callable
203
+ Function to call to check if the polling was successful. Must return a tuple
204
+ where the first element is the result value and the second is a boolean
205
+ indicating success (True) or need to retry (False).
206
+
207
+ Returns
208
+ -------
209
+ Any
210
+ Result value from the polling function when successful.
211
+
212
+ Raises
213
+ ------
214
+ TimeoutError
215
+ If the polling exceeds the maximum duration specified in polling_options.
216
+ RuntimeError
217
+ If the maximum number of tries is exhausted without success.
218
+
219
+ Examples
220
+ --------
221
+ >>> from nextmv.cloud import PollingOptions, poll
222
+ >>> import time
223
+ >>>
224
+ >>> # Define a polling function that succeeds after 3 tries
225
+ >>> counter = 0
226
+ >>> def check_completion() -> tuple[str, bool]:
227
+ ... global counter
228
+ ... counter += 1
229
+ ... if counter >= 3:
230
+ ... return "Success", True
231
+ ... return None, False
232
+ ...
233
+ >>> # Configure polling options
234
+ >>> options = PollingOptions(
235
+ ... max_tries=5,
236
+ ... delay=0.1,
237
+ ... backoff=0.2,
238
+ ... verbose=True
239
+ ... )
240
+ >>>
241
+ >>> # Poll until the function succeeds
242
+ >>> result = poll(options, check_completion)
243
+ >>> print(result)
244
+ 'Success'
245
+ """
246
+
247
+ # Start by sleeping for the duration specified as initial delay.
248
+ if polling_options.verbose:
249
+ log(f"polling | sleeping for initial delay: {polling_options.initial_delay}")
250
+
251
+ __sleep_func(polling_options.initial_delay)
252
+
253
+ start_time = time.time()
254
+ stopped = False
255
+
256
+ # Begin the polling process.
257
+ max_reached = False
258
+ ix = 0
259
+ while True:
260
+ # Check if we reached the maximum number of tries. Break if so.
261
+ if ix >= polling_options.max_tries and polling_options.max_tries >= 0:
262
+ break
263
+ ix += 1
264
+
265
+ # Check is we should stop polling according to the stop callback.
266
+ if polling_options.stop is not None and polling_options.stop():
267
+ stopped = True
268
+
269
+ break
270
+
271
+ # We check if we can stop polling.
272
+ result, ok = polling_func()
273
+ if polling_options.verbose:
274
+ log(f"polling | try # {ix + 1}, ok: {ok}")
275
+
276
+ if ok:
277
+ return result
278
+
279
+ # An exit condition happens if we exceed the allowed duration.
280
+ passed = time.time() - start_time
281
+ if polling_options.verbose:
282
+ log(f"polling | elapsed time: {passed}")
283
+
284
+ if passed >= polling_options.max_duration and polling_options.max_duration >= 0:
285
+ raise TimeoutError(
286
+ f"polling did not succeed after {passed} seconds, exceeds max duration: {polling_options.max_duration}",
287
+ )
288
+
289
+ # Calculate the delay.
290
+ if polling_options.sleep_duration_func is not None:
291
+ # Use the custom sleep duration function if provided.
292
+ sleep_duration = polling_options.sleep_duration_func()
293
+ else:
294
+ # Calculate delay using exponential backoff with jitter.
295
+ if max_reached:
296
+ # If we already reached the maximum, we don't want to further calculate the
297
+ # delay to avoid overflows.
298
+ delay = polling_options.max_delay
299
+ else:
300
+ delay = polling_options.delay # Base
301
+ delay += polling_options.backoff * (2**ix) # Add exponential backoff.
302
+
303
+ # We cannot exceed the max delay.
304
+ if delay >= polling_options.max_delay:
305
+ max_reached = True
306
+ delay = polling_options.max_delay
307
+
308
+ # Add jitter.
309
+ delay += random.uniform(0, polling_options.jitter)
310
+
311
+ sleep_duration = delay
312
+
313
+ if polling_options.verbose:
314
+ log(f"polling | sleeping for duration: {sleep_duration}")
315
+
316
+ __sleep_func(sleep_duration)
317
+
318
+ if stopped:
319
+ log("polling | stop condition met, stopping polling")
320
+
321
+ return None
322
+
323
+ raise RuntimeError(
324
+ f"polling did not succeed after {polling_options.max_tries} tries",
325
+ )