nextmv 0.27.0__py3-none-any.whl → 0.28.1.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 +1 -1
- nextmv/__init__.py +1 -0
- nextmv/base_model.py +52 -7
- nextmv/cloud/__init__.py +3 -0
- nextmv/cloud/acceptance_test.py +711 -20
- nextmv/cloud/account.py +152 -7
- nextmv/cloud/application.py +1213 -382
- nextmv/cloud/batch_experiment.py +133 -21
- nextmv/cloud/client.py +240 -46
- nextmv/cloud/input_set.py +96 -3
- nextmv/cloud/instance.py +89 -3
- nextmv/cloud/manifest.py +508 -132
- nextmv/cloud/package.py +2 -2
- nextmv/cloud/run.py +372 -41
- nextmv/cloud/safe.py +7 -7
- nextmv/cloud/scenario.py +205 -20
- nextmv/cloud/secrets.py +179 -6
- nextmv/cloud/status.py +95 -2
- nextmv/cloud/version.py +132 -4
- nextmv/deprecated.py +36 -2
- nextmv/input.py +298 -80
- nextmv/logger.py +71 -7
- nextmv/model.py +223 -56
- nextmv/options.py +281 -66
- nextmv/output.py +552 -159
- {nextmv-0.27.0.dist-info → nextmv-0.28.1.dev0.dist-info}/METADATA +24 -4
- nextmv-0.28.1.dev0.dist-info/RECORD +30 -0
- nextmv-0.27.0.dist-info/RECORD +0 -30
- {nextmv-0.27.0.dist-info → nextmv-0.28.1.dev0.dist-info}/WHEEL +0 -0
- {nextmv-0.27.0.dist-info → nextmv-0.28.1.dev0.dist-info}/licenses/LICENSE +0 -0
nextmv/cloud/application.py
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
Application module for interacting with Nextmv Cloud applications.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to interact with applications in Nextmv Cloud,
|
|
5
|
+
including application management, running applications, and managing experiments
|
|
6
|
+
and inputs.
|
|
7
|
+
|
|
8
|
+
Classes
|
|
9
|
+
-------
|
|
10
|
+
DownloadURL
|
|
11
|
+
Result of getting a download URL.
|
|
12
|
+
PollingOptions
|
|
13
|
+
Options for polling when waiting for run results.
|
|
14
|
+
UploadURL
|
|
15
|
+
Result of getting an upload URL.
|
|
16
|
+
Application
|
|
17
|
+
Class for interacting with applications in Nextmv Cloud.
|
|
18
|
+
|
|
19
|
+
Functions
|
|
20
|
+
---------
|
|
21
|
+
poll
|
|
22
|
+
Function to poll for results with configurable options.
|
|
23
|
+
"""
|
|
2
24
|
|
|
3
25
|
import json
|
|
4
26
|
import random
|
|
@@ -34,7 +56,7 @@ from nextmv.cloud.run import (
|
|
|
34
56
|
RunResult,
|
|
35
57
|
TrackedRun,
|
|
36
58
|
)
|
|
37
|
-
from nextmv.cloud.safe import
|
|
59
|
+
from nextmv.cloud.safe import _name_and_id
|
|
38
60
|
from nextmv.cloud.scenario import Scenario, ScenarioInputType, _option_sets, _scenarios_by_id
|
|
39
61
|
from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
|
|
40
62
|
from nextmv.cloud.status import StatusV2
|
|
@@ -45,13 +67,36 @@ from nextmv.model import Model, ModelConfiguration
|
|
|
45
67
|
from nextmv.options import Options
|
|
46
68
|
from nextmv.output import Output
|
|
47
69
|
|
|
70
|
+
# Maximum size of the run input/output in bytes. This constant defines the
|
|
71
|
+
# maximum allowed size for run inputs and outputs. When the size exceeds this
|
|
72
|
+
# value, the system will automatically use the large input upload and/or large
|
|
73
|
+
# result download endpoints.
|
|
48
74
|
_MAX_RUN_SIZE: int = 5 * 1024 * 1024
|
|
49
|
-
"""Maximum size of the run input/output. This value is used to determine
|
|
50
|
-
whether to use the large input upload and/or result download endpoints."""
|
|
51
75
|
|
|
52
76
|
|
|
53
77
|
class DownloadURL(BaseModel):
|
|
54
|
-
"""
|
|
78
|
+
"""
|
|
79
|
+
Result of getting a download URL.
|
|
80
|
+
|
|
81
|
+
You can import the `DownloadURL` class directly from `cloud`:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from nextmv.cloud import DownloadURL
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This class represents a download URL that can be used to fetch content
|
|
88
|
+
from Nextmv Cloud, typically used for downloading large run results.
|
|
89
|
+
|
|
90
|
+
Attributes
|
|
91
|
+
----------
|
|
92
|
+
url : str
|
|
93
|
+
URL to use for downloading the file.
|
|
94
|
+
|
|
95
|
+
Examples
|
|
96
|
+
--------
|
|
97
|
+
>>> download_url = DownloadURL(url="https://example.com/download")
|
|
98
|
+
>>> response = requests.get(download_url.url)
|
|
99
|
+
"""
|
|
55
100
|
|
|
56
101
|
url: str
|
|
57
102
|
"""URL to use for downloading the file."""
|
|
@@ -62,6 +107,12 @@ class PollingOptions:
|
|
|
62
107
|
"""
|
|
63
108
|
Options to use when polling for a run result.
|
|
64
109
|
|
|
110
|
+
You can import the `PollingOptions` class directly from `cloud`:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from nextmv.cloud import PollingOptions
|
|
114
|
+
```
|
|
115
|
+
|
|
65
116
|
The Cloud API will be polled for the result. The polling stops if:
|
|
66
117
|
|
|
67
118
|
* The maximum number of polls (tries) are exhausted. This is specified by
|
|
@@ -82,6 +133,43 @@ class PollingOptions:
|
|
|
82
133
|
* Uniform is the uniform distribution.
|
|
83
134
|
|
|
84
135
|
Note that the sleep duration is capped by the `max_delay` parameter.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
backoff : float, default=0.9
|
|
140
|
+
Exponential backoff factor, in seconds, to use between polls.
|
|
141
|
+
delay : float, default=0.1
|
|
142
|
+
Base delay to use between polls, in seconds.
|
|
143
|
+
initial_delay : float, default=1.0
|
|
144
|
+
Initial delay to use before starting the polling strategy, in seconds.
|
|
145
|
+
max_delay : float, default=20.0
|
|
146
|
+
Maximum delay to use between polls, in seconds.
|
|
147
|
+
max_duration : float, default=300.0
|
|
148
|
+
Maximum duration of the polling strategy, in seconds.
|
|
149
|
+
max_tries : int, default=100
|
|
150
|
+
Maximum number of tries to use.
|
|
151
|
+
jitter : float, default=1.0
|
|
152
|
+
Jitter to use for the polling strategy. A uniform distribution is sampled
|
|
153
|
+
between 0 and this number. The resulting random number is added to the
|
|
154
|
+
delay for each poll, adding a random noise. Set this to 0 to avoid using
|
|
155
|
+
random jitter.
|
|
156
|
+
verbose : bool, default=False
|
|
157
|
+
Whether to log the polling strategy. This is useful for debugging.
|
|
158
|
+
stop : callable, default=None
|
|
159
|
+
Function to call to check if the polling should stop. This is useful for
|
|
160
|
+
stopping the polling based on external conditions. The function should
|
|
161
|
+
return True to stop the polling and False to continue. The function does
|
|
162
|
+
not receive any arguments. The function is called before each poll.
|
|
163
|
+
|
|
164
|
+
Examples
|
|
165
|
+
--------
|
|
166
|
+
>>> from nextmv.cloud import PollingOptions
|
|
167
|
+
>>> # Create polling options with custom settings
|
|
168
|
+
>>> polling_options = PollingOptions(
|
|
169
|
+
... max_tries=50,
|
|
170
|
+
... max_duration=600,
|
|
171
|
+
... verbose=True
|
|
172
|
+
... )
|
|
85
173
|
"""
|
|
86
174
|
|
|
87
175
|
backoff: float = 0.9
|
|
@@ -118,12 +206,39 @@ class PollingOptions:
|
|
|
118
206
|
"""
|
|
119
207
|
|
|
120
208
|
|
|
209
|
+
# Default polling options to use when polling for a run result. This constant
|
|
210
|
+
# provides the default values for `PollingOptions` used across the module.
|
|
211
|
+
# Using these defaults is recommended for most use cases unless specific timing
|
|
212
|
+
# needs are required.
|
|
121
213
|
_DEFAULT_POLLING_OPTIONS: PollingOptions = PollingOptions()
|
|
122
|
-
"""Default polling options to use when polling for a run result."""
|
|
123
214
|
|
|
124
215
|
|
|
125
216
|
class UploadURL(BaseModel):
|
|
126
|
-
"""
|
|
217
|
+
"""
|
|
218
|
+
Result of getting an upload URL.
|
|
219
|
+
|
|
220
|
+
You can import the `UploadURL` class directly from `cloud`:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from nextmv.cloud import UploadURL
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
This class represents an upload URL that can be used to send data to
|
|
227
|
+
Nextmv Cloud, typically used for uploading large inputs for runs.
|
|
228
|
+
|
|
229
|
+
Attributes
|
|
230
|
+
----------
|
|
231
|
+
upload_id : str
|
|
232
|
+
ID of the upload, used to reference the uploaded content.
|
|
233
|
+
upload_url : str
|
|
234
|
+
URL to use for uploading the file.
|
|
235
|
+
|
|
236
|
+
Examples
|
|
237
|
+
--------
|
|
238
|
+
>>> upload_url = UploadURL(upload_id="123", upload_url="https://example.com/upload")
|
|
239
|
+
>>> with open("large_input.json", "rb") as f:
|
|
240
|
+
... requests.put(upload_url.upload_url, data=f)
|
|
241
|
+
"""
|
|
127
242
|
|
|
128
243
|
upload_id: str
|
|
129
244
|
"""ID of the upload."""
|
|
@@ -133,7 +248,40 @@ class UploadURL(BaseModel):
|
|
|
133
248
|
|
|
134
249
|
@dataclass
|
|
135
250
|
class Application:
|
|
136
|
-
"""
|
|
251
|
+
"""
|
|
252
|
+
A published decision model that can be executed.
|
|
253
|
+
|
|
254
|
+
You can import the `Application` class directly from `cloud`:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from nextmv.cloud import Application
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
This class represents an application in Nextmv Cloud, providing methods to
|
|
261
|
+
interact with the application, run it with different inputs, manage versions,
|
|
262
|
+
instances, experiments, and more.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
client : Client
|
|
267
|
+
Client to use for interacting with the Nextmv Cloud API.
|
|
268
|
+
id : str
|
|
269
|
+
ID of the application.
|
|
270
|
+
default_instance_id : str, default="devint"
|
|
271
|
+
Default instance ID to use for submitting runs.
|
|
272
|
+
endpoint : str, default="v1/applications/{id}"
|
|
273
|
+
Base endpoint for the application.
|
|
274
|
+
experiments_endpoint : str, default="{base}/experiments"
|
|
275
|
+
Base endpoint for the experiments in the application.
|
|
276
|
+
|
|
277
|
+
Examples
|
|
278
|
+
--------
|
|
279
|
+
>>> from nextmv.cloud import Client, Application
|
|
280
|
+
>>> client = Client(api_key="your-api-key")
|
|
281
|
+
>>> app = Application(client=client, id="your-app-id")
|
|
282
|
+
>>> # Retrieve app information
|
|
283
|
+
>>> instances = app.list_instances()
|
|
284
|
+
"""
|
|
137
285
|
|
|
138
286
|
client: Client
|
|
139
287
|
"""Client to use for interacting with the Nextmv Cloud API."""
|
|
@@ -148,25 +296,39 @@ class Application:
|
|
|
148
296
|
"""Base endpoint for the experiments in the application."""
|
|
149
297
|
|
|
150
298
|
def __post_init__(self):
|
|
151
|
-
"""
|
|
299
|
+
"""Initialize the endpoint and experiments_endpoint attributes.
|
|
152
300
|
|
|
301
|
+
This method is automatically called after class initialization to
|
|
302
|
+
format the endpoint and experiments_endpoint URLs with the application ID.
|
|
303
|
+
"""
|
|
153
304
|
self.endpoint = self.endpoint.format(id=self.id)
|
|
154
305
|
self.experiments_endpoint = self.experiments_endpoint.format(base=self.endpoint)
|
|
155
306
|
|
|
156
307
|
def acceptance_test(self, acceptance_test_id: str) -> AcceptanceTest:
|
|
157
308
|
"""
|
|
158
|
-
|
|
309
|
+
Retrieve details of an acceptance test.
|
|
159
310
|
|
|
160
|
-
|
|
161
|
-
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
acceptance_test_id : str
|
|
314
|
+
ID of the acceptance test to retrieve.
|
|
162
315
|
|
|
163
|
-
Returns
|
|
164
|
-
|
|
316
|
+
Returns
|
|
317
|
+
-------
|
|
318
|
+
AcceptanceTest
|
|
319
|
+
The requested acceptance test details.
|
|
165
320
|
|
|
166
|
-
Raises
|
|
167
|
-
|
|
168
|
-
|
|
321
|
+
Raises
|
|
322
|
+
------
|
|
323
|
+
requests.HTTPError
|
|
324
|
+
If the response status code is not 2xx.
|
|
169
325
|
|
|
326
|
+
Examples
|
|
327
|
+
--------
|
|
328
|
+
>>> test = app.acceptance_test("test-123")
|
|
329
|
+
>>> print(test.name)
|
|
330
|
+
'My Test'
|
|
331
|
+
"""
|
|
170
332
|
response = self.client.request(
|
|
171
333
|
method="GET",
|
|
172
334
|
endpoint=f"{self.experiments_endpoint}/acceptance/{acceptance_test_id}",
|
|
@@ -178,14 +340,26 @@ class Application:
|
|
|
178
340
|
"""
|
|
179
341
|
Get a batch experiment.
|
|
180
342
|
|
|
181
|
-
|
|
182
|
-
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
batch_id : str
|
|
346
|
+
ID of the batch experiment.
|
|
347
|
+
|
|
348
|
+
Returns
|
|
349
|
+
-------
|
|
350
|
+
BatchExperiment
|
|
351
|
+
The requested batch experiment details.
|
|
183
352
|
|
|
184
|
-
|
|
185
|
-
|
|
353
|
+
Raises
|
|
354
|
+
------
|
|
355
|
+
requests.HTTPError
|
|
356
|
+
If the response status code is not 2xx.
|
|
186
357
|
|
|
187
|
-
|
|
188
|
-
|
|
358
|
+
Examples
|
|
359
|
+
--------
|
|
360
|
+
>>> batch_exp = app.batch_experiment("batch-123")
|
|
361
|
+
>>> print(batch_exp.name)
|
|
362
|
+
'My Batch Experiment'
|
|
189
363
|
"""
|
|
190
364
|
|
|
191
365
|
response = self.client.request(
|
|
@@ -199,11 +373,19 @@ class Application:
|
|
|
199
373
|
"""
|
|
200
374
|
Cancel a run.
|
|
201
375
|
|
|
202
|
-
|
|
203
|
-
|
|
376
|
+
Parameters
|
|
377
|
+
----------
|
|
378
|
+
run_id : str
|
|
379
|
+
ID of the run to cancel.
|
|
380
|
+
|
|
381
|
+
Raises
|
|
382
|
+
------
|
|
383
|
+
requests.HTTPError
|
|
384
|
+
If the response status code is not 2xx.
|
|
204
385
|
|
|
205
|
-
|
|
206
|
-
|
|
386
|
+
Examples
|
|
387
|
+
--------
|
|
388
|
+
>>> app.cancel_run("run-456")
|
|
207
389
|
"""
|
|
208
390
|
|
|
209
391
|
_ = self.client.request(
|
|
@@ -215,8 +397,16 @@ class Application:
|
|
|
215
397
|
"""
|
|
216
398
|
Delete the application.
|
|
217
399
|
|
|
218
|
-
|
|
219
|
-
|
|
400
|
+
Permanently removes the application from Nextmv Cloud.
|
|
401
|
+
|
|
402
|
+
Raises
|
|
403
|
+
------
|
|
404
|
+
requests.HTTPError
|
|
405
|
+
If the response status code is not 2xx.
|
|
406
|
+
|
|
407
|
+
Examples
|
|
408
|
+
--------
|
|
409
|
+
>>> app.delete() # Permanently deletes the application
|
|
220
410
|
"""
|
|
221
411
|
|
|
222
412
|
_ = self.client.request(
|
|
@@ -226,14 +416,24 @@ class Application:
|
|
|
226
416
|
|
|
227
417
|
def delete_acceptance_test(self, acceptance_test_id: str) -> None:
|
|
228
418
|
"""
|
|
229
|
-
|
|
419
|
+
Delete an acceptance test.
|
|
420
|
+
|
|
421
|
+
Deletes an acceptance test along with all the associated information
|
|
230
422
|
such as the underlying batch experiment.
|
|
231
423
|
|
|
232
|
-
|
|
233
|
-
|
|
424
|
+
Parameters
|
|
425
|
+
----------
|
|
426
|
+
acceptance_test_id : str
|
|
427
|
+
ID of the acceptance test to delete.
|
|
428
|
+
|
|
429
|
+
Raises
|
|
430
|
+
------
|
|
431
|
+
requests.HTTPError
|
|
432
|
+
If the response status code is not 2xx.
|
|
234
433
|
|
|
235
|
-
|
|
236
|
-
|
|
434
|
+
Examples
|
|
435
|
+
--------
|
|
436
|
+
>>> app.delete_acceptance_test("test-123")
|
|
237
437
|
"""
|
|
238
438
|
|
|
239
439
|
_ = self.client.request(
|
|
@@ -243,18 +443,24 @@ class Application:
|
|
|
243
443
|
|
|
244
444
|
def delete_batch_experiment(self, batch_id: str) -> None:
|
|
245
445
|
"""
|
|
246
|
-
|
|
446
|
+
Delete a batch experiment.
|
|
447
|
+
|
|
448
|
+
Deletes a batch experiment along with all the associated information,
|
|
247
449
|
such as its runs.
|
|
248
450
|
|
|
249
451
|
Parameters
|
|
250
452
|
----------
|
|
251
|
-
batch_id: str
|
|
252
|
-
ID of the batch experiment.
|
|
453
|
+
batch_id : str
|
|
454
|
+
ID of the batch experiment to delete.
|
|
253
455
|
|
|
254
456
|
Raises
|
|
255
457
|
------
|
|
256
458
|
requests.HTTPError
|
|
257
459
|
If the response status code is not 2xx.
|
|
460
|
+
|
|
461
|
+
Examples
|
|
462
|
+
--------
|
|
463
|
+
>>> app.delete_batch_experiment("batch-123")
|
|
258
464
|
"""
|
|
259
465
|
|
|
260
466
|
_ = self.client.request(
|
|
@@ -264,31 +470,45 @@ class Application:
|
|
|
264
470
|
|
|
265
471
|
def delete_scenario_test(self, scenario_test_id: str) -> None:
|
|
266
472
|
"""
|
|
473
|
+
Delete a scenario test.
|
|
474
|
+
|
|
267
475
|
Deletes a scenario test. Scenario tests are based on the batch
|
|
268
476
|
experiments API, so this function summons `delete_batch_experiment`.
|
|
269
477
|
|
|
270
478
|
Parameters
|
|
271
479
|
----------
|
|
272
|
-
scenario_test_id: str
|
|
273
|
-
ID of the scenario test.
|
|
480
|
+
scenario_test_id : str
|
|
481
|
+
ID of the scenario test to delete.
|
|
274
482
|
|
|
275
483
|
Raises
|
|
276
484
|
------
|
|
277
485
|
requests.HTTPError
|
|
278
486
|
If the response status code is not 2xx.
|
|
487
|
+
|
|
488
|
+
Examples
|
|
489
|
+
--------
|
|
490
|
+
>>> app.delete_scenario_test("scenario-123")
|
|
279
491
|
"""
|
|
280
492
|
|
|
281
493
|
self.delete_batch_experiment(batch_id=scenario_test_id)
|
|
282
494
|
|
|
283
495
|
def delete_secrets_collection(self, secrets_collection_id: str) -> None:
|
|
284
496
|
"""
|
|
285
|
-
|
|
497
|
+
Delete a secrets collection.
|
|
286
498
|
|
|
287
|
-
|
|
288
|
-
|
|
499
|
+
Parameters
|
|
500
|
+
----------
|
|
501
|
+
secrets_collection_id : str
|
|
502
|
+
ID of the secrets collection to delete.
|
|
503
|
+
|
|
504
|
+
Raises
|
|
505
|
+
------
|
|
506
|
+
requests.HTTPError
|
|
507
|
+
If the response status code is not 2xx.
|
|
289
508
|
|
|
290
|
-
|
|
291
|
-
|
|
509
|
+
Examples
|
|
510
|
+
--------
|
|
511
|
+
>>> app.delete_secrets_collection("secrets-123")
|
|
292
512
|
"""
|
|
293
513
|
|
|
294
514
|
_ = self.client.request(
|
|
@@ -301,12 +521,24 @@ class Application:
|
|
|
301
521
|
"""
|
|
302
522
|
Check if an application exists.
|
|
303
523
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
524
|
+
Parameters
|
|
525
|
+
----------
|
|
526
|
+
client : Client
|
|
527
|
+
Client to use for interacting with the Nextmv Cloud API.
|
|
528
|
+
id : str
|
|
529
|
+
ID of the application to check.
|
|
307
530
|
|
|
308
|
-
Returns
|
|
531
|
+
Returns
|
|
532
|
+
-------
|
|
533
|
+
bool
|
|
309
534
|
True if the application exists, False otherwise.
|
|
535
|
+
|
|
536
|
+
Examples
|
|
537
|
+
--------
|
|
538
|
+
>>> from nextmv.cloud import Client
|
|
539
|
+
>>> client = Client(api_key="your-api-key")
|
|
540
|
+
>>> Application.exists(client, "app-123")
|
|
541
|
+
True
|
|
310
542
|
"""
|
|
311
543
|
|
|
312
544
|
try:
|
|
@@ -326,14 +558,26 @@ class Application:
|
|
|
326
558
|
"""
|
|
327
559
|
Get an input set.
|
|
328
560
|
|
|
329
|
-
|
|
330
|
-
|
|
561
|
+
Parameters
|
|
562
|
+
----------
|
|
563
|
+
input_set_id : str
|
|
564
|
+
ID of the input set to retrieve.
|
|
565
|
+
|
|
566
|
+
Returns
|
|
567
|
+
-------
|
|
568
|
+
InputSet
|
|
569
|
+
The requested input set.
|
|
331
570
|
|
|
332
|
-
|
|
333
|
-
|
|
571
|
+
Raises
|
|
572
|
+
------
|
|
573
|
+
requests.HTTPError
|
|
574
|
+
If the response status code is not 2xx.
|
|
334
575
|
|
|
335
|
-
|
|
336
|
-
|
|
576
|
+
Examples
|
|
577
|
+
--------
|
|
578
|
+
>>> input_set = app.input_set("input-set-123")
|
|
579
|
+
>>> print(input_set.name)
|
|
580
|
+
'My Input Set'
|
|
337
581
|
"""
|
|
338
582
|
|
|
339
583
|
response = self.client.request(
|
|
@@ -347,14 +591,26 @@ class Application:
|
|
|
347
591
|
"""
|
|
348
592
|
Get an instance.
|
|
349
593
|
|
|
350
|
-
|
|
351
|
-
|
|
594
|
+
Parameters
|
|
595
|
+
----------
|
|
596
|
+
instance_id : str
|
|
597
|
+
ID of the instance to retrieve.
|
|
352
598
|
|
|
353
|
-
Returns
|
|
354
|
-
|
|
599
|
+
Returns
|
|
600
|
+
-------
|
|
601
|
+
Instance
|
|
602
|
+
The requested instance details.
|
|
355
603
|
|
|
356
|
-
Raises
|
|
357
|
-
|
|
604
|
+
Raises
|
|
605
|
+
------
|
|
606
|
+
requests.HTTPError
|
|
607
|
+
If the response status code is not 2xx.
|
|
608
|
+
|
|
609
|
+
Examples
|
|
610
|
+
--------
|
|
611
|
+
>>> instance = app.instance("instance-123")
|
|
612
|
+
>>> print(instance.name)
|
|
613
|
+
'Production Instance'
|
|
358
614
|
"""
|
|
359
615
|
|
|
360
616
|
response = self.client.request(
|
|
@@ -368,11 +624,20 @@ class Application:
|
|
|
368
624
|
"""
|
|
369
625
|
Check if an instance exists.
|
|
370
626
|
|
|
371
|
-
|
|
372
|
-
|
|
627
|
+
Parameters
|
|
628
|
+
----------
|
|
629
|
+
instance_id : str
|
|
630
|
+
ID of the instance to check.
|
|
373
631
|
|
|
374
|
-
Returns
|
|
632
|
+
Returns
|
|
633
|
+
-------
|
|
634
|
+
bool
|
|
375
635
|
True if the instance exists, False otherwise.
|
|
636
|
+
|
|
637
|
+
Examples
|
|
638
|
+
--------
|
|
639
|
+
>>> app.instance_exists("instance-123")
|
|
640
|
+
True
|
|
376
641
|
"""
|
|
377
642
|
|
|
378
643
|
try:
|
|
@@ -387,11 +652,23 @@ class Application:
|
|
|
387
652
|
"""
|
|
388
653
|
List all acceptance tests.
|
|
389
654
|
|
|
390
|
-
Returns
|
|
391
|
-
|
|
655
|
+
Returns
|
|
656
|
+
-------
|
|
657
|
+
list[AcceptanceTest]
|
|
658
|
+
List of all acceptance tests associated with this application.
|
|
659
|
+
|
|
660
|
+
Raises
|
|
661
|
+
------
|
|
662
|
+
requests.HTTPError
|
|
663
|
+
If the response status code is not 2xx.
|
|
392
664
|
|
|
393
|
-
|
|
394
|
-
|
|
665
|
+
Examples
|
|
666
|
+
--------
|
|
667
|
+
>>> tests = app.list_acceptance_tests()
|
|
668
|
+
>>> for test in tests:
|
|
669
|
+
... print(test.name)
|
|
670
|
+
'Test 1'
|
|
671
|
+
'Test 2'
|
|
395
672
|
"""
|
|
396
673
|
|
|
397
674
|
response = self.client.request(
|
|
@@ -428,11 +705,23 @@ class Application:
|
|
|
428
705
|
"""
|
|
429
706
|
List all input sets.
|
|
430
707
|
|
|
431
|
-
Returns
|
|
432
|
-
|
|
708
|
+
Returns
|
|
709
|
+
-------
|
|
710
|
+
list[InputSet]
|
|
711
|
+
List of all input sets associated with this application.
|
|
433
712
|
|
|
434
|
-
Raises
|
|
435
|
-
|
|
713
|
+
Raises
|
|
714
|
+
------
|
|
715
|
+
requests.HTTPError
|
|
716
|
+
If the response status code is not 2xx.
|
|
717
|
+
|
|
718
|
+
Examples
|
|
719
|
+
--------
|
|
720
|
+
>>> input_sets = app.list_input_sets()
|
|
721
|
+
>>> for input_set in input_sets:
|
|
722
|
+
... print(input_set.name)
|
|
723
|
+
'Input Set 1'
|
|
724
|
+
'Input Set 2'
|
|
436
725
|
"""
|
|
437
726
|
|
|
438
727
|
response = self.client.request(
|
|
@@ -446,11 +735,23 @@ class Application:
|
|
|
446
735
|
"""
|
|
447
736
|
List all instances.
|
|
448
737
|
|
|
449
|
-
Returns
|
|
450
|
-
|
|
738
|
+
Returns
|
|
739
|
+
-------
|
|
740
|
+
list[Instance]
|
|
741
|
+
List of all instances associated with this application.
|
|
742
|
+
|
|
743
|
+
Raises
|
|
744
|
+
------
|
|
745
|
+
requests.HTTPError
|
|
746
|
+
If the response status code is not 2xx.
|
|
451
747
|
|
|
452
|
-
|
|
453
|
-
|
|
748
|
+
Examples
|
|
749
|
+
--------
|
|
750
|
+
>>> instances = app.list_instances()
|
|
751
|
+
>>> for instance in instances:
|
|
752
|
+
... print(instance.name)
|
|
753
|
+
'Development Instance'
|
|
754
|
+
'Production Instance'
|
|
454
755
|
"""
|
|
455
756
|
|
|
456
757
|
response = self.client.request(
|
|
@@ -511,11 +812,23 @@ class Application:
|
|
|
511
812
|
"""
|
|
512
813
|
List all secrets collections.
|
|
513
814
|
|
|
514
|
-
Returns
|
|
515
|
-
|
|
815
|
+
Returns
|
|
816
|
+
-------
|
|
817
|
+
list[SecretsCollectionSummary]
|
|
818
|
+
List of all secrets collections associated with this application.
|
|
819
|
+
|
|
820
|
+
Raises
|
|
821
|
+
------
|
|
822
|
+
requests.HTTPError
|
|
823
|
+
If the response status code is not 2xx.
|
|
516
824
|
|
|
517
|
-
|
|
518
|
-
|
|
825
|
+
Examples
|
|
826
|
+
--------
|
|
827
|
+
>>> collections = app.list_secrets_collections()
|
|
828
|
+
>>> for collection in collections:
|
|
829
|
+
... print(collection.name)
|
|
830
|
+
'API Keys'
|
|
831
|
+
'Database Credentials'
|
|
519
832
|
"""
|
|
520
833
|
|
|
521
834
|
response = self.client.request(
|
|
@@ -529,11 +842,23 @@ class Application:
|
|
|
529
842
|
"""
|
|
530
843
|
List all versions.
|
|
531
844
|
|
|
532
|
-
Returns
|
|
533
|
-
|
|
845
|
+
Returns
|
|
846
|
+
-------
|
|
847
|
+
list[Version]
|
|
848
|
+
List of all versions associated with this application.
|
|
849
|
+
|
|
850
|
+
Raises
|
|
851
|
+
------
|
|
852
|
+
requests.HTTPError
|
|
853
|
+
If the response status code is not 2xx.
|
|
534
854
|
|
|
535
|
-
|
|
536
|
-
|
|
855
|
+
Examples
|
|
856
|
+
--------
|
|
857
|
+
>>> versions = app.list_versions()
|
|
858
|
+
>>> for version in versions:
|
|
859
|
+
... print(version.name)
|
|
860
|
+
'v1.0.0'
|
|
861
|
+
'v1.1.0'
|
|
537
862
|
"""
|
|
538
863
|
|
|
539
864
|
response = self.client.request(
|
|
@@ -583,17 +908,32 @@ class Application:
|
|
|
583
908
|
"""
|
|
584
909
|
Create a new application.
|
|
585
910
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
911
|
+
Parameters
|
|
912
|
+
----------
|
|
913
|
+
client : Client
|
|
914
|
+
Client to use for interacting with the Nextmv Cloud API.
|
|
915
|
+
name : str
|
|
916
|
+
Name of the application.
|
|
917
|
+
id : str, optional
|
|
918
|
+
ID of the application. Will be generated if not provided.
|
|
919
|
+
description : str, optional
|
|
920
|
+
Description of the application.
|
|
921
|
+
is_workflow : bool, optional
|
|
922
|
+
Whether the application is a Decision Workflow.
|
|
923
|
+
exist_ok : bool, default=False
|
|
924
|
+
If True and an application with the same ID already exists,
|
|
925
|
+
return the existing application instead of creating a new one.
|
|
926
|
+
|
|
927
|
+
Returns
|
|
928
|
+
-------
|
|
929
|
+
Application
|
|
930
|
+
The newly created (or existing) application.
|
|
594
931
|
|
|
595
|
-
|
|
596
|
-
|
|
932
|
+
Examples
|
|
933
|
+
--------
|
|
934
|
+
>>> from nextmv.cloud import Client
|
|
935
|
+
>>> client = Client(api_key="your-api-key")
|
|
936
|
+
>>> app = Application.new(client=client, name="My New App", id="my-app")
|
|
597
937
|
"""
|
|
598
938
|
|
|
599
939
|
if exist_ok and cls.exists(client=client, id=id):
|
|
@@ -629,30 +969,44 @@ class Application:
|
|
|
629
969
|
description: Optional[str] = None,
|
|
630
970
|
) -> AcceptanceTest:
|
|
631
971
|
"""
|
|
632
|
-
Create a new acceptance test.
|
|
633
|
-
experiment. If you already started a batch experiment, you don't need
|
|
634
|
-
to provide the input_set_id parameter. In that case, the ID of the
|
|
635
|
-
acceptance test and the batch experiment must be the same. If the batch
|
|
636
|
-
experiment does not exist, you can provide the input_set_id parameter
|
|
637
|
-
and a new batch experiment will be created for you.
|
|
972
|
+
Create a new acceptance test.
|
|
638
973
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
input_set_id: ID of the input set to use for the underlying batch
|
|
646
|
-
experiment, in case it hasn't been started.
|
|
647
|
-
description: Description of the acceptance test.
|
|
974
|
+
The acceptance test is based on a batch experiment. If you already
|
|
975
|
+
started a batch experiment, you don't need to provide the input_set_id
|
|
976
|
+
parameter. In that case, the ID of the acceptance test and the batch
|
|
977
|
+
experiment must be the same. If the batch experiment does not exist,
|
|
978
|
+
you can provide the input_set_id parameter and a new batch experiment
|
|
979
|
+
will be created for you.
|
|
648
980
|
|
|
649
|
-
|
|
650
|
-
|
|
981
|
+
Parameters
|
|
982
|
+
----------
|
|
983
|
+
candidate_instance_id : str
|
|
984
|
+
ID of the candidate instance.
|
|
985
|
+
baseline_instance_id : str
|
|
986
|
+
ID of the baseline instance.
|
|
987
|
+
id : str
|
|
988
|
+
ID of the acceptance test.
|
|
989
|
+
metrics : list[Union[Metric, dict[str, Any]]]
|
|
990
|
+
List of metrics to use for the acceptance test.
|
|
991
|
+
name : str
|
|
992
|
+
Name of the acceptance test.
|
|
993
|
+
input_set_id : Optional[str], default=None
|
|
994
|
+
ID of the input set to use for the underlying batch experiment,
|
|
995
|
+
in case it hasn't been started.
|
|
996
|
+
description : Optional[str], default=None
|
|
997
|
+
Description of the acceptance test.
|
|
998
|
+
|
|
999
|
+
Returns
|
|
1000
|
+
-------
|
|
1001
|
+
AcceptanceTest
|
|
1002
|
+
The created acceptance test.
|
|
651
1003
|
|
|
652
|
-
Raises
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1004
|
+
Raises
|
|
1005
|
+
------
|
|
1006
|
+
requests.HTTPError
|
|
1007
|
+
If the response status code is not 2xx.
|
|
1008
|
+
ValueError
|
|
1009
|
+
If the batch experiment ID does not match the acceptance test ID.
|
|
656
1010
|
"""
|
|
657
1011
|
|
|
658
1012
|
if input_set_id is None:
|
|
@@ -714,30 +1068,59 @@ class Application:
|
|
|
714
1068
|
polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
|
|
715
1069
|
) -> AcceptanceTest:
|
|
716
1070
|
"""
|
|
717
|
-
Create a new acceptance test and poll for the result.
|
|
718
|
-
|
|
1071
|
+
Create a new acceptance test and poll for the result.
|
|
1072
|
+
|
|
1073
|
+
This is a convenience method that combines the new_acceptance_test with polling
|
|
719
1074
|
logic to check when the acceptance test is done.
|
|
720
1075
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1076
|
+
Parameters
|
|
1077
|
+
----------
|
|
1078
|
+
candidate_instance_id : str
|
|
1079
|
+
ID of the candidate instance.
|
|
1080
|
+
baseline_instance_id : str
|
|
1081
|
+
ID of the baseline instance.
|
|
1082
|
+
id : str
|
|
1083
|
+
ID of the acceptance test.
|
|
1084
|
+
metrics : list[Union[Metric, dict[str, Any]]]
|
|
1085
|
+
List of metrics to use for the acceptance test.
|
|
1086
|
+
name : str
|
|
1087
|
+
Name of the acceptance test.
|
|
1088
|
+
input_set_id : Optional[str], default=None
|
|
1089
|
+
ID of the input set to use for the underlying batch experiment,
|
|
1090
|
+
in case it hasn't been started.
|
|
1091
|
+
description : Optional[str], default=None
|
|
1092
|
+
Description of the acceptance test.
|
|
1093
|
+
polling_options : PollingOptions, default=_DEFAULT_POLLING_OPTIONS
|
|
1094
|
+
Options to use when polling for the acceptance test result.
|
|
1095
|
+
|
|
1096
|
+
Returns
|
|
1097
|
+
-------
|
|
1098
|
+
AcceptanceTest
|
|
1099
|
+
The completed acceptance test with results.
|
|
1100
|
+
|
|
1101
|
+
Raises
|
|
1102
|
+
------
|
|
1103
|
+
requests.HTTPError
|
|
1104
|
+
If the response status code is not 2xx.
|
|
1105
|
+
TimeoutError
|
|
1106
|
+
If the acceptance test does not succeed after the
|
|
1107
|
+
polling strategy is exhausted based on time duration.
|
|
1108
|
+
RuntimeError
|
|
1109
|
+
If the acceptance test does not succeed after the
|
|
1110
|
+
polling strategy is exhausted based on number of tries.
|
|
1111
|
+
|
|
1112
|
+
Examples
|
|
1113
|
+
--------
|
|
1114
|
+
>>> test = app.new_acceptance_test_with_result(
|
|
1115
|
+
... candidate_instance_id="candidate-123",
|
|
1116
|
+
... baseline_instance_id="baseline-456",
|
|
1117
|
+
... id="test-789",
|
|
1118
|
+
... metrics=[Metric(name="objective", type="numeric")],
|
|
1119
|
+
... name="Performance Test",
|
|
1120
|
+
... input_set_id="input-set-123"
|
|
1121
|
+
... )
|
|
1122
|
+
>>> print(test.status)
|
|
1123
|
+
'completed'
|
|
741
1124
|
"""
|
|
742
1125
|
_ = self.new_acceptance_test(
|
|
743
1126
|
candidate_instance_id=candidate_instance_id,
|
|
@@ -896,7 +1279,6 @@ class Application:
|
|
|
896
1279
|
the input set from a list of inputs that are already available in
|
|
897
1280
|
the application.
|
|
898
1281
|
|
|
899
|
-
|
|
900
1282
|
Returns
|
|
901
1283
|
-------
|
|
902
1284
|
InputSet
|
|
@@ -947,20 +1329,49 @@ class Application:
|
|
|
947
1329
|
"""
|
|
948
1330
|
Create a new instance and associate it with a version.
|
|
949
1331
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1332
|
+
This method creates a new instance associated with a specific version of the application.
|
|
1333
|
+
Instances are configurations of an application version that can be executed.
|
|
1334
|
+
|
|
1335
|
+
Parameters
|
|
1336
|
+
----------
|
|
1337
|
+
version_id : str
|
|
1338
|
+
ID of the version to associate the instance with.
|
|
1339
|
+
id : str
|
|
1340
|
+
ID of the instance. Will be generated if not provided.
|
|
1341
|
+
name : str
|
|
1342
|
+
Name of the instance. Will be generated if not provided.
|
|
1343
|
+
description : Optional[str], default=None
|
|
1344
|
+
Description of the instance.
|
|
1345
|
+
configuration : Optional[InstanceConfiguration], default=None
|
|
1346
|
+
Configuration to use for the instance. This can include resources,
|
|
1347
|
+
timeouts, and other execution parameters.
|
|
1348
|
+
exist_ok : bool, default=False
|
|
1349
|
+
If True and an instance with the same ID already exists,
|
|
1350
|
+
return the existing instance instead of creating a new one.
|
|
1351
|
+
|
|
1352
|
+
Returns
|
|
1353
|
+
-------
|
|
1354
|
+
Instance
|
|
1355
|
+
The newly created (or existing) instance.
|
|
958
1356
|
|
|
959
|
-
|
|
960
|
-
|
|
1357
|
+
Raises
|
|
1358
|
+
------
|
|
1359
|
+
requests.HTTPError
|
|
1360
|
+
If the response status code is not 2xx.
|
|
1361
|
+
ValueError
|
|
1362
|
+
If exist_ok is True and id is None.
|
|
961
1363
|
|
|
962
|
-
|
|
963
|
-
|
|
1364
|
+
Examples
|
|
1365
|
+
--------
|
|
1366
|
+
>>> # Create a new instance for a specific version
|
|
1367
|
+
>>> instance = app.new_instance(
|
|
1368
|
+
... version_id="version-123",
|
|
1369
|
+
... id="prod-instance",
|
|
1370
|
+
... name="Production Instance",
|
|
1371
|
+
... description="Instance for production use"
|
|
1372
|
+
... )
|
|
1373
|
+
>>> print(instance.name)
|
|
1374
|
+
'Production Instance'
|
|
964
1375
|
"""
|
|
965
1376
|
|
|
966
1377
|
if exist_ok and id is None:
|
|
@@ -1135,11 +1546,11 @@ class Application:
|
|
|
1135
1546
|
|
|
1136
1547
|
Raises
|
|
1137
1548
|
----------
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1549
|
+
requests.HTTPError
|
|
1550
|
+
If the response status code is not 2xx.
|
|
1551
|
+
ValueError
|
|
1552
|
+
If the `input` is of type `nextmv.Input` and the .input_format` is
|
|
1553
|
+
not `JSON`. If the final `options` are not of type `dict[str,str]`.
|
|
1143
1554
|
"""
|
|
1144
1555
|
|
|
1145
1556
|
input_data = None
|
|
@@ -1279,7 +1690,7 @@ class Application:
|
|
|
1279
1690
|
batch_experiment_id: Optional[str]
|
|
1280
1691
|
ID of a batch experiment to associate the run with. This is used
|
|
1281
1692
|
when the run is part of a batch experiment.
|
|
1282
|
-
external_result: Optional[Union[ExternalRunResult, dict[str, Any]]]
|
|
1693
|
+
external_result: Optional[Union[ExternalRunResult, dict[str, Any]]] = None
|
|
1283
1694
|
External result to use for the run. This can be a
|
|
1284
1695
|
`cloud.ExternalRunResult` object or a dict. If the object is used,
|
|
1285
1696
|
then the `.to_dict()` method is applied to extract the
|
|
@@ -1294,15 +1705,17 @@ class Application:
|
|
|
1294
1705
|
|
|
1295
1706
|
Raises
|
|
1296
1707
|
----------
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1708
|
+
ValueError
|
|
1709
|
+
If the `input` is of type `nextmv.Input` and the `.input_format` is
|
|
1710
|
+
not `JSON`. If the final `options` are not of type `dict[str,str]`.
|
|
1711
|
+
requests.HTTPError
|
|
1712
|
+
If the response status code is not 2xx.
|
|
1713
|
+
TimeoutError
|
|
1714
|
+
If the run does not succeed after the polling strategy is exhausted
|
|
1715
|
+
based on time duration.
|
|
1716
|
+
RuntimeError
|
|
1717
|
+
If the run does not succeed after the polling strategy is exhausted
|
|
1718
|
+
based on number of tries.
|
|
1306
1719
|
"""
|
|
1307
1720
|
|
|
1308
1721
|
run_id = self.new_run(
|
|
@@ -1451,23 +1864,53 @@ class Application:
|
|
|
1451
1864
|
description: Optional[str] = None,
|
|
1452
1865
|
) -> SecretsCollectionSummary:
|
|
1453
1866
|
"""
|
|
1454
|
-
Create a new secrets collection.
|
|
1455
|
-
|
|
1867
|
+
Create a new secrets collection.
|
|
1868
|
+
|
|
1869
|
+
This method creates a new secrets collection with the provided secrets.
|
|
1870
|
+
A secrets collection is a group of key-value pairs that can be used by
|
|
1871
|
+
your application instances during execution. If no secrets are provided,
|
|
1872
|
+
a ValueError is raised.
|
|
1456
1873
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1874
|
+
Parameters
|
|
1875
|
+
----------
|
|
1876
|
+
secrets : list[Secret]
|
|
1877
|
+
List of secrets to use for the secrets collection. Each secret
|
|
1878
|
+
should be an instance of the Secret class containing a key and value.
|
|
1879
|
+
id : str
|
|
1880
|
+
ID of the secrets collection.
|
|
1881
|
+
name : str
|
|
1882
|
+
Name of the secrets collection.
|
|
1883
|
+
description : Optional[str], default=None
|
|
1884
|
+
Description of the secrets collection.
|
|
1464
1885
|
|
|
1465
|
-
Returns
|
|
1466
|
-
|
|
1886
|
+
Returns
|
|
1887
|
+
-------
|
|
1888
|
+
SecretsCollectionSummary
|
|
1889
|
+
Summary of the secrets collection including its metadata.
|
|
1467
1890
|
|
|
1468
|
-
Raises
|
|
1469
|
-
|
|
1470
|
-
|
|
1891
|
+
Raises
|
|
1892
|
+
------
|
|
1893
|
+
ValueError
|
|
1894
|
+
If no secrets are provided.
|
|
1895
|
+
requests.HTTPError
|
|
1896
|
+
If the response status code is not 2xx.
|
|
1897
|
+
|
|
1898
|
+
Examples
|
|
1899
|
+
--------
|
|
1900
|
+
>>> # Create a new secrets collection with API keys
|
|
1901
|
+
>>> from nextmv.cloud import Secret
|
|
1902
|
+
>>> secrets = [
|
|
1903
|
+
... Secret(key="API_KEY", value="your-api-key"),
|
|
1904
|
+
... Secret(key="DATABASE_URL", value="your-database-url")
|
|
1905
|
+
... ]
|
|
1906
|
+
>>> collection = app.new_secrets_collection(
|
|
1907
|
+
... secrets=secrets,
|
|
1908
|
+
... id="api-secrets",
|
|
1909
|
+
... name="API Secrets",
|
|
1910
|
+
... description="Collection of API secrets for external services"
|
|
1911
|
+
... )
|
|
1912
|
+
>>> print(collection.id)
|
|
1913
|
+
'api-secrets'
|
|
1471
1914
|
"""
|
|
1472
1915
|
|
|
1473
1916
|
if len(secrets) == 0:
|
|
@@ -1502,18 +1945,51 @@ class Application:
|
|
|
1502
1945
|
"""
|
|
1503
1946
|
Create a new version using the current dev binary.
|
|
1504
1947
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
description: Description of the version. Will be generated if not provided.
|
|
1509
|
-
exist_ok: If True and a version with the same ID already exists,
|
|
1510
|
-
return the existing version instead of creating a new one.
|
|
1948
|
+
This method creates a new version of the application using the current development
|
|
1949
|
+
binary. Application versions represent different iterations of your application's
|
|
1950
|
+
code and configuration that can be deployed.
|
|
1511
1951
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1952
|
+
Parameters
|
|
1953
|
+
----------
|
|
1954
|
+
id : Optional[str], default=None
|
|
1955
|
+
ID of the version. If not provided, a unique ID will be generated.
|
|
1956
|
+
name : Optional[str], default=None
|
|
1957
|
+
Name of the version. If not provided, a name will be generated.
|
|
1958
|
+
description : Optional[str], default=None
|
|
1959
|
+
Description of the version. If not provided, a description will be generated.
|
|
1960
|
+
exist_ok : bool, default=False
|
|
1961
|
+
If True and a version with the same ID already exists,
|
|
1962
|
+
return the existing version instead of creating a new one.
|
|
1963
|
+
If True, the 'id' parameter must be provided.
|
|
1514
1964
|
|
|
1515
|
-
|
|
1516
|
-
|
|
1965
|
+
Returns
|
|
1966
|
+
-------
|
|
1967
|
+
Version
|
|
1968
|
+
The newly created (or existing) version.
|
|
1969
|
+
|
|
1970
|
+
Raises
|
|
1971
|
+
------
|
|
1972
|
+
ValueError
|
|
1973
|
+
If exist_ok is True and id is None.
|
|
1974
|
+
requests.HTTPError
|
|
1975
|
+
If the response status code is not 2xx.
|
|
1976
|
+
|
|
1977
|
+
Examples
|
|
1978
|
+
--------
|
|
1979
|
+
>>> # Create a new version
|
|
1980
|
+
>>> version = app.new_version(
|
|
1981
|
+
... id="v1.0.0",
|
|
1982
|
+
... name="Initial Release",
|
|
1983
|
+
... description="First stable version"
|
|
1984
|
+
... )
|
|
1985
|
+
>>> print(version.id)
|
|
1986
|
+
'v1.0.0'
|
|
1987
|
+
|
|
1988
|
+
>>> # Get or create a version with exist_ok
|
|
1989
|
+
>>> version = app.new_version(
|
|
1990
|
+
... id="v1.0.0",
|
|
1991
|
+
... exist_ok=True
|
|
1992
|
+
... )
|
|
1517
1993
|
"""
|
|
1518
1994
|
|
|
1519
1995
|
if exist_ok and id is None:
|
|
@@ -1563,86 +2039,96 @@ class Application:
|
|
|
1563
2039
|
`nextmv.Model`. The model is encoded, some dependencies and
|
|
1564
2040
|
accompanying files are packaged, and the app is pushed to Nextmv Cloud.
|
|
1565
2041
|
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
and model configuration:
|
|
1583
|
-
```python
|
|
1584
|
-
import os
|
|
1585
|
-
|
|
1586
|
-
import nextroute
|
|
1587
|
-
|
|
1588
|
-
import nextmv
|
|
1589
|
-
import nextmv.cloud
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
# Define the model that makes decisions. This model uses the Nextroute
|
|
1593
|
-
# library to solve a vehicle routing problem.
|
|
1594
|
-
class DecisionModel(nextmv.Model):
|
|
1595
|
-
def solve(self, input: nextmv.Input) -> nextmv.Output:
|
|
1596
|
-
nextroute_input = nextroute.schema.Input.from_dict(input.data)
|
|
1597
|
-
nextroute_options = nextroute.Options.extract_from_dict(input.options.to_dict())
|
|
1598
|
-
nextroute_output = nextroute.solve(nextroute_input, nextroute_options)
|
|
1599
|
-
|
|
1600
|
-
return nextmv.Output(
|
|
1601
|
-
options=input.options,
|
|
1602
|
-
solution=nextroute_output.solutions[0].to_dict(),
|
|
1603
|
-
statistics=nextroute_output.statistics.to_dict(),
|
|
1604
|
-
)
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
# Define the options that the model needs.
|
|
1608
|
-
opt = []
|
|
1609
|
-
default_options = nextroute.Options()
|
|
1610
|
-
for name, default_value in default_options.to_dict().items():
|
|
1611
|
-
opt.append(nextmv.Option(name.lower(), type(default_value), default_value, name, False))
|
|
1612
|
-
|
|
1613
|
-
options = nextmv.Options(*opt)
|
|
2042
|
+
Parameters
|
|
2043
|
+
----------
|
|
2044
|
+
manifest : Optional[Manifest], default=None
|
|
2045
|
+
The manifest for the app. If None, an `app.yaml` file in the provided
|
|
2046
|
+
app directory will be used.
|
|
2047
|
+
app_dir : Optional[str], default=None
|
|
2048
|
+
The path to the app's root directory. If None, the current directory
|
|
2049
|
+
will be used. This is for the external strategy approach.
|
|
2050
|
+
verbose : bool, default=False
|
|
2051
|
+
Whether to print verbose output during the push process.
|
|
2052
|
+
model : Optional[Model], default=None
|
|
2053
|
+
The Python-native model to push. Must be specified together with
|
|
2054
|
+
`model_configuration`. This is for the internal strategy approach.
|
|
2055
|
+
model_configuration : Optional[ModelConfiguration], default=None
|
|
2056
|
+
Configuration for the Python-native model. Must be specified together
|
|
2057
|
+
with `model`.
|
|
1614
2058
|
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
name="python_nextroute_model",
|
|
1619
|
-
requirements=[
|
|
1620
|
-
"nextroute==1.8.1",
|
|
1621
|
-
"nextmv==0.14.0.dev1",
|
|
1622
|
-
],
|
|
1623
|
-
options=options,
|
|
1624
|
-
)
|
|
2059
|
+
Returns
|
|
2060
|
+
-------
|
|
2061
|
+
None
|
|
1625
2062
|
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
```
|
|
2063
|
+
Raises
|
|
2064
|
+
------
|
|
2065
|
+
ValueError
|
|
2066
|
+
If neither app_dir nor model/model_configuration is provided correctly,
|
|
2067
|
+
or if only one of model and model_configuration is provided.
|
|
2068
|
+
TypeError
|
|
2069
|
+
If model is not an instance of nextmv.Model or if model_configuration
|
|
2070
|
+
is not an instance of nextmv.ModelConfiguration.
|
|
2071
|
+
Exception
|
|
2072
|
+
If there's an error in the build, packaging, or cleanup process.
|
|
1637
2073
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
2074
|
+
Examples
|
|
2075
|
+
--------
|
|
2076
|
+
1. Push an app using an external strategy (directory-based):
|
|
2077
|
+
|
|
2078
|
+
>>> import os
|
|
2079
|
+
>>> from nextmv import cloud
|
|
2080
|
+
>>> client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
|
|
2081
|
+
>>> app = cloud.Application(client=client, id="<YOUR-APP-ID>")
|
|
2082
|
+
>>> app.push() # Use verbose=True for step-by-step output.
|
|
2083
|
+
|
|
2084
|
+
2. Push an app using an internal strategy (Python-native model):
|
|
2085
|
+
|
|
2086
|
+
>>> import os
|
|
2087
|
+
>>> import nextroute
|
|
2088
|
+
>>> import nextmv
|
|
2089
|
+
>>> import nextmv.cloud
|
|
2090
|
+
>>>
|
|
2091
|
+
>>> # Define the model that makes decisions
|
|
2092
|
+
>>> class DecisionModel(nextmv.Model):
|
|
2093
|
+
... def solve(self, input: nextmv.Input) -> nextmv.Output:
|
|
2094
|
+
... nextroute_input = nextroute.schema.Input.from_dict(input.data)
|
|
2095
|
+
... nextroute_options = nextroute.Options.extract_from_dict(input.options.to_dict())
|
|
2096
|
+
... nextroute_output = nextroute.solve(nextroute_input, nextroute_options)
|
|
2097
|
+
...
|
|
2098
|
+
... return nextmv.Output(
|
|
2099
|
+
... options=input.options,
|
|
2100
|
+
... solution=nextroute_output.solutions[0].to_dict(),
|
|
2101
|
+
... statistics=nextroute_output.statistics.to_dict(),
|
|
2102
|
+
... )
|
|
2103
|
+
>>>
|
|
2104
|
+
>>> # Define the options that the model needs
|
|
2105
|
+
>>> opt = []
|
|
2106
|
+
>>> default_options = nextroute.Options()
|
|
2107
|
+
>>> for name, default_value in default_options.to_dict().items():
|
|
2108
|
+
... opt.append(nextmv.Option(name.lower(), type(default_value), default_value, name, False))
|
|
2109
|
+
>>> options = nextmv.Options(*opt)
|
|
2110
|
+
>>>
|
|
2111
|
+
>>> # Instantiate the model and model configuration
|
|
2112
|
+
>>> model = DecisionModel()
|
|
2113
|
+
>>> model_configuration = nextmv.ModelConfiguration(
|
|
2114
|
+
... name="python_nextroute_model",
|
|
2115
|
+
... requirements=[
|
|
2116
|
+
... "nextroute==1.8.1",
|
|
2117
|
+
... "nextmv==0.14.0.dev1",
|
|
2118
|
+
... ],
|
|
2119
|
+
... options=options,
|
|
2120
|
+
... )
|
|
2121
|
+
>>>
|
|
2122
|
+
>>> # Push the model to Nextmv Cloud
|
|
2123
|
+
>>> client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
|
|
2124
|
+
>>> app = cloud.Application(client=client, id="<YOUR-APP-ID>")
|
|
2125
|
+
>>> manifest = nextmv.cloud.default_python_manifest()
|
|
2126
|
+
>>> app.push(
|
|
2127
|
+
... manifest=manifest,
|
|
2128
|
+
... verbose=True,
|
|
2129
|
+
... model=model,
|
|
2130
|
+
... model_configuration=model_configuration,
|
|
2131
|
+
... )
|
|
1646
2132
|
"""
|
|
1647
2133
|
|
|
1648
2134
|
if verbose:
|
|
@@ -1677,14 +2163,31 @@ class Application:
|
|
|
1677
2163
|
"""
|
|
1678
2164
|
Get the input of a run.
|
|
1679
2165
|
|
|
1680
|
-
|
|
1681
|
-
|
|
2166
|
+
Retrieves the input data that was used for a specific run. This method
|
|
2167
|
+
handles both small and large inputs automatically - if the input size
|
|
2168
|
+
exceeds the maximum allowed size, it will fetch the input from a
|
|
2169
|
+
download URL.
|
|
2170
|
+
|
|
2171
|
+
Parameters
|
|
2172
|
+
----------
|
|
2173
|
+
run_id : str
|
|
2174
|
+
ID of the run to retrieve the input for.
|
|
2175
|
+
|
|
2176
|
+
Returns
|
|
2177
|
+
-------
|
|
2178
|
+
dict[str, Any]
|
|
2179
|
+
Input data of the run as a dictionary.
|
|
1682
2180
|
|
|
1683
|
-
|
|
1684
|
-
|
|
2181
|
+
Raises
|
|
2182
|
+
------
|
|
2183
|
+
requests.HTTPError
|
|
2184
|
+
If the response status code is not 2xx.
|
|
1685
2185
|
|
|
1686
|
-
|
|
1687
|
-
|
|
2186
|
+
Examples
|
|
2187
|
+
--------
|
|
2188
|
+
>>> input_data = app.run_input("run-123")
|
|
2189
|
+
>>> print(input_data)
|
|
2190
|
+
{'locations': [...], 'vehicles': [...]}
|
|
1688
2191
|
"""
|
|
1689
2192
|
run_information = self.run_metadata(run_id=run_id)
|
|
1690
2193
|
|
|
@@ -1713,16 +2216,31 @@ class Application:
|
|
|
1713
2216
|
|
|
1714
2217
|
def run_metadata(self, run_id: str) -> RunInformation:
|
|
1715
2218
|
"""
|
|
1716
|
-
Get the metadata of a run.
|
|
2219
|
+
Get the metadata of a run.
|
|
1717
2220
|
|
|
1718
|
-
|
|
1719
|
-
|
|
2221
|
+
Retrieves information about a run without including the run output.
|
|
2222
|
+
This is useful when you only need the run's status and metadata.
|
|
1720
2223
|
|
|
1721
|
-
|
|
1722
|
-
|
|
2224
|
+
Parameters
|
|
2225
|
+
----------
|
|
2226
|
+
run_id : str
|
|
2227
|
+
ID of the run to retrieve metadata for.
|
|
1723
2228
|
|
|
1724
|
-
|
|
1725
|
-
|
|
2229
|
+
Returns
|
|
2230
|
+
-------
|
|
2231
|
+
RunInformation
|
|
2232
|
+
Metadata of the run (run information without output).
|
|
2233
|
+
|
|
2234
|
+
Raises
|
|
2235
|
+
------
|
|
2236
|
+
requests.HTTPError
|
|
2237
|
+
If the response status code is not 2xx.
|
|
2238
|
+
|
|
2239
|
+
Examples
|
|
2240
|
+
--------
|
|
2241
|
+
>>> metadata = app.run_metadata("run-123")
|
|
2242
|
+
>>> print(metadata.metadata.status_v2)
|
|
2243
|
+
StatusV2.succeeded
|
|
1726
2244
|
"""
|
|
1727
2245
|
|
|
1728
2246
|
response = self.client.request(
|
|
@@ -1739,14 +2257,26 @@ class Application:
|
|
|
1739
2257
|
"""
|
|
1740
2258
|
Get the logs of a run.
|
|
1741
2259
|
|
|
1742
|
-
|
|
1743
|
-
|
|
2260
|
+
Parameters
|
|
2261
|
+
----------
|
|
2262
|
+
run_id : str
|
|
2263
|
+
ID of the run to get logs for.
|
|
1744
2264
|
|
|
1745
|
-
Returns
|
|
2265
|
+
Returns
|
|
2266
|
+
-------
|
|
2267
|
+
RunLog
|
|
1746
2268
|
Logs of the run.
|
|
1747
2269
|
|
|
1748
|
-
Raises
|
|
1749
|
-
|
|
2270
|
+
Raises
|
|
2271
|
+
------
|
|
2272
|
+
requests.HTTPError
|
|
2273
|
+
If the response status code is not 2xx.
|
|
2274
|
+
|
|
2275
|
+
Examples
|
|
2276
|
+
--------
|
|
2277
|
+
>>> logs = app.run_logs("run-123")
|
|
2278
|
+
>>> print(logs.stderr)
|
|
2279
|
+
'Warning: resource usage exceeded'
|
|
1750
2280
|
"""
|
|
1751
2281
|
response = self.client.request(
|
|
1752
2282
|
method="GET",
|
|
@@ -1756,16 +2286,30 @@ class Application:
|
|
|
1756
2286
|
|
|
1757
2287
|
def run_result(self, run_id: str) -> RunResult:
|
|
1758
2288
|
"""
|
|
1759
|
-
Get the result of a run.
|
|
2289
|
+
Get the result of a run.
|
|
1760
2290
|
|
|
1761
|
-
|
|
1762
|
-
run_id: ID of the run.
|
|
2291
|
+
Retrieves the complete result of a run, including the run output.
|
|
1763
2292
|
|
|
1764
|
-
|
|
1765
|
-
|
|
2293
|
+
Parameters
|
|
2294
|
+
----------
|
|
2295
|
+
run_id : str
|
|
2296
|
+
ID of the run to get results for.
|
|
2297
|
+
|
|
2298
|
+
Returns
|
|
2299
|
+
-------
|
|
2300
|
+
RunResult
|
|
2301
|
+
Result of the run, including output.
|
|
2302
|
+
|
|
2303
|
+
Raises
|
|
2304
|
+
------
|
|
2305
|
+
requests.HTTPError
|
|
2306
|
+
If the response status code is not 2xx.
|
|
1766
2307
|
|
|
1767
|
-
|
|
1768
|
-
|
|
2308
|
+
Examples
|
|
2309
|
+
--------
|
|
2310
|
+
>>> result = app.run_result("run-123")
|
|
2311
|
+
>>> print(result.metadata.status_v2)
|
|
2312
|
+
'succeeded'
|
|
1769
2313
|
"""
|
|
1770
2314
|
|
|
1771
2315
|
run_information = self.run_metadata(run_id=run_id)
|
|
@@ -1778,19 +2322,44 @@ class Application:
|
|
|
1778
2322
|
polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
|
|
1779
2323
|
) -> RunResult:
|
|
1780
2324
|
"""
|
|
1781
|
-
Get the result of a run
|
|
1782
|
-
method polls for the result until the run finishes executing or the
|
|
1783
|
-
polling strategy is exhausted.
|
|
2325
|
+
Get the result of a run with polling.
|
|
1784
2326
|
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2327
|
+
Retrieves the result of a run including the run output. This method polls
|
|
2328
|
+
for the result until the run finishes executing or the polling strategy
|
|
2329
|
+
is exhausted.
|
|
1788
2330
|
|
|
1789
|
-
|
|
1790
|
-
|
|
2331
|
+
Parameters
|
|
2332
|
+
----------
|
|
2333
|
+
run_id : str
|
|
2334
|
+
ID of the run to retrieve the result for.
|
|
2335
|
+
polling_options : PollingOptions, default=_DEFAULT_POLLING_OPTIONS
|
|
2336
|
+
Options to use when polling for the run result.
|
|
2337
|
+
|
|
2338
|
+
Returns
|
|
2339
|
+
-------
|
|
2340
|
+
RunResult
|
|
2341
|
+
Complete result of the run including output data.
|
|
2342
|
+
|
|
2343
|
+
Raises
|
|
2344
|
+
------
|
|
2345
|
+
requests.HTTPError
|
|
2346
|
+
If the response status code is not 2xx.
|
|
2347
|
+
TimeoutError
|
|
2348
|
+
If the run does not complete after the polling strategy is
|
|
2349
|
+
exhausted based on time duration.
|
|
2350
|
+
RuntimeError
|
|
2351
|
+
If the run does not complete after the polling strategy is
|
|
2352
|
+
exhausted based on number of tries.
|
|
1791
2353
|
|
|
1792
|
-
|
|
1793
|
-
|
|
2354
|
+
Examples
|
|
2355
|
+
--------
|
|
2356
|
+
>>> from nextmv.cloud import PollingOptions
|
|
2357
|
+
>>> # Create custom polling options
|
|
2358
|
+
>>> polling_opts = PollingOptions(max_tries=50, max_duration=600)
|
|
2359
|
+
>>> # Get run result with polling
|
|
2360
|
+
>>> result = app.run_result_with_polling("run-123", polling_opts)
|
|
2361
|
+
>>> print(result.output)
|
|
2362
|
+
{'solution': {...}}
|
|
1794
2363
|
"""
|
|
1795
2364
|
|
|
1796
2365
|
def polling_func() -> tuple[Any, bool]:
|
|
@@ -1810,24 +2379,34 @@ class Application:
|
|
|
1810
2379
|
|
|
1811
2380
|
def scenario_test(self, scenario_test_id: str) -> BatchExperiment:
|
|
1812
2381
|
"""
|
|
1813
|
-
Get
|
|
1814
|
-
|
|
1815
|
-
|
|
2382
|
+
Get a scenario test.
|
|
2383
|
+
|
|
2384
|
+
Retrieves a scenario test by ID. Scenario tests are based on batch experiments,
|
|
2385
|
+
so this function returns the corresponding batch experiment associated with
|
|
2386
|
+
the scenario test.
|
|
1816
2387
|
|
|
1817
2388
|
Parameters
|
|
1818
2389
|
----------
|
|
1819
2390
|
scenario_test_id : str
|
|
1820
|
-
ID of the scenario test.
|
|
2391
|
+
ID of the scenario test to retrieve.
|
|
1821
2392
|
|
|
1822
2393
|
Returns
|
|
1823
2394
|
-------
|
|
1824
2395
|
BatchExperiment
|
|
1825
|
-
The scenario test.
|
|
2396
|
+
The scenario test details as a batch experiment.
|
|
1826
2397
|
|
|
1827
2398
|
Raises
|
|
1828
2399
|
------
|
|
1829
2400
|
requests.HTTPError
|
|
1830
2401
|
If the response status code is not 2xx.
|
|
2402
|
+
|
|
2403
|
+
Examples
|
|
2404
|
+
--------
|
|
2405
|
+
>>> test = app.scenario_test("scenario-123")
|
|
2406
|
+
>>> print(test.name)
|
|
2407
|
+
'My Scenario Test'
|
|
2408
|
+
>>> print(test.type)
|
|
2409
|
+
'scenario'
|
|
1831
2410
|
"""
|
|
1832
2411
|
|
|
1833
2412
|
return self.batch_experiment(batch_id=scenario_test_id)
|
|
@@ -1845,7 +2424,7 @@ class Application:
|
|
|
1845
2424
|
----------
|
|
1846
2425
|
tracked_run : TrackedRun
|
|
1847
2426
|
The run to track.
|
|
1848
|
-
instance_id: Optional[str]
|
|
2427
|
+
instance_id : Optional[str], default=None
|
|
1849
2428
|
Optional instance ID if you want to associate your tracked run with
|
|
1850
2429
|
an instance.
|
|
1851
2430
|
|
|
@@ -1860,7 +2439,16 @@ class Application:
|
|
|
1860
2439
|
If the response status code is not 2xx.
|
|
1861
2440
|
ValueError
|
|
1862
2441
|
If the tracked run does not have an input or output.
|
|
2442
|
+
|
|
2443
|
+
Examples
|
|
2444
|
+
--------
|
|
2445
|
+
>>> from nextmv.cloud import Application
|
|
2446
|
+
>>> from nextmv.cloud.run import TrackedRun
|
|
2447
|
+
>>> app = Application(id="app_123")
|
|
2448
|
+
>>> tracked_run = TrackedRun(input={"data": [...]}, output={"solution": [...]})
|
|
2449
|
+
>>> run_id = app.track_run(tracked_run)
|
|
1863
2450
|
"""
|
|
2451
|
+
|
|
1864
2452
|
url_input = self.upload_url()
|
|
1865
2453
|
|
|
1866
2454
|
upload_input = tracked_run.input
|
|
@@ -1955,18 +2543,28 @@ class Application:
|
|
|
1955
2543
|
"""
|
|
1956
2544
|
Update an instance.
|
|
1957
2545
|
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2546
|
+
Parameters
|
|
2547
|
+
----------
|
|
2548
|
+
id : str
|
|
2549
|
+
ID of the instance to update.
|
|
2550
|
+
name : str
|
|
2551
|
+
Name of the instance.
|
|
2552
|
+
version_id : Optional[str], default=None
|
|
2553
|
+
ID of the version to associate the instance with.
|
|
2554
|
+
description : Optional[str], default=None
|
|
2555
|
+
Description of the instance.
|
|
2556
|
+
configuration : Optional[InstanceConfiguration], default=None
|
|
2557
|
+
Configuration to use for the instance.
|
|
1964
2558
|
|
|
1965
|
-
Returns
|
|
1966
|
-
|
|
2559
|
+
Returns
|
|
2560
|
+
-------
|
|
2561
|
+
Instance
|
|
2562
|
+
The updated instance.
|
|
1967
2563
|
|
|
1968
|
-
Raises
|
|
1969
|
-
|
|
2564
|
+
Raises
|
|
2565
|
+
------
|
|
2566
|
+
requests.HTTPError
|
|
2567
|
+
If the response status code is not 2xx.
|
|
1970
2568
|
"""
|
|
1971
2569
|
|
|
1972
2570
|
payload = {}
|
|
@@ -2075,28 +2673,40 @@ class Application:
|
|
|
2075
2673
|
description: str,
|
|
2076
2674
|
) -> BatchExperimentInformation:
|
|
2077
2675
|
"""
|
|
2078
|
-
Update a scenario test.
|
|
2079
|
-
|
|
2080
|
-
|
|
2676
|
+
Update a scenario test.
|
|
2677
|
+
|
|
2678
|
+
Updates a scenario test with new name and description. Scenario tests
|
|
2679
|
+
use the batch experiments API, so this method calls the
|
|
2680
|
+
`update_batch_experiment` method, and thus the return type is the same.
|
|
2081
2681
|
|
|
2082
2682
|
Parameters
|
|
2083
2683
|
----------
|
|
2084
2684
|
scenario_test_id : str
|
|
2085
2685
|
ID of the scenario test to update.
|
|
2086
2686
|
name : str
|
|
2087
|
-
|
|
2687
|
+
New name for the scenario test.
|
|
2088
2688
|
description : str
|
|
2089
|
-
|
|
2689
|
+
New description for the scenario test.
|
|
2090
2690
|
|
|
2091
2691
|
Returns
|
|
2092
2692
|
-------
|
|
2093
2693
|
BatchExperimentInformation
|
|
2094
|
-
The information
|
|
2694
|
+
The information about the updated scenario test.
|
|
2095
2695
|
|
|
2096
2696
|
Raises
|
|
2097
2697
|
------
|
|
2098
2698
|
requests.HTTPError
|
|
2099
2699
|
If the response status code is not 2xx.
|
|
2700
|
+
|
|
2701
|
+
Examples
|
|
2702
|
+
--------
|
|
2703
|
+
>>> info = app.update_scenario_test(
|
|
2704
|
+
... scenario_test_id="scenario-123",
|
|
2705
|
+
... name="Updated Test Name",
|
|
2706
|
+
... description="Updated description for this test"
|
|
2707
|
+
... )
|
|
2708
|
+
>>> print(info.name)
|
|
2709
|
+
'Updated Test Name'
|
|
2100
2710
|
"""
|
|
2101
2711
|
|
|
2102
2712
|
return self.update_batch_experiment(
|
|
@@ -2115,18 +2725,50 @@ class Application:
|
|
|
2115
2725
|
"""
|
|
2116
2726
|
Update a secrets collection.
|
|
2117
2727
|
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
description: Description of the secrets collection.
|
|
2122
|
-
secrets: List of secrets to update.
|
|
2728
|
+
This method updates an existing secrets collection with new values for name,
|
|
2729
|
+
description, and secrets. A secrets collection is a group of key-value pairs
|
|
2730
|
+
that can be used by your application instances during execution.
|
|
2123
2731
|
|
|
2124
|
-
|
|
2125
|
-
|
|
2732
|
+
Parameters
|
|
2733
|
+
----------
|
|
2734
|
+
secrets_collection_id : str
|
|
2735
|
+
ID of the secrets collection to update.
|
|
2736
|
+
name : str
|
|
2737
|
+
New name for the secrets collection.
|
|
2738
|
+
description : str
|
|
2739
|
+
New description for the secrets collection.
|
|
2740
|
+
secrets : list[Secret]
|
|
2741
|
+
List of secrets to update. Each secret should be an instance of the
|
|
2742
|
+
Secret class containing a key and value.
|
|
2743
|
+
|
|
2744
|
+
Returns
|
|
2745
|
+
-------
|
|
2746
|
+
SecretsCollectionSummary
|
|
2747
|
+
Summary of the updated secrets collection including its metadata.
|
|
2126
2748
|
|
|
2127
|
-
Raises
|
|
2128
|
-
|
|
2129
|
-
|
|
2749
|
+
Raises
|
|
2750
|
+
------
|
|
2751
|
+
ValueError
|
|
2752
|
+
If no secrets are provided.
|
|
2753
|
+
requests.HTTPError
|
|
2754
|
+
If the response status code is not 2xx.
|
|
2755
|
+
|
|
2756
|
+
Examples
|
|
2757
|
+
--------
|
|
2758
|
+
>>> # Update an existing secrets collection
|
|
2759
|
+
>>> from nextmv.cloud import Secret
|
|
2760
|
+
>>> updated_secrets = [
|
|
2761
|
+
... Secret(key="API_KEY", value="new-api-key"),
|
|
2762
|
+
... Secret(key="DATABASE_URL", value="new-database-url")
|
|
2763
|
+
... ]
|
|
2764
|
+
>>> updated_collection = app.update_secrets_collection(
|
|
2765
|
+
... secrets_collection_id="api-secrets",
|
|
2766
|
+
... name="Updated API Secrets",
|
|
2767
|
+
... description="Updated collection of API secrets",
|
|
2768
|
+
... secrets=updated_secrets
|
|
2769
|
+
... )
|
|
2770
|
+
>>> print(updated_collection.id)
|
|
2771
|
+
'api-secrets'
|
|
2130
2772
|
"""
|
|
2131
2773
|
|
|
2132
2774
|
if len(secrets) == 0:
|
|
@@ -2151,14 +2793,40 @@ class Application:
|
|
|
2151
2793
|
upload_url: UploadURL,
|
|
2152
2794
|
) -> None:
|
|
2153
2795
|
"""
|
|
2154
|
-
Upload
|
|
2796
|
+
Upload large input data to the provided upload URL.
|
|
2797
|
+
|
|
2798
|
+
This method allows uploading large input data (either a dictionary or string)
|
|
2799
|
+
to a pre-signed URL. If the input is a dictionary, it will be converted to
|
|
2800
|
+
a JSON string before upload.
|
|
2801
|
+
|
|
2802
|
+
Parameters
|
|
2803
|
+
----------
|
|
2804
|
+
input : Union[dict[str, Any], str]
|
|
2805
|
+
Input data to upload. Can be either a dictionary that will be
|
|
2806
|
+
converted to JSON, or a pre-formatted JSON string.
|
|
2807
|
+
upload_url : UploadURL
|
|
2808
|
+
Upload URL object containing the pre-signed URL to use for uploading.
|
|
2809
|
+
|
|
2810
|
+
Returns
|
|
2811
|
+
-------
|
|
2812
|
+
None
|
|
2813
|
+
This method doesn't return anything.
|
|
2155
2814
|
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2815
|
+
Raises
|
|
2816
|
+
------
|
|
2817
|
+
requests.HTTPError
|
|
2818
|
+
If the response status code is not 2xx.
|
|
2159
2819
|
|
|
2160
|
-
|
|
2161
|
-
|
|
2820
|
+
Examples
|
|
2821
|
+
--------
|
|
2822
|
+
>>> # Upload a dictionary as JSON
|
|
2823
|
+
>>> data = {"locations": [...], "vehicles": [...]}
|
|
2824
|
+
>>> url = app.upload_url()
|
|
2825
|
+
>>> app.upload_large_input(input=data, upload_url=url)
|
|
2826
|
+
>>>
|
|
2827
|
+
>>> # Upload a pre-formatted JSON string
|
|
2828
|
+
>>> json_str = '{"locations": [...], "vehicles": [...]}'
|
|
2829
|
+
>>> app.upload_large_input(input=json_str, upload_url=url)
|
|
2162
2830
|
"""
|
|
2163
2831
|
|
|
2164
2832
|
if isinstance(input, dict):
|
|
@@ -2173,11 +2841,27 @@ class Application:
|
|
|
2173
2841
|
"""
|
|
2174
2842
|
Get an upload URL to use for uploading a file.
|
|
2175
2843
|
|
|
2176
|
-
|
|
2177
|
-
|
|
2844
|
+
This method generates a pre-signed URL that can be used to upload large files
|
|
2845
|
+
to Nextmv Cloud. It's primarily used for uploading large input data, output
|
|
2846
|
+
results, or log files that exceed the size limits for direct API calls.
|
|
2847
|
+
|
|
2848
|
+
Returns
|
|
2849
|
+
-------
|
|
2850
|
+
UploadURL
|
|
2851
|
+
An object containing both the upload URL and an upload ID for reference.
|
|
2852
|
+
The upload URL is a pre-signed URL that allows temporary write access.
|
|
2853
|
+
|
|
2854
|
+
Raises
|
|
2855
|
+
------
|
|
2856
|
+
requests.HTTPError
|
|
2857
|
+
If the response status code is not 2xx.
|
|
2178
2858
|
|
|
2179
|
-
|
|
2180
|
-
|
|
2859
|
+
Examples
|
|
2860
|
+
--------
|
|
2861
|
+
>>> # Get an upload URL and upload large input data
|
|
2862
|
+
>>> upload_url = app.upload_url()
|
|
2863
|
+
>>> large_input = {"locations": [...], "vehicles": [...]}
|
|
2864
|
+
>>> app.upload_large_input(input=large_input, upload_url=upload_url)
|
|
2181
2865
|
"""
|
|
2182
2866
|
|
|
2183
2867
|
response = self.client.request(
|
|
@@ -2191,14 +2875,38 @@ class Application:
|
|
|
2191
2875
|
"""
|
|
2192
2876
|
Get a secrets collection.
|
|
2193
2877
|
|
|
2194
|
-
|
|
2195
|
-
|
|
2878
|
+
This method retrieves a secrets collection by its ID. A secrets collection
|
|
2879
|
+
is a group of key-value pairs that can be used by your application
|
|
2880
|
+
instances during execution.
|
|
2881
|
+
|
|
2882
|
+
Parameters
|
|
2883
|
+
----------
|
|
2884
|
+
secrets_collection_id : str
|
|
2885
|
+
ID of the secrets collection to retrieve.
|
|
2196
2886
|
|
|
2197
|
-
Returns
|
|
2198
|
-
|
|
2887
|
+
Returns
|
|
2888
|
+
-------
|
|
2889
|
+
SecretsCollection
|
|
2890
|
+
The requested secrets collection, including all secret values
|
|
2891
|
+
and metadata.
|
|
2199
2892
|
|
|
2200
|
-
Raises
|
|
2201
|
-
|
|
2893
|
+
Raises
|
|
2894
|
+
------
|
|
2895
|
+
requests.HTTPError
|
|
2896
|
+
If the response status code is not 2xx.
|
|
2897
|
+
|
|
2898
|
+
Examples
|
|
2899
|
+
--------
|
|
2900
|
+
>>> # Retrieve a secrets collection
|
|
2901
|
+
>>> collection = app.secrets_collection("api-secrets")
|
|
2902
|
+
>>> print(collection.name)
|
|
2903
|
+
'API Secrets'
|
|
2904
|
+
>>> print(len(collection.secrets))
|
|
2905
|
+
2
|
|
2906
|
+
>>> for secret in collection.secrets:
|
|
2907
|
+
... print(secret.location)
|
|
2908
|
+
'API_KEY'
|
|
2909
|
+
'DATABASE_URL'
|
|
2202
2910
|
"""
|
|
2203
2911
|
|
|
2204
2912
|
response = self.client.request(
|
|
@@ -2212,14 +2920,32 @@ class Application:
|
|
|
2212
2920
|
"""
|
|
2213
2921
|
Get a version.
|
|
2214
2922
|
|
|
2215
|
-
|
|
2216
|
-
|
|
2923
|
+
Retrieves a specific version of the application by its ID. Application versions
|
|
2924
|
+
represent different iterations of your application's code and configuration.
|
|
2217
2925
|
|
|
2218
|
-
|
|
2219
|
-
|
|
2926
|
+
Parameters
|
|
2927
|
+
----------
|
|
2928
|
+
version_id : str
|
|
2929
|
+
ID of the version to retrieve.
|
|
2220
2930
|
|
|
2221
|
-
|
|
2222
|
-
|
|
2931
|
+
Returns
|
|
2932
|
+
-------
|
|
2933
|
+
Version
|
|
2934
|
+
The version object containing details about the requested application version.
|
|
2935
|
+
|
|
2936
|
+
Raises
|
|
2937
|
+
------
|
|
2938
|
+
requests.HTTPError
|
|
2939
|
+
If the response status code is not 2xx.
|
|
2940
|
+
|
|
2941
|
+
Examples
|
|
2942
|
+
--------
|
|
2943
|
+
>>> # Retrieve a specific version
|
|
2944
|
+
>>> version = app.version("v1.0.0")
|
|
2945
|
+
>>> print(version.id)
|
|
2946
|
+
'v1.0.0'
|
|
2947
|
+
>>> print(version.name)
|
|
2948
|
+
'Initial Release'
|
|
2223
2949
|
"""
|
|
2224
2950
|
|
|
2225
2951
|
response = self.client.request(
|
|
@@ -2233,11 +2959,34 @@ class Application:
|
|
|
2233
2959
|
"""
|
|
2234
2960
|
Check if a version exists.
|
|
2235
2961
|
|
|
2236
|
-
|
|
2237
|
-
|
|
2962
|
+
This method checks if a specific version of the application exists by
|
|
2963
|
+
attempting to retrieve it. It handles HTTP errors for non-existent versions
|
|
2964
|
+
and returns a boolean indicating existence.
|
|
2965
|
+
|
|
2966
|
+
Parameters
|
|
2967
|
+
----------
|
|
2968
|
+
version_id : str
|
|
2969
|
+
ID of the version to check for existence.
|
|
2970
|
+
|
|
2971
|
+
Returns
|
|
2972
|
+
-------
|
|
2973
|
+
bool
|
|
2974
|
+
True if the version exists, False otherwise.
|
|
2238
2975
|
|
|
2239
|
-
|
|
2240
|
-
|
|
2976
|
+
Raises
|
|
2977
|
+
------
|
|
2978
|
+
requests.HTTPError
|
|
2979
|
+
If an HTTP error occurs that is not related to the non-existence
|
|
2980
|
+
of the version.
|
|
2981
|
+
|
|
2982
|
+
Examples
|
|
2983
|
+
--------
|
|
2984
|
+
>>> # Check if a version exists
|
|
2985
|
+
>>> exists = app.version_exists("v1.0.0")
|
|
2986
|
+
>>> if exists:
|
|
2987
|
+
... print("Version exists!")
|
|
2988
|
+
... else:
|
|
2989
|
+
... print("Version does not exist.")
|
|
2241
2990
|
"""
|
|
2242
2991
|
|
|
2243
2992
|
try:
|
|
@@ -2254,19 +3003,38 @@ class Application:
|
|
|
2254
3003
|
run_information: RunInformation,
|
|
2255
3004
|
) -> RunResult:
|
|
2256
3005
|
"""
|
|
2257
|
-
Get the result of a run.
|
|
2258
|
-
private method that is the base for retrieving a run result, regardless
|
|
2259
|
-
of polling.
|
|
3006
|
+
Get the result of a run.
|
|
2260
3007
|
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
3008
|
+
This is a private method that retrieves the complete result of a run,
|
|
3009
|
+
including the output data. It handles both small and large outputs,
|
|
3010
|
+
automatically using the appropriate API endpoints based on the output
|
|
3011
|
+
size. This method serves as the base implementation for retrieving
|
|
3012
|
+
run results, regardless of polling strategy.
|
|
2264
3013
|
|
|
2265
|
-
|
|
2266
|
-
|
|
3014
|
+
Parameters
|
|
3015
|
+
----------
|
|
3016
|
+
run_id : str
|
|
3017
|
+
ID of the run to retrieve the result for.
|
|
3018
|
+
run_information : RunInformation
|
|
3019
|
+
Information about the run, including metadata such as output size.
|
|
3020
|
+
|
|
3021
|
+
Returns
|
|
3022
|
+
-------
|
|
3023
|
+
RunResult
|
|
3024
|
+
Result of the run, including all metadata and output data.
|
|
3025
|
+
For large outputs, the method will fetch the output from
|
|
3026
|
+
a download URL.
|
|
3027
|
+
|
|
3028
|
+
Raises
|
|
3029
|
+
------
|
|
3030
|
+
requests.HTTPError
|
|
3031
|
+
If the response status code is not 2xx.
|
|
2267
3032
|
|
|
2268
|
-
|
|
2269
|
-
|
|
3033
|
+
Notes
|
|
3034
|
+
-----
|
|
3035
|
+
This method automatically handles large outputs by checking if the
|
|
3036
|
+
output size exceeds _MAX_RUN_SIZE. If it does, the method will request
|
|
3037
|
+
a download URL and fetch the output data separately.
|
|
2270
3038
|
"""
|
|
2271
3039
|
query_params = None
|
|
2272
3040
|
large_output = False
|
|
@@ -2364,7 +3132,7 @@ class Application:
|
|
|
2364
3132
|
# If working with a list of managed inputs, we need to create an
|
|
2365
3133
|
# input set.
|
|
2366
3134
|
if scenario.scenario_input.scenario_input_type == ScenarioInputType.INPUT:
|
|
2367
|
-
name, id =
|
|
3135
|
+
name, id = _name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
2368
3136
|
input_set = self.new_input_set(
|
|
2369
3137
|
id=id,
|
|
2370
3138
|
name=name,
|
|
@@ -2384,7 +3152,7 @@ class Application:
|
|
|
2384
3152
|
for data in scenario.scenario_input.scenario_input_data:
|
|
2385
3153
|
upload_url = self.upload_url()
|
|
2386
3154
|
self.upload_large_input(input=data, upload_url=upload_url)
|
|
2387
|
-
name, id =
|
|
3155
|
+
name, id = _name_and_id(prefix="man-input", entity_id=scenario_id)
|
|
2388
3156
|
managed_input = self.new_managed_input(
|
|
2389
3157
|
id=id,
|
|
2390
3158
|
name=name,
|
|
@@ -2393,7 +3161,7 @@ class Application:
|
|
|
2393
3161
|
)
|
|
2394
3162
|
managed_inputs.append(managed_input)
|
|
2395
3163
|
|
|
2396
|
-
name, id =
|
|
3164
|
+
name, id = _name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
2397
3165
|
input_set = self.new_input_set(
|
|
2398
3166
|
id=id,
|
|
2399
3167
|
name=name,
|
|
@@ -2408,28 +3176,71 @@ class Application:
|
|
|
2408
3176
|
|
|
2409
3177
|
def poll(polling_options: PollingOptions, polling_func: Callable[[], tuple[Any, bool]]) -> Any:
|
|
2410
3178
|
"""
|
|
2411
|
-
|
|
3179
|
+
Poll a function until it succeeds or the polling strategy is exhausted.
|
|
3180
|
+
|
|
3181
|
+
You can import the `poll` function directly from `cloud`:
|
|
3182
|
+
|
|
3183
|
+
```python
|
|
3184
|
+
from nextmv.cloud import poll
|
|
3185
|
+
```
|
|
3186
|
+
|
|
3187
|
+
This function implements a flexible polling strategy with exponential backoff
|
|
3188
|
+
and jitter. It calls the provided polling function repeatedly until it indicates
|
|
3189
|
+
success, the maximum number of tries is reached, or the maximum duration is exceeded.
|
|
2412
3190
|
|
|
2413
3191
|
The `polling_func` is a callable that must return a `tuple[Any, bool]`
|
|
2414
3192
|
where the first element is the result of the polling and the second
|
|
2415
3193
|
element is a boolean indicating if the polling was successful or should be
|
|
2416
3194
|
retried.
|
|
2417
3195
|
|
|
2418
|
-
This function will return the result of the `polling_func` if the polling
|
|
2419
|
-
process is successful, otherwise it will raise a `TimeoutError` or
|
|
2420
|
-
`RuntimeError` depending on the situation.
|
|
2421
|
-
|
|
2422
3196
|
Parameters
|
|
2423
3197
|
----------
|
|
2424
3198
|
polling_options : PollingOptions
|
|
2425
|
-
Options for the polling
|
|
3199
|
+
Options for configuring the polling behavior, including retry counts,
|
|
3200
|
+
delays, timeouts, and verbosity settings.
|
|
2426
3201
|
polling_func : callable
|
|
2427
|
-
Function to call to check if the polling was successful.
|
|
3202
|
+
Function to call to check if the polling was successful. Must return a tuple
|
|
3203
|
+
where the first element is the result value and the second is a boolean
|
|
3204
|
+
indicating success (True) or need to retry (False).
|
|
2428
3205
|
|
|
2429
3206
|
Returns
|
|
2430
3207
|
-------
|
|
2431
3208
|
Any
|
|
2432
|
-
Result
|
|
3209
|
+
Result value from the polling function when successful.
|
|
3210
|
+
|
|
3211
|
+
Raises
|
|
3212
|
+
------
|
|
3213
|
+
TimeoutError
|
|
3214
|
+
If the polling exceeds the maximum duration specified in polling_options.
|
|
3215
|
+
RuntimeError
|
|
3216
|
+
If the maximum number of tries is exhausted without success.
|
|
3217
|
+
|
|
3218
|
+
Examples
|
|
3219
|
+
--------
|
|
3220
|
+
>>> from nextmv.cloud import PollingOptions, poll
|
|
3221
|
+
>>> import time
|
|
3222
|
+
>>>
|
|
3223
|
+
>>> # Define a polling function that succeeds after 3 tries
|
|
3224
|
+
>>> counter = 0
|
|
3225
|
+
>>> def check_completion() -> tuple[str, bool]:
|
|
3226
|
+
... global counter
|
|
3227
|
+
... counter += 1
|
|
3228
|
+
... if counter >= 3:
|
|
3229
|
+
... return "Success", True
|
|
3230
|
+
... return None, False
|
|
3231
|
+
...
|
|
3232
|
+
>>> # Configure polling options
|
|
3233
|
+
>>> options = PollingOptions(
|
|
3234
|
+
... max_tries=5,
|
|
3235
|
+
... delay=0.1,
|
|
3236
|
+
... backoff=0.2,
|
|
3237
|
+
... verbose=True
|
|
3238
|
+
... )
|
|
3239
|
+
>>>
|
|
3240
|
+
>>> # Poll until the function succeeds
|
|
3241
|
+
>>> result = poll(options, check_completion)
|
|
3242
|
+
>>> print(result)
|
|
3243
|
+
'Success'
|
|
2433
3244
|
"""
|
|
2434
3245
|
|
|
2435
3246
|
# Start by sleeping for the duration specified as initial delay.
|
|
@@ -2493,11 +3304,31 @@ def _is_not_exist_error(e: requests.HTTPError) -> bool:
|
|
|
2493
3304
|
"""
|
|
2494
3305
|
Check if the error is a known 404 Not Found error.
|
|
2495
3306
|
|
|
2496
|
-
|
|
2497
|
-
|
|
3307
|
+
This is an internal helper function that examines HTTPError objects to determine
|
|
3308
|
+
if they represent a "Not Found" (404) condition, either directly or through a
|
|
3309
|
+
nested exception.
|
|
3310
|
+
|
|
3311
|
+
Parameters
|
|
3312
|
+
----------
|
|
3313
|
+
e : requests.HTTPError
|
|
3314
|
+
The HTTP error to check.
|
|
2498
3315
|
|
|
2499
|
-
Returns
|
|
3316
|
+
Returns
|
|
3317
|
+
-------
|
|
3318
|
+
bool
|
|
2500
3319
|
True if the error is a 404 Not Found error, False otherwise.
|
|
3320
|
+
|
|
3321
|
+
Examples
|
|
3322
|
+
--------
|
|
3323
|
+
>>> try:
|
|
3324
|
+
... response = requests.get('https://api.example.com/nonexistent')
|
|
3325
|
+
... response.raise_for_status()
|
|
3326
|
+
... except requests.HTTPError as err:
|
|
3327
|
+
... if _is_not_exist_error(err):
|
|
3328
|
+
... print("Resource does not exist")
|
|
3329
|
+
... else:
|
|
3330
|
+
... print("Another error occurred")
|
|
3331
|
+
Resource does not exist
|
|
2501
3332
|
"""
|
|
2502
3333
|
if (
|
|
2503
3334
|
# Check whether the error is caused by a 404 status code - meaning the app does not exist.
|