nextmv 0.40.0__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nextmv/__about__.py +1 -1
- nextmv/__entrypoint__.py +1 -2
- nextmv/__init__.py +2 -4
- nextmv/cli/CONTRIBUTING.md +583 -0
- nextmv/cli/cloud/__init__.py +49 -0
- nextmv/cli/cloud/acceptance/__init__.py +27 -0
- nextmv/cli/cloud/acceptance/create.py +391 -0
- nextmv/cli/cloud/acceptance/delete.py +64 -0
- nextmv/cli/cloud/acceptance/get.py +103 -0
- nextmv/cli/cloud/acceptance/list.py +62 -0
- nextmv/cli/cloud/acceptance/update.py +95 -0
- nextmv/cli/cloud/account/__init__.py +28 -0
- nextmv/cli/cloud/account/create.py +83 -0
- nextmv/cli/cloud/account/delete.py +59 -0
- nextmv/cli/cloud/account/get.py +66 -0
- nextmv/cli/cloud/account/update.py +70 -0
- nextmv/cli/cloud/app/__init__.py +35 -0
- nextmv/cli/cloud/app/create.py +140 -0
- nextmv/cli/cloud/app/delete.py +57 -0
- nextmv/cli/cloud/app/exists.py +44 -0
- nextmv/cli/cloud/app/get.py +66 -0
- nextmv/cli/cloud/app/list.py +61 -0
- nextmv/cli/cloud/app/push.py +432 -0
- nextmv/cli/cloud/app/update.py +124 -0
- nextmv/cli/cloud/batch/__init__.py +29 -0
- nextmv/cli/cloud/batch/create.py +452 -0
- nextmv/cli/cloud/batch/delete.py +64 -0
- nextmv/cli/cloud/batch/get.py +104 -0
- nextmv/cli/cloud/batch/list.py +63 -0
- nextmv/cli/cloud/batch/metadata.py +66 -0
- nextmv/cli/cloud/batch/update.py +95 -0
- nextmv/cli/cloud/data/__init__.py +26 -0
- nextmv/cli/cloud/data/upload.py +162 -0
- nextmv/cli/cloud/ensemble/__init__.py +33 -0
- nextmv/cli/cloud/ensemble/create.py +413 -0
- nextmv/cli/cloud/ensemble/delete.py +63 -0
- nextmv/cli/cloud/ensemble/get.py +65 -0
- nextmv/cli/cloud/ensemble/list.py +63 -0
- nextmv/cli/cloud/ensemble/update.py +103 -0
- nextmv/cli/cloud/input_set/__init__.py +32 -0
- nextmv/cli/cloud/input_set/create.py +168 -0
- nextmv/cli/cloud/input_set/delete.py +64 -0
- nextmv/cli/cloud/input_set/get.py +63 -0
- nextmv/cli/cloud/input_set/list.py +63 -0
- nextmv/cli/cloud/input_set/update.py +123 -0
- nextmv/cli/cloud/instance/__init__.py +35 -0
- nextmv/cli/cloud/instance/create.py +289 -0
- nextmv/cli/cloud/instance/delete.py +61 -0
- nextmv/cli/cloud/instance/exists.py +39 -0
- nextmv/cli/cloud/instance/get.py +62 -0
- nextmv/cli/cloud/instance/list.py +60 -0
- nextmv/cli/cloud/instance/update.py +216 -0
- nextmv/cli/cloud/managed_input/__init__.py +31 -0
- nextmv/cli/cloud/managed_input/create.py +144 -0
- nextmv/cli/cloud/managed_input/delete.py +64 -0
- nextmv/cli/cloud/managed_input/get.py +63 -0
- nextmv/cli/cloud/managed_input/list.py +60 -0
- nextmv/cli/cloud/managed_input/update.py +97 -0
- nextmv/cli/cloud/run/__init__.py +37 -0
- nextmv/cli/cloud/run/cancel.py +37 -0
- nextmv/cli/cloud/run/create.py +524 -0
- nextmv/cli/cloud/run/get.py +199 -0
- nextmv/cli/cloud/run/input.py +86 -0
- nextmv/cli/cloud/run/list.py +80 -0
- nextmv/cli/cloud/run/logs.py +166 -0
- nextmv/cli/cloud/run/metadata.py +67 -0
- nextmv/cli/cloud/run/track.py +500 -0
- nextmv/cli/cloud/scenario/__init__.py +29 -0
- nextmv/cli/cloud/scenario/create.py +451 -0
- nextmv/cli/cloud/scenario/delete.py +61 -0
- nextmv/cli/cloud/scenario/get.py +102 -0
- nextmv/cli/cloud/scenario/list.py +63 -0
- nextmv/cli/cloud/scenario/metadata.py +67 -0
- nextmv/cli/cloud/scenario/update.py +93 -0
- nextmv/cli/cloud/secrets/__init__.py +33 -0
- nextmv/cli/cloud/secrets/create.py +206 -0
- nextmv/cli/cloud/secrets/delete.py +63 -0
- nextmv/cli/cloud/secrets/get.py +66 -0
- nextmv/cli/cloud/secrets/list.py +60 -0
- nextmv/cli/cloud/secrets/update.py +144 -0
- nextmv/cli/cloud/shadow/__init__.py +33 -0
- nextmv/cli/cloud/shadow/create.py +184 -0
- nextmv/cli/cloud/shadow/delete.py +64 -0
- nextmv/cli/cloud/shadow/get.py +61 -0
- nextmv/cli/cloud/shadow/list.py +63 -0
- nextmv/cli/cloud/shadow/metadata.py +66 -0
- nextmv/cli/cloud/shadow/start.py +43 -0
- nextmv/cli/cloud/shadow/stop.py +53 -0
- nextmv/cli/cloud/shadow/update.py +96 -0
- nextmv/cli/cloud/switchback/__init__.py +33 -0
- nextmv/cli/cloud/switchback/create.py +151 -0
- nextmv/cli/cloud/switchback/delete.py +64 -0
- nextmv/cli/cloud/switchback/get.py +62 -0
- nextmv/cli/cloud/switchback/list.py +63 -0
- nextmv/cli/cloud/switchback/metadata.py +68 -0
- nextmv/cli/cloud/switchback/start.py +43 -0
- nextmv/cli/cloud/switchback/stop.py +53 -0
- nextmv/cli/cloud/switchback/update.py +96 -0
- nextmv/cli/cloud/upload/__init__.py +22 -0
- nextmv/cli/cloud/upload/create.py +39 -0
- nextmv/cli/cloud/version/__init__.py +33 -0
- nextmv/cli/cloud/version/create.py +96 -0
- nextmv/cli/cloud/version/delete.py +61 -0
- nextmv/cli/cloud/version/exists.py +39 -0
- nextmv/cli/cloud/version/get.py +62 -0
- nextmv/cli/cloud/version/list.py +60 -0
- nextmv/cli/cloud/version/update.py +92 -0
- nextmv/cli/community/__init__.py +24 -0
- nextmv/cli/community/clone.py +20 -204
- nextmv/cli/community/list.py +61 -126
- nextmv/cli/configuration/__init__.py +23 -0
- nextmv/cli/configuration/config.py +103 -6
- nextmv/cli/configuration/create.py +17 -18
- nextmv/cli/configuration/delete.py +25 -13
- nextmv/cli/configuration/list.py +4 -4
- nextmv/cli/confirm.py +34 -0
- nextmv/cli/main.py +68 -36
- nextmv/cli/message.py +170 -0
- nextmv/cli/options.py +196 -0
- nextmv/cli/version.py +20 -1
- nextmv/cloud/__init__.py +17 -38
- nextmv/cloud/acceptance_test.py +20 -83
- nextmv/cloud/account.py +269 -30
- nextmv/cloud/application/__init__.py +898 -0
- nextmv/cloud/application/_acceptance.py +424 -0
- nextmv/cloud/application/_batch_scenario.py +845 -0
- nextmv/cloud/application/_ensemble.py +251 -0
- nextmv/cloud/application/_input_set.py +263 -0
- nextmv/cloud/application/_instance.py +289 -0
- nextmv/cloud/application/_managed_input.py +227 -0
- nextmv/cloud/application/_run.py +1393 -0
- nextmv/cloud/application/_secrets.py +294 -0
- nextmv/cloud/application/_shadow.py +320 -0
- nextmv/cloud/application/_switchback.py +332 -0
- nextmv/cloud/application/_utils.py +54 -0
- nextmv/cloud/application/_version.py +304 -0
- nextmv/cloud/batch_experiment.py +6 -2
- nextmv/cloud/community.py +446 -0
- nextmv/cloud/instance.py +11 -1
- nextmv/cloud/integration.py +8 -5
- nextmv/cloud/package.py +50 -9
- nextmv/cloud/shadow.py +254 -0
- nextmv/cloud/switchback.py +228 -0
- nextmv/deprecated.py +5 -3
- nextmv/input.py +20 -88
- nextmv/local/application.py +3 -15
- nextmv/local/runner.py +1 -1
- nextmv/model.py +50 -11
- nextmv/options.py +11 -256
- nextmv/output.py +0 -62
- nextmv/polling.py +54 -16
- nextmv/run.py +84 -37
- nextmv/status.py +1 -51
- {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/METADATA +37 -11
- nextmv-1.0.0.dist-info/RECORD +185 -0
- nextmv-1.0.0.dist-info/entry_points.txt +2 -0
- nextmv/cli/community/community.py +0 -24
- nextmv/cli/configuration/configuration.py +0 -23
- nextmv/cli/error.py +0 -22
- nextmv/cloud/application.py +0 -4204
- nextmv-0.40.0.dist-info/RECORD +0 -66
- {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/WHEEL +0 -0
- {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/licenses/LICENSE +0 -0
nextmv/cloud/application.py
DELETED
|
@@ -1,4204 +0,0 @@
|
|
|
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
|
-
"""
|
|
24
|
-
|
|
25
|
-
import io
|
|
26
|
-
import json
|
|
27
|
-
import os
|
|
28
|
-
import pathlib
|
|
29
|
-
import shutil
|
|
30
|
-
import tarfile
|
|
31
|
-
import tempfile
|
|
32
|
-
from dataclasses import dataclass
|
|
33
|
-
from datetime import datetime
|
|
34
|
-
from typing import Any
|
|
35
|
-
|
|
36
|
-
import requests
|
|
37
|
-
|
|
38
|
-
from nextmv._serialization import deflated_serialize_json
|
|
39
|
-
from nextmv.base_model import BaseModel
|
|
40
|
-
from nextmv.cloud import package
|
|
41
|
-
from nextmv.cloud.acceptance_test import AcceptanceTest, Metric
|
|
42
|
-
from nextmv.cloud.assets import RunAsset
|
|
43
|
-
from nextmv.cloud.batch_experiment import (
|
|
44
|
-
BatchExperiment,
|
|
45
|
-
BatchExperimentInformation,
|
|
46
|
-
BatchExperimentMetadata,
|
|
47
|
-
BatchExperimentRun,
|
|
48
|
-
ExperimentStatus,
|
|
49
|
-
to_runs,
|
|
50
|
-
)
|
|
51
|
-
from nextmv.cloud.client import Client, get_size
|
|
52
|
-
from nextmv.cloud.ensemble import EnsembleDefinition, EvaluationRule, RunGroup
|
|
53
|
-
from nextmv.cloud.input_set import InputSet, ManagedInput
|
|
54
|
-
from nextmv.cloud.instance import Instance, InstanceConfiguration
|
|
55
|
-
from nextmv.cloud.scenario import Scenario, ScenarioInputType, _option_sets, _scenarios_by_id
|
|
56
|
-
from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
|
|
57
|
-
from nextmv.cloud.url import DownloadURL, UploadURL
|
|
58
|
-
from nextmv.cloud.version import Version
|
|
59
|
-
from nextmv.input import Input, InputFormat
|
|
60
|
-
from nextmv.logger import log
|
|
61
|
-
from nextmv.manifest import Manifest
|
|
62
|
-
from nextmv.model import Model, ModelConfiguration
|
|
63
|
-
from nextmv.options import Options
|
|
64
|
-
from nextmv.output import ASSETS_KEY, STATISTICS_KEY, Asset, Output, OutputFormat, Statistics
|
|
65
|
-
from nextmv.polling import DEFAULT_POLLING_OPTIONS, PollingOptions, poll
|
|
66
|
-
from nextmv.run import (
|
|
67
|
-
ExternalRunResult,
|
|
68
|
-
Format,
|
|
69
|
-
FormatInput,
|
|
70
|
-
FormatOutput,
|
|
71
|
-
Run,
|
|
72
|
-
RunConfiguration,
|
|
73
|
-
RunInformation,
|
|
74
|
-
RunLog,
|
|
75
|
-
RunResult,
|
|
76
|
-
TrackedRun,
|
|
77
|
-
)
|
|
78
|
-
from nextmv.safe import safe_id, safe_name_and_id
|
|
79
|
-
from nextmv.status import StatusV2
|
|
80
|
-
|
|
81
|
-
# Maximum size of the run input/output in bytes. This constant defines the
|
|
82
|
-
# maximum allowed size for run inputs and outputs. When the size exceeds this
|
|
83
|
-
# value, the system will automatically use the large input upload and/or large
|
|
84
|
-
# result download endpoints.
|
|
85
|
-
_MAX_RUN_SIZE: int = 5 * 1024 * 1024
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@dataclass
|
|
89
|
-
class Application:
|
|
90
|
-
"""
|
|
91
|
-
A published decision model that can be executed.
|
|
92
|
-
|
|
93
|
-
You can import the `Application` class directly from `cloud`:
|
|
94
|
-
|
|
95
|
-
```python
|
|
96
|
-
from nextmv.cloud import Application
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
This class represents an application in Nextmv Cloud, providing methods to
|
|
100
|
-
interact with the application, run it with different inputs, manage versions,
|
|
101
|
-
instances, experiments, and more.
|
|
102
|
-
|
|
103
|
-
Parameters
|
|
104
|
-
----------
|
|
105
|
-
client : Client
|
|
106
|
-
Client to use for interacting with the Nextmv Cloud API.
|
|
107
|
-
id : str
|
|
108
|
-
ID of the application.
|
|
109
|
-
default_instance_id : str, default=None
|
|
110
|
-
Default instance ID to use for submitting runs.
|
|
111
|
-
endpoint : str, default="v1/applications/{id}"
|
|
112
|
-
Base endpoint for the application.
|
|
113
|
-
experiments_endpoint : str, default="{base}/experiments"
|
|
114
|
-
Base endpoint for the experiments in the application.
|
|
115
|
-
|
|
116
|
-
Examples
|
|
117
|
-
--------
|
|
118
|
-
>>> from nextmv.cloud import Client, Application
|
|
119
|
-
>>> client = Client(api_key="your-api-key")
|
|
120
|
-
>>> app = Application(client=client, id="your-app-id")
|
|
121
|
-
>>> # Retrieve app information
|
|
122
|
-
>>> instances = app.list_instances()
|
|
123
|
-
"""
|
|
124
|
-
|
|
125
|
-
client: Client
|
|
126
|
-
"""Client to use for interacting with the Nextmv Cloud API."""
|
|
127
|
-
id: str
|
|
128
|
-
"""ID of the application."""
|
|
129
|
-
|
|
130
|
-
default_instance_id: str = None
|
|
131
|
-
"""Default instance ID to use for submitting runs."""
|
|
132
|
-
endpoint: str = "v1/applications/{id}"
|
|
133
|
-
"""Base endpoint for the application."""
|
|
134
|
-
experiments_endpoint: str = "{base}/experiments"
|
|
135
|
-
"""Base endpoint for the experiments in the application."""
|
|
136
|
-
ensembles_endpoint: str = "{base}/ensembles"
|
|
137
|
-
"""Base endpoint for managing the ensemble definitions in the application"""
|
|
138
|
-
|
|
139
|
-
def __post_init__(self):
|
|
140
|
-
"""Initialize the endpoint and experiments_endpoint attributes.
|
|
141
|
-
|
|
142
|
-
This method is automatically called after class initialization to
|
|
143
|
-
format the endpoint and experiments_endpoint URLs with the application ID.
|
|
144
|
-
"""
|
|
145
|
-
self.endpoint = self.endpoint.format(id=self.id)
|
|
146
|
-
self.experiments_endpoint = self.experiments_endpoint.format(base=self.endpoint)
|
|
147
|
-
self.ensembles_endpoint = self.ensembles_endpoint.format(base=self.endpoint)
|
|
148
|
-
|
|
149
|
-
@classmethod
|
|
150
|
-
def new(
|
|
151
|
-
cls,
|
|
152
|
-
client: Client,
|
|
153
|
-
name: str,
|
|
154
|
-
id: str | None = None,
|
|
155
|
-
description: str | None = None,
|
|
156
|
-
is_workflow: bool | None = None,
|
|
157
|
-
exist_ok: bool = False,
|
|
158
|
-
) -> "Application":
|
|
159
|
-
"""
|
|
160
|
-
Create a new application directly in Nextmv Cloud.
|
|
161
|
-
|
|
162
|
-
The application is created as an empty shell, and executable code must
|
|
163
|
-
be pushed to the app before running it remotely.
|
|
164
|
-
|
|
165
|
-
Parameters
|
|
166
|
-
----------
|
|
167
|
-
client : Client
|
|
168
|
-
Client to use for interacting with the Nextmv Cloud API.
|
|
169
|
-
name : str
|
|
170
|
-
Name of the application.
|
|
171
|
-
id : str, optional
|
|
172
|
-
ID of the application. Will be generated if not provided.
|
|
173
|
-
description : str, optional
|
|
174
|
-
Description of the application.
|
|
175
|
-
is_workflow : bool, optional
|
|
176
|
-
Whether the application is a Decision Workflow.
|
|
177
|
-
exist_ok : bool, default=False
|
|
178
|
-
If True and an application with the same ID already exists,
|
|
179
|
-
return the existing application instead of creating a new one.
|
|
180
|
-
|
|
181
|
-
Returns
|
|
182
|
-
-------
|
|
183
|
-
Application
|
|
184
|
-
The newly created (or existing) application.
|
|
185
|
-
|
|
186
|
-
Examples
|
|
187
|
-
--------
|
|
188
|
-
>>> from nextmv.cloud import Client
|
|
189
|
-
>>> client = Client(api_key="your-api-key")
|
|
190
|
-
>>> app = Application.new(client=client, name="My New App", id="my-app")
|
|
191
|
-
"""
|
|
192
|
-
|
|
193
|
-
if id is None:
|
|
194
|
-
id = safe_id("app")
|
|
195
|
-
|
|
196
|
-
if exist_ok and cls.exists(client=client, id=id):
|
|
197
|
-
return Application(client=client, id=id)
|
|
198
|
-
|
|
199
|
-
payload = {
|
|
200
|
-
"name": name,
|
|
201
|
-
"id": id,
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if description is not None:
|
|
205
|
-
payload["description"] = description
|
|
206
|
-
|
|
207
|
-
if is_workflow is not None:
|
|
208
|
-
payload["is_pipeline"] = is_workflow
|
|
209
|
-
|
|
210
|
-
response = client.request(
|
|
211
|
-
method="POST",
|
|
212
|
-
endpoint="v1/applications",
|
|
213
|
-
payload=payload,
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
return cls(client=client, id=response.json()["id"])
|
|
217
|
-
|
|
218
|
-
def acceptance_test(self, acceptance_test_id: str) -> AcceptanceTest:
|
|
219
|
-
"""
|
|
220
|
-
Retrieve details of an acceptance test.
|
|
221
|
-
|
|
222
|
-
Parameters
|
|
223
|
-
----------
|
|
224
|
-
acceptance_test_id : str
|
|
225
|
-
ID of the acceptance test to retrieve.
|
|
226
|
-
|
|
227
|
-
Returns
|
|
228
|
-
-------
|
|
229
|
-
AcceptanceTest
|
|
230
|
-
The requested acceptance test details.
|
|
231
|
-
|
|
232
|
-
Raises
|
|
233
|
-
------
|
|
234
|
-
requests.HTTPError
|
|
235
|
-
If the response status code is not 2xx.
|
|
236
|
-
|
|
237
|
-
Examples
|
|
238
|
-
--------
|
|
239
|
-
>>> test = app.acceptance_test("test-123")
|
|
240
|
-
>>> print(test.name)
|
|
241
|
-
'My Test'
|
|
242
|
-
"""
|
|
243
|
-
response = self.client.request(
|
|
244
|
-
method="GET",
|
|
245
|
-
endpoint=f"{self.experiments_endpoint}/acceptance/{acceptance_test_id}",
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
return AcceptanceTest.from_dict(response.json())
|
|
249
|
-
|
|
250
|
-
def acceptance_test_with_polling(
|
|
251
|
-
self,
|
|
252
|
-
acceptance_test_id: str,
|
|
253
|
-
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
254
|
-
) -> AcceptanceTest:
|
|
255
|
-
"""
|
|
256
|
-
Retrieve details of an acceptance test using polling.
|
|
257
|
-
|
|
258
|
-
Retrieves the result of an acceptance test. This method polls for the
|
|
259
|
-
result until the test finishes executing or the polling strategy is
|
|
260
|
-
exhausted.
|
|
261
|
-
|
|
262
|
-
Parameters
|
|
263
|
-
----------
|
|
264
|
-
acceptance_test_id : str
|
|
265
|
-
ID of the acceptance test to retrieve.
|
|
266
|
-
|
|
267
|
-
Returns
|
|
268
|
-
-------
|
|
269
|
-
AcceptanceTest
|
|
270
|
-
The requested acceptance test details.
|
|
271
|
-
|
|
272
|
-
Raises
|
|
273
|
-
------
|
|
274
|
-
requests.HTTPError
|
|
275
|
-
If the response status code is not 2xx.
|
|
276
|
-
|
|
277
|
-
Examples
|
|
278
|
-
--------
|
|
279
|
-
>>> test = app.acceptance_test_with_polling("test-123")
|
|
280
|
-
>>> print(test.name)
|
|
281
|
-
'My Test'
|
|
282
|
-
"""
|
|
283
|
-
|
|
284
|
-
def polling_func() -> tuple[Any, bool]:
|
|
285
|
-
acceptance_test_result = self.acceptance_test(acceptance_test_id=acceptance_test_id)
|
|
286
|
-
if acceptance_test_result.status in {
|
|
287
|
-
ExperimentStatus.COMPLETED,
|
|
288
|
-
ExperimentStatus.FAILED,
|
|
289
|
-
ExperimentStatus.DRAFT,
|
|
290
|
-
ExperimentStatus.CANCELED,
|
|
291
|
-
ExperimentStatus.DELETE_FAILED,
|
|
292
|
-
}:
|
|
293
|
-
return acceptance_test_result, True
|
|
294
|
-
|
|
295
|
-
return None, False
|
|
296
|
-
|
|
297
|
-
acceptance_test = poll(polling_options=polling_options, polling_func=polling_func)
|
|
298
|
-
|
|
299
|
-
return self.acceptance_test(acceptance_test_id=acceptance_test.id)
|
|
300
|
-
|
|
301
|
-
def batch_experiment(self, batch_id: str) -> BatchExperiment:
|
|
302
|
-
"""
|
|
303
|
-
Get a batch experiment. This method also returns the runs of the batch
|
|
304
|
-
experiment under the `.runs` attribute.
|
|
305
|
-
|
|
306
|
-
Parameters
|
|
307
|
-
----------
|
|
308
|
-
batch_id : str
|
|
309
|
-
ID of the batch experiment.
|
|
310
|
-
|
|
311
|
-
Returns
|
|
312
|
-
-------
|
|
313
|
-
BatchExperiment
|
|
314
|
-
The requested batch experiment details.
|
|
315
|
-
|
|
316
|
-
Raises
|
|
317
|
-
------
|
|
318
|
-
requests.HTTPError
|
|
319
|
-
If the response status code is not 2xx.
|
|
320
|
-
|
|
321
|
-
Examples
|
|
322
|
-
--------
|
|
323
|
-
>>> batch_exp = app.batch_experiment("batch-123")
|
|
324
|
-
>>> print(batch_exp.name)
|
|
325
|
-
'My Batch Experiment'
|
|
326
|
-
"""
|
|
327
|
-
|
|
328
|
-
response = self.client.request(
|
|
329
|
-
method="GET",
|
|
330
|
-
endpoint=f"{self.experiments_endpoint}/batch/{batch_id}",
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
exp = BatchExperiment.from_dict(response.json())
|
|
334
|
-
|
|
335
|
-
runs_response = self.client.request(
|
|
336
|
-
method="GET",
|
|
337
|
-
endpoint=f"{self.experiments_endpoint}/batch/{batch_id}/runs",
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
runs = [Run.from_dict(run) for run in runs_response.json().get("runs", [])]
|
|
341
|
-
exp.runs = runs
|
|
342
|
-
|
|
343
|
-
return exp
|
|
344
|
-
|
|
345
|
-
def batch_experiment_metadata(self, batch_id: str) -> BatchExperimentMetadata:
|
|
346
|
-
"""
|
|
347
|
-
Get metadata for a batch experiment.
|
|
348
|
-
|
|
349
|
-
Parameters
|
|
350
|
-
----------
|
|
351
|
-
batch_id : str
|
|
352
|
-
ID of the batch experiment.
|
|
353
|
-
|
|
354
|
-
Returns
|
|
355
|
-
-------
|
|
356
|
-
BatchExperimentMetadata
|
|
357
|
-
The requested batch experiment metadata.
|
|
358
|
-
|
|
359
|
-
Raises
|
|
360
|
-
------
|
|
361
|
-
requests.HTTPError
|
|
362
|
-
If the response status code is not 2xx.
|
|
363
|
-
|
|
364
|
-
Examples
|
|
365
|
-
--------
|
|
366
|
-
>>> metadata = app.batch_experiment_metadata("batch-123")
|
|
367
|
-
>>> print(metadata.name)
|
|
368
|
-
'My Batch Experiment'
|
|
369
|
-
"""
|
|
370
|
-
|
|
371
|
-
response = self.client.request(
|
|
372
|
-
method="GET",
|
|
373
|
-
endpoint=f"{self.experiments_endpoint}/batch/{batch_id}/metadata",
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
return BatchExperimentMetadata.from_dict(response.json())
|
|
377
|
-
|
|
378
|
-
def batch_experiment_with_polling(
|
|
379
|
-
self,
|
|
380
|
-
batch_id: str,
|
|
381
|
-
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
382
|
-
) -> BatchExperiment:
|
|
383
|
-
"""
|
|
384
|
-
Get a batch experiment with polling.
|
|
385
|
-
|
|
386
|
-
Retrieves the result of an experiment. This method polls for the result
|
|
387
|
-
until the experiment finishes executing or the polling strategy is
|
|
388
|
-
exhausted.
|
|
389
|
-
|
|
390
|
-
Parameters
|
|
391
|
-
----------
|
|
392
|
-
batch_id : str
|
|
393
|
-
ID of the batch experiment.
|
|
394
|
-
|
|
395
|
-
Returns
|
|
396
|
-
-------
|
|
397
|
-
BatchExperiment
|
|
398
|
-
The requested batch experiment details.
|
|
399
|
-
|
|
400
|
-
Raises
|
|
401
|
-
------
|
|
402
|
-
requests.HTTPError
|
|
403
|
-
If the response status code is not 2xx.
|
|
404
|
-
|
|
405
|
-
Examples
|
|
406
|
-
--------
|
|
407
|
-
>>> batch_exp = app.batch_experiment_with_polling("batch-123")
|
|
408
|
-
>>> print(batch_exp.name)
|
|
409
|
-
'My Batch Experiment'
|
|
410
|
-
"""
|
|
411
|
-
|
|
412
|
-
def polling_func() -> tuple[Any, bool]:
|
|
413
|
-
batch_metadata = self.batch_experiment_metadata(batch_id=batch_id)
|
|
414
|
-
if batch_metadata.status in {
|
|
415
|
-
ExperimentStatus.COMPLETED,
|
|
416
|
-
ExperimentStatus.FAILED,
|
|
417
|
-
ExperimentStatus.DRAFT,
|
|
418
|
-
ExperimentStatus.CANCELED,
|
|
419
|
-
ExperimentStatus.DELETE_FAILED,
|
|
420
|
-
}:
|
|
421
|
-
return batch_metadata, True
|
|
422
|
-
|
|
423
|
-
return None, False
|
|
424
|
-
|
|
425
|
-
batch_information = poll(polling_options=polling_options, polling_func=polling_func)
|
|
426
|
-
|
|
427
|
-
return self.batch_experiment(batch_id=batch_information.id)
|
|
428
|
-
|
|
429
|
-
def cancel_run(self, run_id: str) -> None:
|
|
430
|
-
"""
|
|
431
|
-
Cancel a run.
|
|
432
|
-
|
|
433
|
-
Parameters
|
|
434
|
-
----------
|
|
435
|
-
run_id : str
|
|
436
|
-
ID of the run to cancel.
|
|
437
|
-
|
|
438
|
-
Raises
|
|
439
|
-
------
|
|
440
|
-
requests.HTTPError
|
|
441
|
-
If the response status code is not 2xx.
|
|
442
|
-
|
|
443
|
-
Examples
|
|
444
|
-
--------
|
|
445
|
-
>>> app.cancel_run("run-456")
|
|
446
|
-
"""
|
|
447
|
-
|
|
448
|
-
_ = self.client.request(
|
|
449
|
-
method="PATCH",
|
|
450
|
-
endpoint=f"{self.endpoint}/runs/{run_id}/cancel",
|
|
451
|
-
)
|
|
452
|
-
|
|
453
|
-
def delete(self) -> None:
|
|
454
|
-
"""
|
|
455
|
-
Delete the application.
|
|
456
|
-
|
|
457
|
-
Permanently removes the application from Nextmv Cloud.
|
|
458
|
-
|
|
459
|
-
Raises
|
|
460
|
-
------
|
|
461
|
-
requests.HTTPError
|
|
462
|
-
If the response status code is not 2xx.
|
|
463
|
-
|
|
464
|
-
Examples
|
|
465
|
-
--------
|
|
466
|
-
>>> app.delete() # Permanently deletes the application
|
|
467
|
-
"""
|
|
468
|
-
|
|
469
|
-
_ = self.client.request(
|
|
470
|
-
method="DELETE",
|
|
471
|
-
endpoint=self.endpoint,
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
def delete_acceptance_test(self, acceptance_test_id: str) -> None:
|
|
475
|
-
"""
|
|
476
|
-
Delete an acceptance test.
|
|
477
|
-
|
|
478
|
-
Deletes an acceptance test along with all the associated information
|
|
479
|
-
such as the underlying batch experiment.
|
|
480
|
-
|
|
481
|
-
Parameters
|
|
482
|
-
----------
|
|
483
|
-
acceptance_test_id : str
|
|
484
|
-
ID of the acceptance test to delete.
|
|
485
|
-
|
|
486
|
-
Raises
|
|
487
|
-
------
|
|
488
|
-
requests.HTTPError
|
|
489
|
-
If the response status code is not 2xx.
|
|
490
|
-
|
|
491
|
-
Examples
|
|
492
|
-
--------
|
|
493
|
-
>>> app.delete_acceptance_test("test-123")
|
|
494
|
-
"""
|
|
495
|
-
|
|
496
|
-
_ = self.client.request(
|
|
497
|
-
method="DELETE",
|
|
498
|
-
endpoint=f"{self.experiments_endpoint}/acceptance/{acceptance_test_id}",
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
def delete_batch_experiment(self, batch_id: str) -> None:
|
|
502
|
-
"""
|
|
503
|
-
Delete a batch experiment.
|
|
504
|
-
|
|
505
|
-
Deletes a batch experiment along with all the associated information,
|
|
506
|
-
such as its runs.
|
|
507
|
-
|
|
508
|
-
Parameters
|
|
509
|
-
----------
|
|
510
|
-
batch_id : str
|
|
511
|
-
ID of the batch experiment to delete.
|
|
512
|
-
|
|
513
|
-
Raises
|
|
514
|
-
------
|
|
515
|
-
requests.HTTPError
|
|
516
|
-
If the response status code is not 2xx.
|
|
517
|
-
|
|
518
|
-
Examples
|
|
519
|
-
--------
|
|
520
|
-
>>> app.delete_batch_experiment("batch-123")
|
|
521
|
-
"""
|
|
522
|
-
|
|
523
|
-
_ = self.client.request(
|
|
524
|
-
method="DELETE",
|
|
525
|
-
endpoint=f"{self.experiments_endpoint}/batch/{batch_id}",
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
def delete_ensemble_definition(self, ensemble_definition_id: str) -> None:
|
|
529
|
-
"""
|
|
530
|
-
Delete an ensemble definition.
|
|
531
|
-
|
|
532
|
-
Parameters
|
|
533
|
-
----------
|
|
534
|
-
ensemble_definition_id : str
|
|
535
|
-
ID of the ensemble definition to delete.
|
|
536
|
-
|
|
537
|
-
Raises
|
|
538
|
-
------
|
|
539
|
-
requests.HTTPError
|
|
540
|
-
If the response status code is not 2xx.
|
|
541
|
-
|
|
542
|
-
Examples
|
|
543
|
-
--------
|
|
544
|
-
>>> app.delete_ensemble_definition("development-ensemble-definition")
|
|
545
|
-
"""
|
|
546
|
-
|
|
547
|
-
_ = self.client.request(
|
|
548
|
-
method="DELETE",
|
|
549
|
-
endpoint=f"{self.ensembles_endpoint}/{ensemble_definition_id}",
|
|
550
|
-
)
|
|
551
|
-
|
|
552
|
-
def delete_scenario_test(self, scenario_test_id: str) -> None:
|
|
553
|
-
"""
|
|
554
|
-
Delete a scenario test.
|
|
555
|
-
|
|
556
|
-
Deletes a scenario test. Scenario tests are based on the batch
|
|
557
|
-
experiments API, so this function summons `delete_batch_experiment`.
|
|
558
|
-
|
|
559
|
-
Parameters
|
|
560
|
-
----------
|
|
561
|
-
scenario_test_id : str
|
|
562
|
-
ID of the scenario test to delete.
|
|
563
|
-
|
|
564
|
-
Raises
|
|
565
|
-
------
|
|
566
|
-
requests.HTTPError
|
|
567
|
-
If the response status code is not 2xx.
|
|
568
|
-
|
|
569
|
-
Examples
|
|
570
|
-
--------
|
|
571
|
-
>>> app.delete_scenario_test("scenario-123")
|
|
572
|
-
"""
|
|
573
|
-
|
|
574
|
-
self.delete_batch_experiment(batch_id=scenario_test_id)
|
|
575
|
-
|
|
576
|
-
def delete_secrets_collection(self, secrets_collection_id: str) -> None:
|
|
577
|
-
"""
|
|
578
|
-
Delete a secrets collection.
|
|
579
|
-
|
|
580
|
-
Parameters
|
|
581
|
-
----------
|
|
582
|
-
secrets_collection_id : str
|
|
583
|
-
ID of the secrets collection to delete.
|
|
584
|
-
|
|
585
|
-
Raises
|
|
586
|
-
------
|
|
587
|
-
requests.HTTPError
|
|
588
|
-
If the response status code is not 2xx.
|
|
589
|
-
|
|
590
|
-
Examples
|
|
591
|
-
--------
|
|
592
|
-
>>> app.delete_secrets_collection("secrets-123")
|
|
593
|
-
"""
|
|
594
|
-
|
|
595
|
-
_ = self.client.request(
|
|
596
|
-
method="DELETE",
|
|
597
|
-
endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
|
|
598
|
-
)
|
|
599
|
-
|
|
600
|
-
def ensemble_definition(self, ensemble_definition_id: str) -> EnsembleDefinition:
|
|
601
|
-
"""
|
|
602
|
-
Get an ensemble definition.
|
|
603
|
-
|
|
604
|
-
Parameters
|
|
605
|
-
----------
|
|
606
|
-
ensemble_definition_id : str
|
|
607
|
-
ID of the ensemble definition to retrieve.
|
|
608
|
-
|
|
609
|
-
Returns
|
|
610
|
-
-------
|
|
611
|
-
EnsembleDefintion
|
|
612
|
-
The requested ensemble definition details.
|
|
613
|
-
|
|
614
|
-
Raises
|
|
615
|
-
------
|
|
616
|
-
requests.HTTPError
|
|
617
|
-
If the response status code is not 2xx.
|
|
618
|
-
|
|
619
|
-
Examples
|
|
620
|
-
--------
|
|
621
|
-
>>> ensemble_definition = app.ensemble_definition("instance-123")
|
|
622
|
-
>>> print(ensemble_definition.name)
|
|
623
|
-
'Production Ensemble Definition'
|
|
624
|
-
"""
|
|
625
|
-
|
|
626
|
-
response = self.client.request(
|
|
627
|
-
method="GET",
|
|
628
|
-
endpoint=f"{self.ensembles_endpoint}/{ensemble_definition_id}",
|
|
629
|
-
)
|
|
630
|
-
|
|
631
|
-
return EnsembleDefinition.from_dict(response.json())
|
|
632
|
-
|
|
633
|
-
@staticmethod
|
|
634
|
-
def exists(client: Client, id: str) -> bool:
|
|
635
|
-
"""
|
|
636
|
-
Check if an application exists.
|
|
637
|
-
|
|
638
|
-
Parameters
|
|
639
|
-
----------
|
|
640
|
-
client : Client
|
|
641
|
-
Client to use for interacting with the Nextmv Cloud API.
|
|
642
|
-
id : str
|
|
643
|
-
ID of the application to check.
|
|
644
|
-
|
|
645
|
-
Returns
|
|
646
|
-
-------
|
|
647
|
-
bool
|
|
648
|
-
True if the application exists, False otherwise.
|
|
649
|
-
|
|
650
|
-
Examples
|
|
651
|
-
--------
|
|
652
|
-
>>> from nextmv.cloud import Client
|
|
653
|
-
>>> client = Client(api_key="your-api-key")
|
|
654
|
-
>>> Application.exists(client, "app-123")
|
|
655
|
-
True
|
|
656
|
-
"""
|
|
657
|
-
|
|
658
|
-
try:
|
|
659
|
-
_ = client.request(
|
|
660
|
-
method="GET",
|
|
661
|
-
endpoint=f"v1/applications/{id}",
|
|
662
|
-
)
|
|
663
|
-
# If the request was successful, the application exists.
|
|
664
|
-
return True
|
|
665
|
-
except requests.HTTPError as e:
|
|
666
|
-
if _is_not_exist_error(e):
|
|
667
|
-
return False
|
|
668
|
-
# Re-throw the exception if it is not the expected 404 error.
|
|
669
|
-
raise e from None
|
|
670
|
-
|
|
671
|
-
def input_set(self, input_set_id: str) -> InputSet:
|
|
672
|
-
"""
|
|
673
|
-
Get an input set.
|
|
674
|
-
|
|
675
|
-
Parameters
|
|
676
|
-
----------
|
|
677
|
-
input_set_id : str
|
|
678
|
-
ID of the input set to retrieve.
|
|
679
|
-
|
|
680
|
-
Returns
|
|
681
|
-
-------
|
|
682
|
-
InputSet
|
|
683
|
-
The requested input set.
|
|
684
|
-
|
|
685
|
-
Raises
|
|
686
|
-
------
|
|
687
|
-
requests.HTTPError
|
|
688
|
-
If the response status code is not 2xx.
|
|
689
|
-
|
|
690
|
-
Examples
|
|
691
|
-
--------
|
|
692
|
-
>>> input_set = app.input_set("input-set-123")
|
|
693
|
-
>>> print(input_set.name)
|
|
694
|
-
'My Input Set'
|
|
695
|
-
"""
|
|
696
|
-
|
|
697
|
-
response = self.client.request(
|
|
698
|
-
method="GET",
|
|
699
|
-
endpoint=f"{self.experiments_endpoint}/inputsets/{input_set_id}",
|
|
700
|
-
)
|
|
701
|
-
|
|
702
|
-
return InputSet.from_dict(response.json())
|
|
703
|
-
|
|
704
|
-
def instance(self, instance_id: str) -> Instance:
|
|
705
|
-
"""
|
|
706
|
-
Get an instance.
|
|
707
|
-
|
|
708
|
-
Parameters
|
|
709
|
-
----------
|
|
710
|
-
instance_id : str
|
|
711
|
-
ID of the instance to retrieve.
|
|
712
|
-
|
|
713
|
-
Returns
|
|
714
|
-
-------
|
|
715
|
-
Instance
|
|
716
|
-
The requested instance details.
|
|
717
|
-
|
|
718
|
-
Raises
|
|
719
|
-
------
|
|
720
|
-
requests.HTTPError
|
|
721
|
-
If the response status code is not 2xx.
|
|
722
|
-
|
|
723
|
-
Examples
|
|
724
|
-
--------
|
|
725
|
-
>>> instance = app.instance("instance-123")
|
|
726
|
-
>>> print(instance.name)
|
|
727
|
-
'Production Instance'
|
|
728
|
-
"""
|
|
729
|
-
|
|
730
|
-
response = self.client.request(
|
|
731
|
-
method="GET",
|
|
732
|
-
endpoint=f"{self.endpoint}/instances/{instance_id}",
|
|
733
|
-
)
|
|
734
|
-
|
|
735
|
-
return Instance.from_dict(response.json())
|
|
736
|
-
|
|
737
|
-
def instance_exists(self, instance_id: str) -> bool:
|
|
738
|
-
"""
|
|
739
|
-
Check if an instance exists.
|
|
740
|
-
|
|
741
|
-
Parameters
|
|
742
|
-
----------
|
|
743
|
-
instance_id : str
|
|
744
|
-
ID of the instance to check.
|
|
745
|
-
|
|
746
|
-
Returns
|
|
747
|
-
-------
|
|
748
|
-
bool
|
|
749
|
-
True if the instance exists, False otherwise.
|
|
750
|
-
|
|
751
|
-
Examples
|
|
752
|
-
--------
|
|
753
|
-
>>> app.instance_exists("instance-123")
|
|
754
|
-
True
|
|
755
|
-
"""
|
|
756
|
-
|
|
757
|
-
try:
|
|
758
|
-
self.instance(instance_id=instance_id)
|
|
759
|
-
return True
|
|
760
|
-
except requests.HTTPError as e:
|
|
761
|
-
if _is_not_exist_error(e):
|
|
762
|
-
return False
|
|
763
|
-
raise e
|
|
764
|
-
|
|
765
|
-
def list_acceptance_tests(self) -> list[AcceptanceTest]:
|
|
766
|
-
"""
|
|
767
|
-
List all acceptance tests.
|
|
768
|
-
|
|
769
|
-
Returns
|
|
770
|
-
-------
|
|
771
|
-
list[AcceptanceTest]
|
|
772
|
-
List of all acceptance tests associated with this application.
|
|
773
|
-
|
|
774
|
-
Raises
|
|
775
|
-
------
|
|
776
|
-
requests.HTTPError
|
|
777
|
-
If the response status code is not 2xx.
|
|
778
|
-
|
|
779
|
-
Examples
|
|
780
|
-
--------
|
|
781
|
-
>>> tests = app.list_acceptance_tests()
|
|
782
|
-
>>> for test in tests:
|
|
783
|
-
... print(test.name)
|
|
784
|
-
'Test 1'
|
|
785
|
-
'Test 2'
|
|
786
|
-
"""
|
|
787
|
-
|
|
788
|
-
response = self.client.request(
|
|
789
|
-
method="GET",
|
|
790
|
-
endpoint=f"{self.experiments_endpoint}/acceptance",
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
return [AcceptanceTest.from_dict(acceptance_test) for acceptance_test in response.json()]
|
|
794
|
-
|
|
795
|
-
def list_batch_experiments(self) -> list[BatchExperimentMetadata]:
|
|
796
|
-
"""
|
|
797
|
-
List all batch experiments.
|
|
798
|
-
|
|
799
|
-
Returns
|
|
800
|
-
-------
|
|
801
|
-
list[BatchExperimentMetadata]
|
|
802
|
-
List of batch experiments.
|
|
803
|
-
|
|
804
|
-
Raises
|
|
805
|
-
------
|
|
806
|
-
requests.HTTPError
|
|
807
|
-
If the response status code is not 2xx.
|
|
808
|
-
"""
|
|
809
|
-
|
|
810
|
-
response = self.client.request(
|
|
811
|
-
method="GET",
|
|
812
|
-
endpoint=f"{self.experiments_endpoint}/batch",
|
|
813
|
-
query_params={"type": "batch"},
|
|
814
|
-
)
|
|
815
|
-
|
|
816
|
-
return [BatchExperimentMetadata.from_dict(batch_experiment) for batch_experiment in response.json()]
|
|
817
|
-
|
|
818
|
-
def list_ensemble_definitions(self) -> list[EnsembleDefinition]:
|
|
819
|
-
"""
|
|
820
|
-
List all ensemble_definitions.
|
|
821
|
-
|
|
822
|
-
Returns
|
|
823
|
-
-------
|
|
824
|
-
list[EnsembleDefinition]
|
|
825
|
-
List of all ensemble definitions associated with this application.
|
|
826
|
-
|
|
827
|
-
Raises
|
|
828
|
-
------
|
|
829
|
-
requests.HTTPError
|
|
830
|
-
If the response status code is not 2xx.
|
|
831
|
-
|
|
832
|
-
Examples
|
|
833
|
-
--------
|
|
834
|
-
>>> ensemble_definitions = app.list_ensemble_definitions()
|
|
835
|
-
>>> for ensemble_definition in ensemble_definitions:
|
|
836
|
-
... print(ensemble_definition.name)
|
|
837
|
-
'Development Ensemble Definition'
|
|
838
|
-
'Production Ensemble Definition'
|
|
839
|
-
"""
|
|
840
|
-
|
|
841
|
-
response = self.client.request(
|
|
842
|
-
method="GET",
|
|
843
|
-
endpoint=f"{self.ensembles_endpoint}",
|
|
844
|
-
)
|
|
845
|
-
|
|
846
|
-
return [EnsembleDefinition.from_dict(ensemble_definition) for ensemble_definition in response.json()["items"]]
|
|
847
|
-
|
|
848
|
-
def list_input_sets(self) -> list[InputSet]:
|
|
849
|
-
"""
|
|
850
|
-
List all input sets.
|
|
851
|
-
|
|
852
|
-
Returns
|
|
853
|
-
-------
|
|
854
|
-
list[InputSet]
|
|
855
|
-
List of all input sets associated with this application.
|
|
856
|
-
|
|
857
|
-
Raises
|
|
858
|
-
------
|
|
859
|
-
requests.HTTPError
|
|
860
|
-
If the response status code is not 2xx.
|
|
861
|
-
|
|
862
|
-
Examples
|
|
863
|
-
--------
|
|
864
|
-
>>> input_sets = app.list_input_sets()
|
|
865
|
-
>>> for input_set in input_sets:
|
|
866
|
-
... print(input_set.name)
|
|
867
|
-
'Input Set 1'
|
|
868
|
-
'Input Set 2'
|
|
869
|
-
"""
|
|
870
|
-
|
|
871
|
-
response = self.client.request(
|
|
872
|
-
method="GET",
|
|
873
|
-
endpoint=f"{self.experiments_endpoint}/inputsets",
|
|
874
|
-
)
|
|
875
|
-
|
|
876
|
-
return [InputSet.from_dict(input_set) for input_set in response.json()]
|
|
877
|
-
|
|
878
|
-
def list_instances(self) -> list[Instance]:
|
|
879
|
-
"""
|
|
880
|
-
List all instances.
|
|
881
|
-
|
|
882
|
-
Returns
|
|
883
|
-
-------
|
|
884
|
-
list[Instance]
|
|
885
|
-
List of all instances associated with this application.
|
|
886
|
-
|
|
887
|
-
Raises
|
|
888
|
-
------
|
|
889
|
-
requests.HTTPError
|
|
890
|
-
If the response status code is not 2xx.
|
|
891
|
-
|
|
892
|
-
Examples
|
|
893
|
-
--------
|
|
894
|
-
>>> instances = app.list_instances()
|
|
895
|
-
>>> for instance in instances:
|
|
896
|
-
... print(instance.name)
|
|
897
|
-
'Development Instance'
|
|
898
|
-
'Production Instance'
|
|
899
|
-
"""
|
|
900
|
-
|
|
901
|
-
response = self.client.request(
|
|
902
|
-
method="GET",
|
|
903
|
-
endpoint=f"{self.endpoint}/instances",
|
|
904
|
-
)
|
|
905
|
-
|
|
906
|
-
return [Instance.from_dict(instance) for instance in response.json()]
|
|
907
|
-
|
|
908
|
-
def list_managed_inputs(self) -> list[ManagedInput]:
|
|
909
|
-
"""
|
|
910
|
-
List all managed inputs.
|
|
911
|
-
|
|
912
|
-
Returns
|
|
913
|
-
-------
|
|
914
|
-
list[ManagedInput]
|
|
915
|
-
List of managed inputs.
|
|
916
|
-
|
|
917
|
-
Raises
|
|
918
|
-
------
|
|
919
|
-
requests.HTTPError
|
|
920
|
-
If the response status code is not 2xx.
|
|
921
|
-
"""
|
|
922
|
-
|
|
923
|
-
response = self.client.request(
|
|
924
|
-
method="GET",
|
|
925
|
-
endpoint=f"{self.endpoint}/inputs",
|
|
926
|
-
)
|
|
927
|
-
|
|
928
|
-
return [ManagedInput.from_dict(managed_input) for managed_input in response.json()]
|
|
929
|
-
|
|
930
|
-
def list_runs(self) -> list[Run]:
|
|
931
|
-
"""
|
|
932
|
-
List all runs.
|
|
933
|
-
|
|
934
|
-
Returns
|
|
935
|
-
-------
|
|
936
|
-
list[Run]
|
|
937
|
-
List of runs.
|
|
938
|
-
|
|
939
|
-
Raises
|
|
940
|
-
------
|
|
941
|
-
requests.HTTPError
|
|
942
|
-
If the response status code is not 2xx.
|
|
943
|
-
"""
|
|
944
|
-
|
|
945
|
-
response = self.client.request(
|
|
946
|
-
method="GET",
|
|
947
|
-
endpoint=f"{self.endpoint}/runs",
|
|
948
|
-
)
|
|
949
|
-
|
|
950
|
-
return [Run.from_dict(run) for run in response.json().get("runs", [])]
|
|
951
|
-
|
|
952
|
-
def list_scenario_tests(self) -> list[BatchExperimentMetadata]:
|
|
953
|
-
"""
|
|
954
|
-
List all batch scenario tests. Scenario tests are based on the batch
|
|
955
|
-
experiments API, so this function returns the same information as
|
|
956
|
-
`list_batch_experiments`, albeit using a different query parameter.
|
|
957
|
-
|
|
958
|
-
Returns
|
|
959
|
-
-------
|
|
960
|
-
list[BatchExperimentMetadata]
|
|
961
|
-
List of scenario tests.
|
|
962
|
-
|
|
963
|
-
Raises
|
|
964
|
-
------
|
|
965
|
-
requests.HTTPError
|
|
966
|
-
If the response status code is not 2xx.
|
|
967
|
-
"""
|
|
968
|
-
|
|
969
|
-
response = self.client.request(
|
|
970
|
-
method="GET",
|
|
971
|
-
endpoint=f"{self.experiments_endpoint}/batch",
|
|
972
|
-
query_params={"type": "scenario"},
|
|
973
|
-
)
|
|
974
|
-
|
|
975
|
-
return [BatchExperimentMetadata.from_dict(batch_experiment) for batch_experiment in response.json()]
|
|
976
|
-
|
|
977
|
-
def list_secrets_collections(self) -> list[SecretsCollectionSummary]:
|
|
978
|
-
"""
|
|
979
|
-
List all secrets collections.
|
|
980
|
-
|
|
981
|
-
Returns
|
|
982
|
-
-------
|
|
983
|
-
list[SecretsCollectionSummary]
|
|
984
|
-
List of all secrets collections associated with this application.
|
|
985
|
-
|
|
986
|
-
Raises
|
|
987
|
-
------
|
|
988
|
-
requests.HTTPError
|
|
989
|
-
If the response status code is not 2xx.
|
|
990
|
-
|
|
991
|
-
Examples
|
|
992
|
-
--------
|
|
993
|
-
>>> collections = app.list_secrets_collections()
|
|
994
|
-
>>> for collection in collections:
|
|
995
|
-
... print(collection.name)
|
|
996
|
-
'API Keys'
|
|
997
|
-
'Database Credentials'
|
|
998
|
-
"""
|
|
999
|
-
|
|
1000
|
-
response = self.client.request(
|
|
1001
|
-
method="GET",
|
|
1002
|
-
endpoint=f"{self.endpoint}/secrets",
|
|
1003
|
-
)
|
|
1004
|
-
|
|
1005
|
-
return [SecretsCollectionSummary.from_dict(secrets) for secrets in response.json()["items"]]
|
|
1006
|
-
|
|
1007
|
-
def list_versions(self) -> list[Version]:
|
|
1008
|
-
"""
|
|
1009
|
-
List all versions.
|
|
1010
|
-
|
|
1011
|
-
Returns
|
|
1012
|
-
-------
|
|
1013
|
-
list[Version]
|
|
1014
|
-
List of all versions associated with this application.
|
|
1015
|
-
|
|
1016
|
-
Raises
|
|
1017
|
-
------
|
|
1018
|
-
requests.HTTPError
|
|
1019
|
-
If the response status code is not 2xx.
|
|
1020
|
-
|
|
1021
|
-
Examples
|
|
1022
|
-
--------
|
|
1023
|
-
>>> versions = app.list_versions()
|
|
1024
|
-
>>> for version in versions:
|
|
1025
|
-
... print(version.name)
|
|
1026
|
-
'v1.0.0'
|
|
1027
|
-
'v1.1.0'
|
|
1028
|
-
"""
|
|
1029
|
-
|
|
1030
|
-
response = self.client.request(
|
|
1031
|
-
method="GET",
|
|
1032
|
-
endpoint=f"{self.endpoint}/versions",
|
|
1033
|
-
)
|
|
1034
|
-
|
|
1035
|
-
return [Version.from_dict(version) for version in response.json()]
|
|
1036
|
-
|
|
1037
|
-
def managed_input(self, managed_input_id: str) -> ManagedInput:
|
|
1038
|
-
"""
|
|
1039
|
-
Get a managed input.
|
|
1040
|
-
|
|
1041
|
-
Parameters
|
|
1042
|
-
----------
|
|
1043
|
-
managed_input_id: str
|
|
1044
|
-
ID of the managed input.
|
|
1045
|
-
|
|
1046
|
-
Returns
|
|
1047
|
-
-------
|
|
1048
|
-
ManagedInput
|
|
1049
|
-
The managed input.
|
|
1050
|
-
|
|
1051
|
-
Raises
|
|
1052
|
-
------
|
|
1053
|
-
requests.HTTPError
|
|
1054
|
-
If the response status code is not 2xx.
|
|
1055
|
-
"""
|
|
1056
|
-
|
|
1057
|
-
response = self.client.request(
|
|
1058
|
-
method="GET",
|
|
1059
|
-
endpoint=f"{self.endpoint}/inputs/{managed_input_id}",
|
|
1060
|
-
)
|
|
1061
|
-
|
|
1062
|
-
return ManagedInput.from_dict(response.json())
|
|
1063
|
-
|
|
1064
|
-
def new_acceptance_test(
|
|
1065
|
-
self,
|
|
1066
|
-
candidate_instance_id: str,
|
|
1067
|
-
baseline_instance_id: str,
|
|
1068
|
-
id: str,
|
|
1069
|
-
metrics: list[Metric | dict[str, Any]],
|
|
1070
|
-
name: str,
|
|
1071
|
-
input_set_id: str | None = None,
|
|
1072
|
-
description: str | None = None,
|
|
1073
|
-
) -> AcceptanceTest:
|
|
1074
|
-
"""
|
|
1075
|
-
Create a new acceptance test.
|
|
1076
|
-
|
|
1077
|
-
The acceptance test is based on a batch experiment. If you already
|
|
1078
|
-
started a batch experiment, you don't need to provide the input_set_id
|
|
1079
|
-
parameter. In that case, the ID of the acceptance test and the batch
|
|
1080
|
-
experiment must be the same. If the batch experiment does not exist,
|
|
1081
|
-
you can provide the input_set_id parameter and a new batch experiment
|
|
1082
|
-
will be created for you.
|
|
1083
|
-
|
|
1084
|
-
Parameters
|
|
1085
|
-
----------
|
|
1086
|
-
candidate_instance_id : str
|
|
1087
|
-
ID of the candidate instance.
|
|
1088
|
-
baseline_instance_id : str
|
|
1089
|
-
ID of the baseline instance.
|
|
1090
|
-
id : str
|
|
1091
|
-
ID of the acceptance test.
|
|
1092
|
-
metrics : list[Union[Metric, dict[str, Any]]]
|
|
1093
|
-
List of metrics to use for the acceptance test.
|
|
1094
|
-
name : str
|
|
1095
|
-
Name of the acceptance test.
|
|
1096
|
-
input_set_id : Optional[str], default=None
|
|
1097
|
-
ID of the input set to use for the underlying batch experiment,
|
|
1098
|
-
in case it hasn't been started.
|
|
1099
|
-
description : Optional[str], default=None
|
|
1100
|
-
Description of the acceptance test.
|
|
1101
|
-
|
|
1102
|
-
Returns
|
|
1103
|
-
-------
|
|
1104
|
-
AcceptanceTest
|
|
1105
|
-
The created acceptance test.
|
|
1106
|
-
|
|
1107
|
-
Raises
|
|
1108
|
-
------
|
|
1109
|
-
requests.HTTPError
|
|
1110
|
-
If the response status code is not 2xx.
|
|
1111
|
-
ValueError
|
|
1112
|
-
If the batch experiment ID does not match the acceptance test ID.
|
|
1113
|
-
"""
|
|
1114
|
-
|
|
1115
|
-
if input_set_id is None:
|
|
1116
|
-
try:
|
|
1117
|
-
batch_experiment = self.batch_experiment(batch_id=id)
|
|
1118
|
-
batch_experiment_id = batch_experiment.id
|
|
1119
|
-
except requests.HTTPError as e:
|
|
1120
|
-
if e.response.status_code != 404:
|
|
1121
|
-
raise e
|
|
1122
|
-
|
|
1123
|
-
raise ValueError(
|
|
1124
|
-
f"batch experiment {id} does not exist, input_set_id must be defined to create a new one"
|
|
1125
|
-
) from e
|
|
1126
|
-
else:
|
|
1127
|
-
# Get all input IDs from the input set.
|
|
1128
|
-
input_set = self.input_set(input_set_id=input_set_id)
|
|
1129
|
-
if not input_set.input_ids:
|
|
1130
|
-
raise ValueError(f"input set {input_set_id} does not contain any inputs")
|
|
1131
|
-
runs = []
|
|
1132
|
-
for input_id in input_set.input_ids:
|
|
1133
|
-
runs.append(
|
|
1134
|
-
BatchExperimentRun(
|
|
1135
|
-
instance_id=candidate_instance_id,
|
|
1136
|
-
input_set_id=input_set_id,
|
|
1137
|
-
input_id=input_id,
|
|
1138
|
-
)
|
|
1139
|
-
)
|
|
1140
|
-
runs.append(
|
|
1141
|
-
BatchExperimentRun(
|
|
1142
|
-
instance_id=baseline_instance_id,
|
|
1143
|
-
input_set_id=input_set_id,
|
|
1144
|
-
input_id=input_id,
|
|
1145
|
-
)
|
|
1146
|
-
)
|
|
1147
|
-
batch_experiment_id = self.new_batch_experiment(
|
|
1148
|
-
name=name,
|
|
1149
|
-
description=description,
|
|
1150
|
-
id=id,
|
|
1151
|
-
runs=runs,
|
|
1152
|
-
)
|
|
1153
|
-
|
|
1154
|
-
if batch_experiment_id != id:
|
|
1155
|
-
raise ValueError(f"batch experiment_id ({batch_experiment_id}) does not match acceptance test id ({id})")
|
|
1156
|
-
|
|
1157
|
-
payload_metrics = [{}] * len(metrics)
|
|
1158
|
-
for i, metric in enumerate(metrics):
|
|
1159
|
-
payload_metrics[i] = metric.to_dict() if isinstance(metric, Metric) else metric
|
|
1160
|
-
|
|
1161
|
-
payload = {
|
|
1162
|
-
"candidate": {"instance_id": candidate_instance_id},
|
|
1163
|
-
"control": {"instance_id": baseline_instance_id},
|
|
1164
|
-
"metrics": payload_metrics,
|
|
1165
|
-
"experiment_id": batch_experiment_id,
|
|
1166
|
-
"name": name,
|
|
1167
|
-
}
|
|
1168
|
-
if description is not None:
|
|
1169
|
-
payload["description"] = description
|
|
1170
|
-
if id is not None:
|
|
1171
|
-
payload["id"] = id
|
|
1172
|
-
|
|
1173
|
-
response = self.client.request(
|
|
1174
|
-
method="POST",
|
|
1175
|
-
endpoint=f"{self.experiments_endpoint}/acceptance",
|
|
1176
|
-
payload=payload,
|
|
1177
|
-
)
|
|
1178
|
-
|
|
1179
|
-
return AcceptanceTest.from_dict(response.json())
|
|
1180
|
-
|
|
1181
|
-
def new_acceptance_test_with_result(
|
|
1182
|
-
self,
|
|
1183
|
-
candidate_instance_id: str,
|
|
1184
|
-
baseline_instance_id: str,
|
|
1185
|
-
id: str,
|
|
1186
|
-
metrics: list[Metric | dict[str, Any]],
|
|
1187
|
-
name: str,
|
|
1188
|
-
input_set_id: str | None = None,
|
|
1189
|
-
description: str | None = None,
|
|
1190
|
-
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
1191
|
-
) -> AcceptanceTest:
|
|
1192
|
-
"""
|
|
1193
|
-
Create a new acceptance test and poll for the result.
|
|
1194
|
-
|
|
1195
|
-
This is a convenience method that combines the new_acceptance_test with polling
|
|
1196
|
-
logic to check when the acceptance test is done.
|
|
1197
|
-
|
|
1198
|
-
Parameters
|
|
1199
|
-
----------
|
|
1200
|
-
candidate_instance_id : str
|
|
1201
|
-
ID of the candidate instance.
|
|
1202
|
-
baseline_instance_id : str
|
|
1203
|
-
ID of the baseline instance.
|
|
1204
|
-
id : str
|
|
1205
|
-
ID of the acceptance test.
|
|
1206
|
-
metrics : list[Union[Metric, dict[str, Any]]]
|
|
1207
|
-
List of metrics to use for the acceptance test.
|
|
1208
|
-
name : str
|
|
1209
|
-
Name of the acceptance test.
|
|
1210
|
-
input_set_id : Optional[str], default=None
|
|
1211
|
-
ID of the input set to use for the underlying batch experiment,
|
|
1212
|
-
in case it hasn't been started.
|
|
1213
|
-
description : Optional[str], default=None
|
|
1214
|
-
Description of the acceptance test.
|
|
1215
|
-
polling_options : PollingOptions, default=_DEFAULT_POLLING_OPTIONS
|
|
1216
|
-
Options to use when polling for the acceptance test result.
|
|
1217
|
-
|
|
1218
|
-
Returns
|
|
1219
|
-
-------
|
|
1220
|
-
AcceptanceTest
|
|
1221
|
-
The completed acceptance test with results.
|
|
1222
|
-
|
|
1223
|
-
Raises
|
|
1224
|
-
------
|
|
1225
|
-
requests.HTTPError
|
|
1226
|
-
If the response status code is not 2xx.
|
|
1227
|
-
TimeoutError
|
|
1228
|
-
If the acceptance test does not succeed after the
|
|
1229
|
-
polling strategy is exhausted based on time duration.
|
|
1230
|
-
RuntimeError
|
|
1231
|
-
If the acceptance test does not succeed after the
|
|
1232
|
-
polling strategy is exhausted based on number of tries.
|
|
1233
|
-
|
|
1234
|
-
Examples
|
|
1235
|
-
--------
|
|
1236
|
-
>>> test = app.new_acceptance_test_with_result(
|
|
1237
|
-
... candidate_instance_id="candidate-123",
|
|
1238
|
-
... baseline_instance_id="baseline-456",
|
|
1239
|
-
... id="test-789",
|
|
1240
|
-
... metrics=[Metric(name="objective", type="numeric")],
|
|
1241
|
-
... name="Performance Test",
|
|
1242
|
-
... input_set_id="input-set-123"
|
|
1243
|
-
... )
|
|
1244
|
-
>>> print(test.status)
|
|
1245
|
-
'completed'
|
|
1246
|
-
"""
|
|
1247
|
-
|
|
1248
|
-
acceptance_test = self.new_acceptance_test(
|
|
1249
|
-
candidate_instance_id=candidate_instance_id,
|
|
1250
|
-
baseline_instance_id=baseline_instance_id,
|
|
1251
|
-
id=id,
|
|
1252
|
-
metrics=metrics,
|
|
1253
|
-
name=name,
|
|
1254
|
-
input_set_id=input_set_id,
|
|
1255
|
-
description=description,
|
|
1256
|
-
)
|
|
1257
|
-
|
|
1258
|
-
return self.acceptance_test_with_polling(
|
|
1259
|
-
acceptance_test_id=acceptance_test.id,
|
|
1260
|
-
polling_options=polling_options,
|
|
1261
|
-
)
|
|
1262
|
-
|
|
1263
|
-
def new_batch_experiment(
|
|
1264
|
-
self,
|
|
1265
|
-
name: str,
|
|
1266
|
-
input_set_id: str | None = None,
|
|
1267
|
-
instance_ids: list[str] | None = None,
|
|
1268
|
-
description: str | None = None,
|
|
1269
|
-
id: str | None = None,
|
|
1270
|
-
option_sets: dict[str, dict[str, str]] | None = None,
|
|
1271
|
-
runs: list[BatchExperimentRun | dict[str, Any]] | None = None,
|
|
1272
|
-
type: str | None = "batch",
|
|
1273
|
-
) -> str:
|
|
1274
|
-
"""
|
|
1275
|
-
Create a new batch experiment.
|
|
1276
|
-
|
|
1277
|
-
Parameters
|
|
1278
|
-
----------
|
|
1279
|
-
name: str
|
|
1280
|
-
Name of the batch experiment.
|
|
1281
|
-
input_set_id: str
|
|
1282
|
-
ID of the input set to use for the batch experiment.
|
|
1283
|
-
instance_ids: list[str]
|
|
1284
|
-
List of instance IDs to use for the batch experiment.
|
|
1285
|
-
This argument is deprecated, use `runs` instead.
|
|
1286
|
-
description: Optional[str]
|
|
1287
|
-
Optional description of the batch experiment.
|
|
1288
|
-
id: Optional[str]
|
|
1289
|
-
ID of the batch experiment. Will be generated if not provided.
|
|
1290
|
-
option_sets: Optional[dict[str, dict[str, str]]]
|
|
1291
|
-
Option sets to use for the batch experiment. This is a dictionary
|
|
1292
|
-
where the keys are option set IDs and the values are dictionaries
|
|
1293
|
-
with the actual options.
|
|
1294
|
-
runs: Optional[list[BatchExperimentRun]]
|
|
1295
|
-
List of runs to use for the batch experiment.
|
|
1296
|
-
type: Optional[str]
|
|
1297
|
-
Type of the batch experiment. This is used to determine the
|
|
1298
|
-
experiment type. The default value is "batch". If you want to
|
|
1299
|
-
create a scenario test, set this to "scenario".
|
|
1300
|
-
|
|
1301
|
-
Returns
|
|
1302
|
-
-------
|
|
1303
|
-
str
|
|
1304
|
-
ID of the batch experiment.
|
|
1305
|
-
|
|
1306
|
-
Raises
|
|
1307
|
-
------
|
|
1308
|
-
requests.HTTPError
|
|
1309
|
-
If the response status code is not 2xx.
|
|
1310
|
-
"""
|
|
1311
|
-
|
|
1312
|
-
payload = {
|
|
1313
|
-
"name": name,
|
|
1314
|
-
}
|
|
1315
|
-
if input_set_id is not None:
|
|
1316
|
-
payload["input_set_id"] = input_set_id
|
|
1317
|
-
if instance_ids is not None:
|
|
1318
|
-
input_set = self.input_set(input_set_id)
|
|
1319
|
-
runs = to_runs(instance_ids, input_set)
|
|
1320
|
-
payload_runs = [run.to_dict() for run in runs]
|
|
1321
|
-
payload["runs"] = payload_runs
|
|
1322
|
-
if description is not None:
|
|
1323
|
-
payload["description"] = description
|
|
1324
|
-
if id is not None:
|
|
1325
|
-
payload["id"] = id
|
|
1326
|
-
if option_sets is not None:
|
|
1327
|
-
payload["option_sets"] = option_sets
|
|
1328
|
-
if runs is not None:
|
|
1329
|
-
payload_runs = [{}] * len(runs)
|
|
1330
|
-
for i, run in enumerate(runs):
|
|
1331
|
-
payload_runs[i] = run.to_dict() if isinstance(run, BatchExperimentRun) else run
|
|
1332
|
-
payload["runs"] = payload_runs
|
|
1333
|
-
if type is not None:
|
|
1334
|
-
payload["type"] = type
|
|
1335
|
-
|
|
1336
|
-
response = self.client.request(
|
|
1337
|
-
method="POST",
|
|
1338
|
-
endpoint=f"{self.experiments_endpoint}/batch",
|
|
1339
|
-
payload=payload,
|
|
1340
|
-
)
|
|
1341
|
-
|
|
1342
|
-
return response.json()["id"]
|
|
1343
|
-
|
|
1344
|
-
def new_batch_experiment_with_result(
|
|
1345
|
-
self,
|
|
1346
|
-
name: str,
|
|
1347
|
-
input_set_id: str | None = None,
|
|
1348
|
-
instance_ids: list[str] | None = None,
|
|
1349
|
-
description: str | None = None,
|
|
1350
|
-
id: str | None = None,
|
|
1351
|
-
option_sets: dict[str, dict[str, str]] | None = None,
|
|
1352
|
-
runs: list[BatchExperimentRun | dict[str, Any]] | None = None,
|
|
1353
|
-
type: str | None = "batch",
|
|
1354
|
-
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
1355
|
-
) -> BatchExperiment:
|
|
1356
|
-
"""
|
|
1357
|
-
Convenience method to create a new batch experiment and poll for the
|
|
1358
|
-
result.
|
|
1359
|
-
|
|
1360
|
-
This method combines the `new_batch_experiment` and
|
|
1361
|
-
`batch_experiment_with_polling` methods, applying polling logic to
|
|
1362
|
-
check when the experiment succeeded.
|
|
1363
|
-
|
|
1364
|
-
Parameters
|
|
1365
|
-
----------
|
|
1366
|
-
name: str
|
|
1367
|
-
Name of the batch experiment.
|
|
1368
|
-
input_set_id: str
|
|
1369
|
-
ID of the input set to use for the batch experiment.
|
|
1370
|
-
instance_ids: list[str]
|
|
1371
|
-
List of instance IDs to use for the batch experiment. This argument
|
|
1372
|
-
is deprecated, use `runs` instead.
|
|
1373
|
-
description: Optional[str]
|
|
1374
|
-
Optional description of the batch experiment.
|
|
1375
|
-
id: Optional[str]
|
|
1376
|
-
ID of the batch experiment. Will be generated if not provided.
|
|
1377
|
-
option_sets: Optional[dict[str, dict[str, str]]]
|
|
1378
|
-
Option sets to use for the batch experiment. This is a dictionary
|
|
1379
|
-
where the keys are option set IDs and the values are dictionaries
|
|
1380
|
-
with the actual options.
|
|
1381
|
-
runs: Optional[list[BatchExperimentRun]]
|
|
1382
|
-
List of runs to use for the batch experiment.
|
|
1383
|
-
type: Optional[str]
|
|
1384
|
-
Type of the batch experiment. This is used to determine the
|
|
1385
|
-
experiment type. The default value is "batch". If you want to
|
|
1386
|
-
create a scenario test, set this to "scenario".
|
|
1387
|
-
polling_options : PollingOptions, default=_DEFAULT_POLLING_OPTIONS
|
|
1388
|
-
Options to use when polling for the batch experiment result.
|
|
1389
|
-
|
|
1390
|
-
Returns
|
|
1391
|
-
-------
|
|
1392
|
-
BatchExperiment
|
|
1393
|
-
The completed batch experiment with results.
|
|
1394
|
-
|
|
1395
|
-
Raises
|
|
1396
|
-
------
|
|
1397
|
-
requests.HTTPError
|
|
1398
|
-
If the response status code is not 2xx.
|
|
1399
|
-
"""
|
|
1400
|
-
|
|
1401
|
-
batch_id = self.new_batch_experiment(
|
|
1402
|
-
name=name,
|
|
1403
|
-
input_set_id=input_set_id,
|
|
1404
|
-
instance_ids=instance_ids,
|
|
1405
|
-
description=description,
|
|
1406
|
-
id=id,
|
|
1407
|
-
option_sets=option_sets,
|
|
1408
|
-
runs=runs,
|
|
1409
|
-
type=type,
|
|
1410
|
-
)
|
|
1411
|
-
|
|
1412
|
-
return self.batch_experiment_with_polling(batch_id=batch_id, polling_options=polling_options)
|
|
1413
|
-
|
|
1414
|
-
def new_ensemble_defintion(
|
|
1415
|
-
self,
|
|
1416
|
-
id: str,
|
|
1417
|
-
run_groups: list[RunGroup],
|
|
1418
|
-
rules: list[EvaluationRule],
|
|
1419
|
-
name: str | None = None,
|
|
1420
|
-
description: str | None = None,
|
|
1421
|
-
) -> EnsembleDefinition:
|
|
1422
|
-
"""
|
|
1423
|
-
Create a new ensemble definition.
|
|
1424
|
-
|
|
1425
|
-
Parameters
|
|
1426
|
-
----------
|
|
1427
|
-
id: str
|
|
1428
|
-
ID of the ensemble defintion.
|
|
1429
|
-
run_groups: list[RunGroup]
|
|
1430
|
-
Information to facilitate the execution of child runs.
|
|
1431
|
-
rules: list[EvaluationRule]
|
|
1432
|
-
Information to facilitate the selection of
|
|
1433
|
-
a result for the ensemble run from child runs.
|
|
1434
|
-
name: Optional[str]
|
|
1435
|
-
Name of the ensemble definition.
|
|
1436
|
-
description: Optional[str]
|
|
1437
|
-
Description of the ensemble definition.
|
|
1438
|
-
"""
|
|
1439
|
-
|
|
1440
|
-
if name is None:
|
|
1441
|
-
name = id
|
|
1442
|
-
if description is None:
|
|
1443
|
-
description = name
|
|
1444
|
-
|
|
1445
|
-
payload = {
|
|
1446
|
-
"id": id,
|
|
1447
|
-
"run_groups": [run_group.to_dict() for run_group in run_groups],
|
|
1448
|
-
"rules": [rule.to_dict() for rule in rules],
|
|
1449
|
-
"name": name,
|
|
1450
|
-
"description": description,
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
response = self.client.request(
|
|
1454
|
-
method="POST",
|
|
1455
|
-
endpoint=f"{self.ensembles_endpoint}",
|
|
1456
|
-
payload=payload,
|
|
1457
|
-
)
|
|
1458
|
-
|
|
1459
|
-
return EnsembleDefinition.from_dict(response.json())
|
|
1460
|
-
|
|
1461
|
-
def new_input_set(
|
|
1462
|
-
self,
|
|
1463
|
-
id: str,
|
|
1464
|
-
name: str,
|
|
1465
|
-
description: str | None = None,
|
|
1466
|
-
end_time: datetime | None = None,
|
|
1467
|
-
instance_id: str | None = None,
|
|
1468
|
-
maximum_runs: int | None = None,
|
|
1469
|
-
run_ids: list[str] | None = None,
|
|
1470
|
-
start_time: datetime | None = None,
|
|
1471
|
-
inputs: list[ManagedInput] | None = None,
|
|
1472
|
-
) -> InputSet:
|
|
1473
|
-
"""
|
|
1474
|
-
Create a new input set. You can create an input set from three
|
|
1475
|
-
different methodologies:
|
|
1476
|
-
|
|
1477
|
-
1. Using `instance_id`, `start_time`, `end_time` and `maximum_runs`.
|
|
1478
|
-
Instance runs will be obtained from the application matching the
|
|
1479
|
-
criteria of dates and maximum number of runs.
|
|
1480
|
-
2. Using `run_ids`. The input set will be created using the list of
|
|
1481
|
-
runs specified by the user.
|
|
1482
|
-
3. Using `inputs`. The input set will be created using the list of
|
|
1483
|
-
inputs specified by the user. This is useful for creating an input
|
|
1484
|
-
set from a list of inputs that are already available in the
|
|
1485
|
-
application.
|
|
1486
|
-
|
|
1487
|
-
Parameters
|
|
1488
|
-
----------
|
|
1489
|
-
id: str
|
|
1490
|
-
ID of the input set
|
|
1491
|
-
name: str
|
|
1492
|
-
Name of the input set.
|
|
1493
|
-
description: Optional[str]
|
|
1494
|
-
Optional description of the input set.
|
|
1495
|
-
end_time: Optional[datetime]
|
|
1496
|
-
End time of the input set. This is used to filter the runs
|
|
1497
|
-
associated with the input set.
|
|
1498
|
-
instance_id: Optional[str]
|
|
1499
|
-
ID of the instance to use for the input set. This is used to
|
|
1500
|
-
filter the runs associated with the input set. If not provided,
|
|
1501
|
-
the application's `default_instance_id` is used.
|
|
1502
|
-
maximum_runs: Optional[int]
|
|
1503
|
-
Maximum number of runs to use for the input set. This is used to
|
|
1504
|
-
filter the runs associated with the input set. If not provided,
|
|
1505
|
-
all runs are used.
|
|
1506
|
-
run_ids: Optional[list[str]]
|
|
1507
|
-
List of run IDs to use for the input set.
|
|
1508
|
-
start_time: Optional[datetime]
|
|
1509
|
-
Start time of the input set. This is used to filter the runs
|
|
1510
|
-
associated with the input set.
|
|
1511
|
-
inputs: Optional[list[ExperimentInput]]
|
|
1512
|
-
List of inputs to use for the input set. This is used to create
|
|
1513
|
-
the input set from a list of inputs that are already available in
|
|
1514
|
-
the application.
|
|
1515
|
-
|
|
1516
|
-
Returns
|
|
1517
|
-
-------
|
|
1518
|
-
InputSet
|
|
1519
|
-
The new input set.
|
|
1520
|
-
|
|
1521
|
-
Raises
|
|
1522
|
-
------
|
|
1523
|
-
requests.HTTPError
|
|
1524
|
-
If the response status code is not 2xx.
|
|
1525
|
-
"""
|
|
1526
|
-
|
|
1527
|
-
payload = {
|
|
1528
|
-
"id": id,
|
|
1529
|
-
"name": name,
|
|
1530
|
-
}
|
|
1531
|
-
if description is not None:
|
|
1532
|
-
payload["description"] = description
|
|
1533
|
-
if end_time is not None:
|
|
1534
|
-
payload["end_time"] = end_time.isoformat()
|
|
1535
|
-
if instance_id is not None:
|
|
1536
|
-
payload["instance_id"] = instance_id
|
|
1537
|
-
if maximum_runs is not None:
|
|
1538
|
-
payload["maximum_runs"] = maximum_runs
|
|
1539
|
-
if run_ids is not None:
|
|
1540
|
-
payload["run_ids"] = run_ids
|
|
1541
|
-
if start_time is not None:
|
|
1542
|
-
payload["start_time"] = start_time.isoformat()
|
|
1543
|
-
if inputs is not None:
|
|
1544
|
-
payload["inputs"] = [input.to_dict() for input in inputs]
|
|
1545
|
-
|
|
1546
|
-
response = self.client.request(
|
|
1547
|
-
method="POST",
|
|
1548
|
-
endpoint=f"{self.experiments_endpoint}/inputsets",
|
|
1549
|
-
payload=payload,
|
|
1550
|
-
)
|
|
1551
|
-
|
|
1552
|
-
return InputSet.from_dict(response.json())
|
|
1553
|
-
|
|
1554
|
-
def new_instance(
|
|
1555
|
-
self,
|
|
1556
|
-
version_id: str,
|
|
1557
|
-
id: str,
|
|
1558
|
-
name: str,
|
|
1559
|
-
description: str | None = None,
|
|
1560
|
-
configuration: InstanceConfiguration | None = None,
|
|
1561
|
-
exist_ok: bool = False,
|
|
1562
|
-
) -> Instance:
|
|
1563
|
-
"""
|
|
1564
|
-
Create a new instance and associate it with a version.
|
|
1565
|
-
|
|
1566
|
-
This method creates a new instance associated with a specific version of the application.
|
|
1567
|
-
Instances are configurations of an application version that can be executed.
|
|
1568
|
-
|
|
1569
|
-
Parameters
|
|
1570
|
-
----------
|
|
1571
|
-
version_id : str
|
|
1572
|
-
ID of the version to associate the instance with.
|
|
1573
|
-
id : str
|
|
1574
|
-
ID of the instance. Will be generated if not provided.
|
|
1575
|
-
name : str
|
|
1576
|
-
Name of the instance. Will be generated if not provided.
|
|
1577
|
-
description : Optional[str], default=None
|
|
1578
|
-
Description of the instance.
|
|
1579
|
-
configuration : Optional[InstanceConfiguration], default=None
|
|
1580
|
-
Configuration to use for the instance. This can include resources,
|
|
1581
|
-
timeouts, and other execution parameters.
|
|
1582
|
-
exist_ok : bool, default=False
|
|
1583
|
-
If True and an instance with the same ID already exists,
|
|
1584
|
-
return the existing instance instead of creating a new one.
|
|
1585
|
-
|
|
1586
|
-
Returns
|
|
1587
|
-
-------
|
|
1588
|
-
Instance
|
|
1589
|
-
The newly created (or existing) instance.
|
|
1590
|
-
|
|
1591
|
-
Raises
|
|
1592
|
-
------
|
|
1593
|
-
requests.HTTPError
|
|
1594
|
-
If the response status code is not 2xx.
|
|
1595
|
-
ValueError
|
|
1596
|
-
If exist_ok is True and id is None.
|
|
1597
|
-
|
|
1598
|
-
Examples
|
|
1599
|
-
--------
|
|
1600
|
-
>>> # Create a new instance for a specific version
|
|
1601
|
-
>>> instance = app.new_instance(
|
|
1602
|
-
... version_id="version-123",
|
|
1603
|
-
... id="prod-instance",
|
|
1604
|
-
... name="Production Instance",
|
|
1605
|
-
... description="Instance for production use"
|
|
1606
|
-
... )
|
|
1607
|
-
>>> print(instance.name)
|
|
1608
|
-
'Production Instance'
|
|
1609
|
-
"""
|
|
1610
|
-
|
|
1611
|
-
if exist_ok and id is None:
|
|
1612
|
-
raise ValueError("If exist_ok is True, id must be provided")
|
|
1613
|
-
|
|
1614
|
-
if exist_ok and self.instance_exists(instance_id=id):
|
|
1615
|
-
return self.instance(instance_id=id)
|
|
1616
|
-
|
|
1617
|
-
payload = {
|
|
1618
|
-
"version_id": version_id,
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
if id is not None:
|
|
1622
|
-
payload["id"] = id
|
|
1623
|
-
if name is not None:
|
|
1624
|
-
payload["name"] = name
|
|
1625
|
-
if description is not None:
|
|
1626
|
-
payload["description"] = description
|
|
1627
|
-
if configuration is not None:
|
|
1628
|
-
payload["configuration"] = configuration.to_dict()
|
|
1629
|
-
|
|
1630
|
-
response = self.client.request(
|
|
1631
|
-
method="POST",
|
|
1632
|
-
endpoint=f"{self.endpoint}/instances",
|
|
1633
|
-
payload=payload,
|
|
1634
|
-
)
|
|
1635
|
-
|
|
1636
|
-
return Instance.from_dict(response.json())
|
|
1637
|
-
|
|
1638
|
-
def new_managed_input(
|
|
1639
|
-
self,
|
|
1640
|
-
id: str,
|
|
1641
|
-
name: str,
|
|
1642
|
-
description: str | None = None,
|
|
1643
|
-
upload_id: str | None = None,
|
|
1644
|
-
run_id: str | None = None,
|
|
1645
|
-
format: Format | dict[str, Any] | None = None,
|
|
1646
|
-
) -> ManagedInput:
|
|
1647
|
-
"""
|
|
1648
|
-
Create a new managed input. There are two methods for creating a
|
|
1649
|
-
managed input:
|
|
1650
|
-
|
|
1651
|
-
1. Specifying the `upload_id` parameter. You may use the `upload_url`
|
|
1652
|
-
method to obtain the upload ID and the `upload_large_input` method
|
|
1653
|
-
to upload the data to it.
|
|
1654
|
-
2. Specifying the `run_id` parameter. The managed input will be
|
|
1655
|
-
created from the run specified by the `run_id` parameter.
|
|
1656
|
-
|
|
1657
|
-
Either the `upload_id` or the `run_id` parameter must be specified.
|
|
1658
|
-
|
|
1659
|
-
Parameters
|
|
1660
|
-
----------
|
|
1661
|
-
id: str
|
|
1662
|
-
ID of the managed input.
|
|
1663
|
-
name: str
|
|
1664
|
-
Name of the managed input.
|
|
1665
|
-
description: Optional[str]
|
|
1666
|
-
Optional description of the managed input.
|
|
1667
|
-
upload_id: Optional[str]
|
|
1668
|
-
ID of the upload to use for the managed input.
|
|
1669
|
-
run_id: Optional[str]
|
|
1670
|
-
ID of the run to use for the managed input.
|
|
1671
|
-
format: Optional[Format]
|
|
1672
|
-
Format of the managed input. Default will be formatted as `JSON`.
|
|
1673
|
-
|
|
1674
|
-
Returns
|
|
1675
|
-
-------
|
|
1676
|
-
ManagedInput
|
|
1677
|
-
The new managed input.
|
|
1678
|
-
|
|
1679
|
-
Raises
|
|
1680
|
-
------
|
|
1681
|
-
requests.HTTPError
|
|
1682
|
-
If the response status code is not 2xx.
|
|
1683
|
-
ValueError
|
|
1684
|
-
If neither the `upload_id` nor the `run_id` parameter is
|
|
1685
|
-
specified.
|
|
1686
|
-
"""
|
|
1687
|
-
|
|
1688
|
-
if upload_id is None and run_id is None:
|
|
1689
|
-
raise ValueError("Either upload_id or run_id must be specified")
|
|
1690
|
-
|
|
1691
|
-
payload = {
|
|
1692
|
-
"id": id,
|
|
1693
|
-
"name": name,
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
if description is not None:
|
|
1697
|
-
payload["description"] = description
|
|
1698
|
-
if upload_id is not None:
|
|
1699
|
-
payload["upload_id"] = upload_id
|
|
1700
|
-
if run_id is not None:
|
|
1701
|
-
payload["run_id"] = run_id
|
|
1702
|
-
|
|
1703
|
-
if format is not None:
|
|
1704
|
-
payload["format"] = format.to_dict() if isinstance(format, Format) else format
|
|
1705
|
-
else:
|
|
1706
|
-
payload["format"] = Format(
|
|
1707
|
-
format_input=FormatInput(input_type=InputFormat.JSON),
|
|
1708
|
-
format_output=FormatOutput(output_type=OutputFormat.JSON),
|
|
1709
|
-
).to_dict()
|
|
1710
|
-
|
|
1711
|
-
response = self.client.request(
|
|
1712
|
-
method="POST",
|
|
1713
|
-
endpoint=f"{self.endpoint}/inputs",
|
|
1714
|
-
payload=payload,
|
|
1715
|
-
)
|
|
1716
|
-
|
|
1717
|
-
return ManagedInput.from_dict(response.json())
|
|
1718
|
-
|
|
1719
|
-
def new_run( # noqa: C901 # Refactor this function at some point.
|
|
1720
|
-
self,
|
|
1721
|
-
input: Input | dict[str, Any] | BaseModel | str = None,
|
|
1722
|
-
instance_id: str | None = None,
|
|
1723
|
-
name: str | None = None,
|
|
1724
|
-
description: str | None = None,
|
|
1725
|
-
upload_id: str | None = None,
|
|
1726
|
-
options: Options | dict[str, str] | None = None,
|
|
1727
|
-
configuration: RunConfiguration | dict[str, Any] | None = None,
|
|
1728
|
-
batch_experiment_id: str | None = None,
|
|
1729
|
-
external_result: ExternalRunResult | dict[str, Any] | None = None,
|
|
1730
|
-
json_configurations: dict[str, Any] | None = None,
|
|
1731
|
-
input_dir_path: str | None = None,
|
|
1732
|
-
) -> str:
|
|
1733
|
-
"""
|
|
1734
|
-
Submit an input to start a new run of the application. Returns the
|
|
1735
|
-
`run_id` of the submitted run.
|
|
1736
|
-
|
|
1737
|
-
Parameters
|
|
1738
|
-
----------
|
|
1739
|
-
input: Union[Input, dict[str, Any], BaseModel, str]
|
|
1740
|
-
Input to use for the run. This can be a `nextmv.Input` object,
|
|
1741
|
-
`dict`, `BaseModel` or `str`.
|
|
1742
|
-
|
|
1743
|
-
If `nextmv.Input` is used, and the `input_format` is either
|
|
1744
|
-
`nextmv.InputFormat.JSON` or `nextmv.InputFormat.TEXT`, then the
|
|
1745
|
-
input data is extracted from the `.data` property.
|
|
1746
|
-
|
|
1747
|
-
If you want to work with `nextmv.InputFormat.CSV_ARCHIVE` or
|
|
1748
|
-
`nextmv.InputFormat.MULTI_FILE`, you should use the `input_dir_path`
|
|
1749
|
-
argument instead. This argument takes precedence over the `input`.
|
|
1750
|
-
If `input_dir_path` is specified, this function looks for files in that
|
|
1751
|
-
directory and tars them, to later be uploaded using the
|
|
1752
|
-
`upload_large_input` method. If both the `input_dir_path` and `input`
|
|
1753
|
-
arguments are provided, the `input` is ignored.
|
|
1754
|
-
|
|
1755
|
-
When `input_dir_path` is specified, the `configuration` argument must
|
|
1756
|
-
also be provided. More specifically, the
|
|
1757
|
-
`RunConfiguration.format.format_input.input_type` parameter
|
|
1758
|
-
dictates what kind of input is being submitted to the Nextmv Cloud.
|
|
1759
|
-
Make sure that this parameter is specified when working with the
|
|
1760
|
-
following input formats:
|
|
1761
|
-
|
|
1762
|
-
- `nextmv.InputFormat.CSV_ARCHIVE`
|
|
1763
|
-
- `nextmv.InputFormat.MULTI_FILE`
|
|
1764
|
-
|
|
1765
|
-
When working with JSON or text data, use the `input` argument
|
|
1766
|
-
directly.
|
|
1767
|
-
|
|
1768
|
-
In general, if an input is too large, it will be uploaded with the
|
|
1769
|
-
`upload_large_input` method.
|
|
1770
|
-
instance_id: Optional[str]
|
|
1771
|
-
ID of the instance to use for the run. If not provided, the default
|
|
1772
|
-
instance ID associated to the Class (`default_instance_id`) is
|
|
1773
|
-
used.
|
|
1774
|
-
name: Optional[str]
|
|
1775
|
-
Name of the run.
|
|
1776
|
-
description: Optional[str]
|
|
1777
|
-
Description of the run.
|
|
1778
|
-
upload_id: Optional[str]
|
|
1779
|
-
ID to use when running a large input. If the `input` exceeds the
|
|
1780
|
-
maximum allowed size, then it is uploaded and the corresponding
|
|
1781
|
-
`upload_id` is used.
|
|
1782
|
-
options: Optional[Union[Options, dict[str, str]]]
|
|
1783
|
-
Options to use for the run. This can be a `nextmv.Options` object
|
|
1784
|
-
or a dict. If a dict is used, the keys must be strings and the
|
|
1785
|
-
values must be strings as well. If a `nextmv.Options` object is
|
|
1786
|
-
used, the options are extracted from the `.to_cloud_dict()` method.
|
|
1787
|
-
Note that specifying `options` overrides the `input.options` (if
|
|
1788
|
-
the `input` is of type `nextmv.Input`).
|
|
1789
|
-
configuration: Optional[Union[RunConfiguration, dict[str, Any]]]
|
|
1790
|
-
Configuration to use for the run. This can be a
|
|
1791
|
-
`cloud.RunConfiguration` object or a dict. If the object is used,
|
|
1792
|
-
then the `.to_dict()` method is applied to extract the
|
|
1793
|
-
configuration.
|
|
1794
|
-
batch_experiment_id: Optional[str]
|
|
1795
|
-
ID of a batch experiment to associate the run with. This is used
|
|
1796
|
-
when the run is part of a batch experiment.
|
|
1797
|
-
external_result: Optional[Union[ExternalRunResult, dict[str, Any]]]
|
|
1798
|
-
External result to use for the run. This can be a
|
|
1799
|
-
`nextmv.ExternalRunResult` object or a dict. If the object is used,
|
|
1800
|
-
then the `.to_dict()` method is applied to extract the
|
|
1801
|
-
configuration. This is used when the run is an external run. We
|
|
1802
|
-
suggest that instead of specifying this parameter, you use the
|
|
1803
|
-
`track_run` method of the class.
|
|
1804
|
-
json_configurations: Optional[dict[str, Any]]
|
|
1805
|
-
Optional configurations for JSON serialization. This is used to
|
|
1806
|
-
customize the serialization before data is sent.
|
|
1807
|
-
input_dir_path: Optional[str]
|
|
1808
|
-
Path to a directory containing input files. If specified, the
|
|
1809
|
-
function will package the files in the directory into a tar file
|
|
1810
|
-
and upload it as a large input. This is useful for input formats
|
|
1811
|
-
like `nextmv.InputFormat.CSV_ARCHIVE` or `nextmv.InputFormat.MULTI_FILE`.
|
|
1812
|
-
If both `input` and `input_dir_path` are specified, the `input` is
|
|
1813
|
-
ignored, and the files in the directory are used instead.
|
|
1814
|
-
|
|
1815
|
-
Returns
|
|
1816
|
-
----------
|
|
1817
|
-
str
|
|
1818
|
-
ID (`run_id`) of the run that was submitted.
|
|
1819
|
-
|
|
1820
|
-
Raises
|
|
1821
|
-
----------
|
|
1822
|
-
requests.HTTPError
|
|
1823
|
-
If the response status code is not 2xx.
|
|
1824
|
-
ValueError
|
|
1825
|
-
If the `input` is of type `nextmv.Input` and the .input_format` is
|
|
1826
|
-
not `JSON`. If the final `options` are not of type `dict[str,str]`.
|
|
1827
|
-
"""
|
|
1828
|
-
|
|
1829
|
-
tar_file = ""
|
|
1830
|
-
if input_dir_path is not None and input_dir_path != "":
|
|
1831
|
-
if not os.path.exists(input_dir_path):
|
|
1832
|
-
raise ValueError(f"Directory {input_dir_path} does not exist.")
|
|
1833
|
-
|
|
1834
|
-
if not os.path.isdir(input_dir_path):
|
|
1835
|
-
raise ValueError(f"Path {input_dir_path} is not a directory.")
|
|
1836
|
-
|
|
1837
|
-
tar_file = self.__package_inputs(input_dir_path)
|
|
1838
|
-
|
|
1839
|
-
input_data = self.__extract_input_data(input)
|
|
1840
|
-
|
|
1841
|
-
input_size = 0
|
|
1842
|
-
if input_data is not None:
|
|
1843
|
-
input_size = get_size(input_data)
|
|
1844
|
-
|
|
1845
|
-
upload_id_used = upload_id is not None
|
|
1846
|
-
if self.__upload_url_required(upload_id_used, input_size, tar_file, input):
|
|
1847
|
-
upload_url = self.upload_url()
|
|
1848
|
-
self.upload_large_input(input=input_data, upload_url=upload_url, tar_file=tar_file)
|
|
1849
|
-
upload_id = upload_url.upload_id
|
|
1850
|
-
upload_id_used = True
|
|
1851
|
-
|
|
1852
|
-
options_dict = self.__extract_options_dict(options, json_configurations)
|
|
1853
|
-
|
|
1854
|
-
# Builds the payload progressively based on the different arguments
|
|
1855
|
-
# that must be provided.
|
|
1856
|
-
payload = {}
|
|
1857
|
-
if upload_id_used:
|
|
1858
|
-
payload["upload_id"] = upload_id
|
|
1859
|
-
else:
|
|
1860
|
-
payload["input"] = input_data
|
|
1861
|
-
|
|
1862
|
-
if name is not None:
|
|
1863
|
-
payload["name"] = name
|
|
1864
|
-
if description is not None:
|
|
1865
|
-
payload["description"] = description
|
|
1866
|
-
if len(options_dict) > 0:
|
|
1867
|
-
for k, v in options_dict.items():
|
|
1868
|
-
if not isinstance(v, str):
|
|
1869
|
-
raise ValueError(f"options must be dict[str,str], option {k} has type {type(v)} instead.")
|
|
1870
|
-
payload["options"] = options_dict
|
|
1871
|
-
|
|
1872
|
-
configuration_dict = self.__extract_run_config(input, configuration, input_dir_path)
|
|
1873
|
-
payload["configuration"] = configuration_dict
|
|
1874
|
-
|
|
1875
|
-
if batch_experiment_id is not None:
|
|
1876
|
-
payload["batch_experiment_id"] = batch_experiment_id
|
|
1877
|
-
if external_result is not None:
|
|
1878
|
-
external_dict = (
|
|
1879
|
-
external_result.to_dict() if isinstance(external_result, ExternalRunResult) else external_result
|
|
1880
|
-
)
|
|
1881
|
-
payload["result"] = external_dict
|
|
1882
|
-
|
|
1883
|
-
query_params = {}
|
|
1884
|
-
if instance_id is not None or self.default_instance_id is not None:
|
|
1885
|
-
query_params["instance_id"] = instance_id if instance_id is not None else self.default_instance_id
|
|
1886
|
-
|
|
1887
|
-
response = self.client.request(
|
|
1888
|
-
method="POST",
|
|
1889
|
-
endpoint=f"{self.endpoint}/runs",
|
|
1890
|
-
payload=payload,
|
|
1891
|
-
query_params=query_params,
|
|
1892
|
-
json_configurations=json_configurations,
|
|
1893
|
-
)
|
|
1894
|
-
|
|
1895
|
-
return response.json()["run_id"]
|
|
1896
|
-
|
|
1897
|
-
def new_run_with_result(
|
|
1898
|
-
self,
|
|
1899
|
-
input: Input | dict[str, Any] | BaseModel | str = None,
|
|
1900
|
-
instance_id: str | None = None,
|
|
1901
|
-
name: str | None = None,
|
|
1902
|
-
description: str | None = None,
|
|
1903
|
-
upload_id: str | None = None,
|
|
1904
|
-
run_options: Options | dict[str, str] | None = None,
|
|
1905
|
-
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
1906
|
-
configuration: RunConfiguration | dict[str, Any] | None = None,
|
|
1907
|
-
batch_experiment_id: str | None = None,
|
|
1908
|
-
external_result: ExternalRunResult | dict[str, Any] | None = None,
|
|
1909
|
-
json_configurations: dict[str, Any] | None = None,
|
|
1910
|
-
input_dir_path: str | None = None,
|
|
1911
|
-
output_dir_path: str | None = ".",
|
|
1912
|
-
) -> RunResult:
|
|
1913
|
-
"""
|
|
1914
|
-
Submit an input to start a new run of the application and poll for the
|
|
1915
|
-
result. This is a convenience method that combines the `new_run` and
|
|
1916
|
-
`run_result_with_polling` methods, applying polling logic to check when
|
|
1917
|
-
the run succeeded.
|
|
1918
|
-
|
|
1919
|
-
Parameters
|
|
1920
|
-
----------
|
|
1921
|
-
input: Union[Input, dict[str, Any], BaseModel, str]
|
|
1922
|
-
Input to use for the run. This can be a `nextmv.Input` object,
|
|
1923
|
-
`dict`, `BaseModel` or `str`.
|
|
1924
|
-
|
|
1925
|
-
If `nextmv.Input` is used, and the `input_format` is either
|
|
1926
|
-
`nextmv.InputFormat.JSON` or `nextmv.InputFormat.TEXT`, then the
|
|
1927
|
-
input data is extracted from the `.data` property.
|
|
1928
|
-
|
|
1929
|
-
If you want to work with `nextmv.InputFormat.CSV_ARCHIVE` or
|
|
1930
|
-
`nextmv.InputFormat.MULTI_FILE`, you should use the `input_dir_path`
|
|
1931
|
-
argument instead. This argument takes precedence over the `input`.
|
|
1932
|
-
If `input_dir_path` is specified, this function looks for files in that
|
|
1933
|
-
directory and tars them, to later be uploaded using the
|
|
1934
|
-
`upload_large_input` method. If both the `input_dir_path` and `input`
|
|
1935
|
-
arguments are provided, the `input` is ignored.
|
|
1936
|
-
|
|
1937
|
-
When `input_dir_path` is specified, the `configuration` argument must
|
|
1938
|
-
also be provided. More specifically, the
|
|
1939
|
-
`RunConfiguration.format.format_input.input_type` parameter
|
|
1940
|
-
dictates what kind of input is being submitted to the Nextmv Cloud.
|
|
1941
|
-
Make sure that this parameter is specified when working with the
|
|
1942
|
-
following input formats:
|
|
1943
|
-
|
|
1944
|
-
- `nextmv.InputFormat.CSV_ARCHIVE`
|
|
1945
|
-
- `nextmv.InputFormat.MULTI_FILE`
|
|
1946
|
-
|
|
1947
|
-
When working with JSON or text data, use the `input` argument
|
|
1948
|
-
directly.
|
|
1949
|
-
|
|
1950
|
-
In general, if an input is too large, it will be uploaded with the
|
|
1951
|
-
`upload_large_input` method.
|
|
1952
|
-
instance_id: Optional[str]
|
|
1953
|
-
ID of the instance to use for the run. If not provided, the default
|
|
1954
|
-
instance ID associated to the Class (`default_instance_id`) is
|
|
1955
|
-
used.
|
|
1956
|
-
name: Optional[str]
|
|
1957
|
-
Name of the run.
|
|
1958
|
-
description: Optional[str]
|
|
1959
|
-
Description of the run.
|
|
1960
|
-
upload_id: Optional[str]
|
|
1961
|
-
ID to use when running a large input. If the `input` exceeds the
|
|
1962
|
-
maximum allowed size, then it is uploaded and the corresponding
|
|
1963
|
-
`upload_id` is used.
|
|
1964
|
-
run_options: Optional[Union[Options, dict[str, str]]]
|
|
1965
|
-
Options to use for the run. This can be a `nextmv.Options` object
|
|
1966
|
-
or a dict. If a dict is used, the keys must be strings and the
|
|
1967
|
-
values must be strings as well. If a `nextmv.Options` object is
|
|
1968
|
-
used, the options are extracted from the `.to_cloud_dict()` method.
|
|
1969
|
-
Note that specifying `options` overrides the `input.options` (if
|
|
1970
|
-
the `input` is of type `nextmv.Input`).
|
|
1971
|
-
polling_options: PollingOptions
|
|
1972
|
-
Options to use when polling for the run result. This is a
|
|
1973
|
-
convenience method that combines the `new_run` and
|
|
1974
|
-
`run_result_with_polling` methods, applying polling logic to check
|
|
1975
|
-
when the run succeeded.
|
|
1976
|
-
configuration: Optional[Union[RunConfiguration, dict[str, Any]]]
|
|
1977
|
-
Configuration to use for the run. This can be a
|
|
1978
|
-
`cloud.RunConfiguration` object or a dict. If the object is used,
|
|
1979
|
-
then the `.to_dict()` method is applied to extract the
|
|
1980
|
-
configuration.
|
|
1981
|
-
batch_experiment_id: Optional[str]
|
|
1982
|
-
ID of a batch experiment to associate the run with. This is used
|
|
1983
|
-
when the run is part of a batch experiment.
|
|
1984
|
-
external_result: Optional[Union[ExternalRunResult, dict[str, Any]]] = None
|
|
1985
|
-
External result to use for the run. This can be a
|
|
1986
|
-
`cloud.ExternalRunResult` object or a dict. If the object is used,
|
|
1987
|
-
then the `.to_dict()` method is applied to extract the
|
|
1988
|
-
configuration. This is used when the run is an external run. We
|
|
1989
|
-
suggest that instead of specifying this parameter, you use the
|
|
1990
|
-
`track_run_with_result` method of the class.
|
|
1991
|
-
json_configurations: Optional[dict[str, Any]]
|
|
1992
|
-
Optional configurations for JSON serialization. This is used to
|
|
1993
|
-
customize the serialization before data is sent.
|
|
1994
|
-
input_dir_path: Optional[str]
|
|
1995
|
-
Path to a directory containing input files. If specified, the
|
|
1996
|
-
function will package the files in the directory into a tar file
|
|
1997
|
-
and upload it as a large input. This is useful for input formats
|
|
1998
|
-
like `nextmv.InputFormat.CSV_ARCHIVE` or `nextmv.InputFormat.MULTI_FILE`.
|
|
1999
|
-
If both `input` and `input_dir_path` are specified, the `input` is
|
|
2000
|
-
ignored, and the files in the directory are used instead.
|
|
2001
|
-
output_dir_path : Optional[str], default="."
|
|
2002
|
-
Path to a directory where non-JSON output files will be saved. This is
|
|
2003
|
-
required if the output is non-JSON. If the directory does not exist, it
|
|
2004
|
-
will be created. Uses the current directory by default.
|
|
2005
|
-
|
|
2006
|
-
Returns
|
|
2007
|
-
----------
|
|
2008
|
-
RunResult
|
|
2009
|
-
Result of the run.
|
|
2010
|
-
|
|
2011
|
-
Raises
|
|
2012
|
-
----------
|
|
2013
|
-
ValueError
|
|
2014
|
-
If the `input` is of type `nextmv.Input` and the `.input_format` is
|
|
2015
|
-
not `JSON`. If the final `options` are not of type `dict[str,str]`.
|
|
2016
|
-
requests.HTTPError
|
|
2017
|
-
If the response status code is not 2xx.
|
|
2018
|
-
TimeoutError
|
|
2019
|
-
If the run does not succeed after the polling strategy is exhausted
|
|
2020
|
-
based on time duration.
|
|
2021
|
-
RuntimeError
|
|
2022
|
-
If the run does not succeed after the polling strategy is exhausted
|
|
2023
|
-
based on number of tries.
|
|
2024
|
-
"""
|
|
2025
|
-
|
|
2026
|
-
run_id = self.new_run(
|
|
2027
|
-
input=input,
|
|
2028
|
-
instance_id=instance_id,
|
|
2029
|
-
name=name,
|
|
2030
|
-
description=description,
|
|
2031
|
-
upload_id=upload_id,
|
|
2032
|
-
options=run_options,
|
|
2033
|
-
configuration=configuration,
|
|
2034
|
-
batch_experiment_id=batch_experiment_id,
|
|
2035
|
-
external_result=external_result,
|
|
2036
|
-
json_configurations=json_configurations,
|
|
2037
|
-
input_dir_path=input_dir_path,
|
|
2038
|
-
)
|
|
2039
|
-
|
|
2040
|
-
return self.run_result_with_polling(
|
|
2041
|
-
run_id=run_id,
|
|
2042
|
-
polling_options=polling_options,
|
|
2043
|
-
output_dir_path=output_dir_path,
|
|
2044
|
-
)
|
|
2045
|
-
|
|
2046
|
-
def new_scenario_test(
|
|
2047
|
-
self,
|
|
2048
|
-
id: str,
|
|
2049
|
-
name: str,
|
|
2050
|
-
scenarios: list[Scenario],
|
|
2051
|
-
description: str | None = None,
|
|
2052
|
-
repetitions: int | None = 0,
|
|
2053
|
-
) -> str:
|
|
2054
|
-
"""
|
|
2055
|
-
Create a new scenario test. The test is based on `scenarios` and you
|
|
2056
|
-
may specify `repetitions` to run the test multiple times. 0 repetitions
|
|
2057
|
-
means that the tests will be executed once. 1 repetition means that the
|
|
2058
|
-
test will be repeated once, i.e.: it will be executed twice. 2
|
|
2059
|
-
repetitions equals 3 executions, so on, and so forth.
|
|
2060
|
-
|
|
2061
|
-
For each scenario, consider the `scenario_input` and `configuration`.
|
|
2062
|
-
The `scenario_input.scenario_input_type` allows you to specify the data
|
|
2063
|
-
that will be used for that scenario.
|
|
2064
|
-
|
|
2065
|
-
- `ScenarioInputType.INPUT_SET`: the data should be taken from an
|
|
2066
|
-
existing input set.
|
|
2067
|
-
- `ScenarioInputType.INPUT`: the data should be taken from a list of
|
|
2068
|
-
existing inputs. When using this type, an input set will be created
|
|
2069
|
-
from this set of managed inputs.
|
|
2070
|
-
- `ScenarioInputType.New`: a new set of data will be uploaded as a set
|
|
2071
|
-
of managed inputs. A new input set will be created from this set of
|
|
2072
|
-
managed inputs.
|
|
2073
|
-
|
|
2074
|
-
On the other hand, the `configuration` allows you to specify multiple
|
|
2075
|
-
option variations for the scenario. Please see the
|
|
2076
|
-
`ScenarioConfiguration` class for more information.
|
|
2077
|
-
|
|
2078
|
-
The scenario tests uses the batch experiments API under the hood.
|
|
2079
|
-
|
|
2080
|
-
Parameters
|
|
2081
|
-
----------
|
|
2082
|
-
id: str
|
|
2083
|
-
ID of the scenario test.
|
|
2084
|
-
name: str
|
|
2085
|
-
Name of the scenario test.
|
|
2086
|
-
scenarios: list[Scenario]
|
|
2087
|
-
List of scenarios to use for the scenario test. At least one
|
|
2088
|
-
scenario should be provided.
|
|
2089
|
-
description: Optional[str]
|
|
2090
|
-
Optional description of the scenario test.
|
|
2091
|
-
repetitions: Optional[int]
|
|
2092
|
-
Number of repetitions to use for the scenario test. 0
|
|
2093
|
-
repetitions means that the tests will be executed once. 1
|
|
2094
|
-
repetition means that the test will be repeated once, i.e.: it
|
|
2095
|
-
will be executed twice. 2 repetitions equals 3 executions, so on,
|
|
2096
|
-
and so forth.
|
|
2097
|
-
|
|
2098
|
-
Returns
|
|
2099
|
-
-------
|
|
2100
|
-
str
|
|
2101
|
-
ID of the scenario test.
|
|
2102
|
-
|
|
2103
|
-
Raises
|
|
2104
|
-
------
|
|
2105
|
-
requests.HTTPError
|
|
2106
|
-
If the response status code is not 2xx.
|
|
2107
|
-
ValueError
|
|
2108
|
-
If no scenarios are provided.
|
|
2109
|
-
"""
|
|
2110
|
-
|
|
2111
|
-
if len(scenarios) < 1:
|
|
2112
|
-
raise ValueError("At least one scenario must be provided")
|
|
2113
|
-
|
|
2114
|
-
scenarios_by_id = _scenarios_by_id(scenarios)
|
|
2115
|
-
|
|
2116
|
-
# Save all the information needed by scenario.
|
|
2117
|
-
input_sets = {}
|
|
2118
|
-
instances = {}
|
|
2119
|
-
for scenario_id, scenario in scenarios_by_id.items():
|
|
2120
|
-
instance = self.instance(instance_id=scenario.instance_id)
|
|
2121
|
-
|
|
2122
|
-
# Each scenario is associated to an input set, so we must either
|
|
2123
|
-
# get it or create it.
|
|
2124
|
-
input_set = self.__input_set_for_scenario(scenario, scenario_id)
|
|
2125
|
-
|
|
2126
|
-
instances[scenario_id] = instance
|
|
2127
|
-
input_sets[scenario_id] = input_set
|
|
2128
|
-
|
|
2129
|
-
# Calculate the combinations of all the option sets across scenarios.
|
|
2130
|
-
opt_sets_by_scenario = _option_sets(scenarios)
|
|
2131
|
-
|
|
2132
|
-
# The scenario tests results in multiple individual runs.
|
|
2133
|
-
runs = []
|
|
2134
|
-
run_counter = 0
|
|
2135
|
-
opt_sets = {}
|
|
2136
|
-
for scenario_id, scenario_opt_sets in opt_sets_by_scenario.items():
|
|
2137
|
-
opt_sets = {**opt_sets, **scenario_opt_sets}
|
|
2138
|
-
input_set = input_sets[scenario_id]
|
|
2139
|
-
scenario = scenarios_by_id[scenario_id]
|
|
2140
|
-
|
|
2141
|
-
for set_key in scenario_opt_sets.keys():
|
|
2142
|
-
inputs = input_set.input_ids if len(input_set.input_ids) > 0 else input_set.inputs
|
|
2143
|
-
for input in inputs:
|
|
2144
|
-
input_id = input.id if isinstance(input, ManagedInput) else input
|
|
2145
|
-
for repetition in range(repetitions + 1):
|
|
2146
|
-
run_counter += 1
|
|
2147
|
-
run = BatchExperimentRun(
|
|
2148
|
-
input_id=input_id,
|
|
2149
|
-
input_set_id=input_set.id,
|
|
2150
|
-
instance_id=scenario.instance_id,
|
|
2151
|
-
option_set=set_key,
|
|
2152
|
-
scenario_id=scenario_id,
|
|
2153
|
-
repetition=repetition,
|
|
2154
|
-
run_number=f"{run_counter}",
|
|
2155
|
-
)
|
|
2156
|
-
runs.append(run)
|
|
2157
|
-
|
|
2158
|
-
return self.new_batch_experiment(
|
|
2159
|
-
id=id,
|
|
2160
|
-
name=name,
|
|
2161
|
-
description=description,
|
|
2162
|
-
type="scenario",
|
|
2163
|
-
option_sets=opt_sets,
|
|
2164
|
-
runs=runs,
|
|
2165
|
-
)
|
|
2166
|
-
|
|
2167
|
-
def new_scenario_test_with_result(
|
|
2168
|
-
self,
|
|
2169
|
-
id: str,
|
|
2170
|
-
name: str,
|
|
2171
|
-
scenarios: list[Scenario],
|
|
2172
|
-
description: str | None = None,
|
|
2173
|
-
repetitions: int | None = 0,
|
|
2174
|
-
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2175
|
-
) -> BatchExperiment:
|
|
2176
|
-
"""
|
|
2177
|
-
Convenience method to create a new scenario test and poll for the
|
|
2178
|
-
result.
|
|
2179
|
-
|
|
2180
|
-
This method combines the `new_scenario_test` and
|
|
2181
|
-
`scenario_test_with_polling` methods, applying polling logic to
|
|
2182
|
-
check when the test succeeded.
|
|
2183
|
-
|
|
2184
|
-
The scenario tests uses the batch experiments API under the hood.
|
|
2185
|
-
|
|
2186
|
-
Parameters
|
|
2187
|
-
----------
|
|
2188
|
-
id: str
|
|
2189
|
-
ID of the scenario test.
|
|
2190
|
-
name: str
|
|
2191
|
-
Name of the scenario test.
|
|
2192
|
-
scenarios: list[Scenario]
|
|
2193
|
-
List of scenarios to use for the scenario test. At least one
|
|
2194
|
-
scenario should be provided.
|
|
2195
|
-
description: Optional[str]
|
|
2196
|
-
Optional description of the scenario test.
|
|
2197
|
-
repetitions: Optional[int]
|
|
2198
|
-
Number of repetitions to use for the scenario test. 0
|
|
2199
|
-
repetitions means that the tests will be executed once. 1
|
|
2200
|
-
repetition means that the test will be repeated once, i.e.: it
|
|
2201
|
-
will be executed twice. 2 repetitions equals 3 executions, so on,
|
|
2202
|
-
and so forth.
|
|
2203
|
-
|
|
2204
|
-
Returns
|
|
2205
|
-
-------
|
|
2206
|
-
BatchExperiment
|
|
2207
|
-
The completed scenario test as a BatchExperiment.
|
|
2208
|
-
|
|
2209
|
-
Raises
|
|
2210
|
-
------
|
|
2211
|
-
requests.HTTPError
|
|
2212
|
-
If the response status code is not 2xx.
|
|
2213
|
-
ValueError
|
|
2214
|
-
If no scenarios are provided.
|
|
2215
|
-
"""
|
|
2216
|
-
|
|
2217
|
-
test_id = self.new_scenario_test(
|
|
2218
|
-
id=id,
|
|
2219
|
-
name=name,
|
|
2220
|
-
scenarios=scenarios,
|
|
2221
|
-
description=description,
|
|
2222
|
-
repetitions=repetitions,
|
|
2223
|
-
)
|
|
2224
|
-
|
|
2225
|
-
return self.scenario_test_with_polling(
|
|
2226
|
-
scenario_test_id=test_id,
|
|
2227
|
-
polling_options=polling_options,
|
|
2228
|
-
)
|
|
2229
|
-
|
|
2230
|
-
def new_secrets_collection(
|
|
2231
|
-
self,
|
|
2232
|
-
secrets: list[Secret],
|
|
2233
|
-
id: str,
|
|
2234
|
-
name: str,
|
|
2235
|
-
description: str | None = None,
|
|
2236
|
-
) -> SecretsCollectionSummary:
|
|
2237
|
-
"""
|
|
2238
|
-
Create a new secrets collection.
|
|
2239
|
-
|
|
2240
|
-
This method creates a new secrets collection with the provided secrets.
|
|
2241
|
-
A secrets collection is a group of key-value pairs that can be used by
|
|
2242
|
-
your application instances during execution. If no secrets are provided,
|
|
2243
|
-
a ValueError is raised.
|
|
2244
|
-
|
|
2245
|
-
Parameters
|
|
2246
|
-
----------
|
|
2247
|
-
secrets : list[Secret]
|
|
2248
|
-
List of secrets to use for the secrets collection. Each secret
|
|
2249
|
-
should be an instance of the Secret class containing a key and value.
|
|
2250
|
-
id : str
|
|
2251
|
-
ID of the secrets collection.
|
|
2252
|
-
name : str
|
|
2253
|
-
Name of the secrets collection.
|
|
2254
|
-
description : Optional[str], default=None
|
|
2255
|
-
Description of the secrets collection.
|
|
2256
|
-
|
|
2257
|
-
Returns
|
|
2258
|
-
-------
|
|
2259
|
-
SecretsCollectionSummary
|
|
2260
|
-
Summary of the secrets collection including its metadata.
|
|
2261
|
-
|
|
2262
|
-
Raises
|
|
2263
|
-
------
|
|
2264
|
-
ValueError
|
|
2265
|
-
If no secrets are provided.
|
|
2266
|
-
requests.HTTPError
|
|
2267
|
-
If the response status code is not 2xx.
|
|
2268
|
-
|
|
2269
|
-
Examples
|
|
2270
|
-
--------
|
|
2271
|
-
>>> # Create a new secrets collection with API keys
|
|
2272
|
-
>>> from nextmv.cloud import Secret
|
|
2273
|
-
>>> secrets = [
|
|
2274
|
-
... Secret(
|
|
2275
|
-
... location="API_KEY",
|
|
2276
|
-
... value="your-api-key",
|
|
2277
|
-
... secret_type=SecretType.ENV,
|
|
2278
|
-
... ),
|
|
2279
|
-
... Secret(
|
|
2280
|
-
... location="DATABASE_URL",
|
|
2281
|
-
... value="your-database-url",
|
|
2282
|
-
... secret_type=SecretType.ENV,
|
|
2283
|
-
... ),
|
|
2284
|
-
... ]
|
|
2285
|
-
>>> collection = app.new_secrets_collection(
|
|
2286
|
-
... secrets=secrets,
|
|
2287
|
-
... id="api-secrets",
|
|
2288
|
-
... name="API Secrets",
|
|
2289
|
-
... description="Collection of API secrets for external services"
|
|
2290
|
-
... )
|
|
2291
|
-
>>> print(collection.id)
|
|
2292
|
-
'api-secrets'
|
|
2293
|
-
"""
|
|
2294
|
-
|
|
2295
|
-
if len(secrets) == 0:
|
|
2296
|
-
raise ValueError("secrets must be provided")
|
|
2297
|
-
|
|
2298
|
-
payload = {
|
|
2299
|
-
"secrets": [secret.to_dict() for secret in secrets],
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
if id is not None:
|
|
2303
|
-
payload["id"] = id
|
|
2304
|
-
if name is not None:
|
|
2305
|
-
payload["name"] = name
|
|
2306
|
-
if description is not None:
|
|
2307
|
-
payload["description"] = description
|
|
2308
|
-
|
|
2309
|
-
response = self.client.request(
|
|
2310
|
-
method="POST",
|
|
2311
|
-
endpoint=f"{self.endpoint}/secrets",
|
|
2312
|
-
payload=payload,
|
|
2313
|
-
)
|
|
2314
|
-
|
|
2315
|
-
return SecretsCollectionSummary.from_dict(response.json())
|
|
2316
|
-
|
|
2317
|
-
def new_version(
|
|
2318
|
-
self,
|
|
2319
|
-
id: str | None = None,
|
|
2320
|
-
name: str | None = None,
|
|
2321
|
-
description: str | None = None,
|
|
2322
|
-
exist_ok: bool = False,
|
|
2323
|
-
) -> Version:
|
|
2324
|
-
"""
|
|
2325
|
-
Create a new version using the current dev binary.
|
|
2326
|
-
|
|
2327
|
-
This method creates a new version of the application using the current development
|
|
2328
|
-
binary. Application versions represent different iterations of your application's
|
|
2329
|
-
code and configuration that can be deployed.
|
|
2330
|
-
|
|
2331
|
-
Parameters
|
|
2332
|
-
----------
|
|
2333
|
-
id : Optional[str], default=None
|
|
2334
|
-
ID of the version. If not provided, a unique ID will be generated.
|
|
2335
|
-
name : Optional[str], default=None
|
|
2336
|
-
Name of the version. If not provided, a name will be generated.
|
|
2337
|
-
description : Optional[str], default=None
|
|
2338
|
-
Description of the version. If not provided, a description will be generated.
|
|
2339
|
-
exist_ok : bool, default=False
|
|
2340
|
-
If True and a version with the same ID already exists,
|
|
2341
|
-
return the existing version instead of creating a new one.
|
|
2342
|
-
If True, the 'id' parameter must be provided.
|
|
2343
|
-
|
|
2344
|
-
Returns
|
|
2345
|
-
-------
|
|
2346
|
-
Version
|
|
2347
|
-
The newly created (or existing) version.
|
|
2348
|
-
|
|
2349
|
-
Raises
|
|
2350
|
-
------
|
|
2351
|
-
ValueError
|
|
2352
|
-
If exist_ok is True and id is None.
|
|
2353
|
-
requests.HTTPError
|
|
2354
|
-
If the response status code is not 2xx.
|
|
2355
|
-
|
|
2356
|
-
Examples
|
|
2357
|
-
--------
|
|
2358
|
-
>>> # Create a new version
|
|
2359
|
-
>>> version = app.new_version(
|
|
2360
|
-
... id="v1.0.0",
|
|
2361
|
-
... name="Initial Release",
|
|
2362
|
-
... description="First stable version"
|
|
2363
|
-
... )
|
|
2364
|
-
>>> print(version.id)
|
|
2365
|
-
'v1.0.0'
|
|
2366
|
-
|
|
2367
|
-
>>> # Get or create a version with exist_ok
|
|
2368
|
-
>>> version = app.new_version(
|
|
2369
|
-
... id="v1.0.0",
|
|
2370
|
-
... exist_ok=True
|
|
2371
|
-
... )
|
|
2372
|
-
"""
|
|
2373
|
-
|
|
2374
|
-
if exist_ok and id is None:
|
|
2375
|
-
raise ValueError("If exist_ok is True, id must be provided")
|
|
2376
|
-
|
|
2377
|
-
if exist_ok and self.version_exists(version_id=id):
|
|
2378
|
-
return self.version(version_id=id)
|
|
2379
|
-
|
|
2380
|
-
if id is None:
|
|
2381
|
-
id = safe_id(prefix="version")
|
|
2382
|
-
if name is None:
|
|
2383
|
-
name = id
|
|
2384
|
-
|
|
2385
|
-
payload = {
|
|
2386
|
-
"id": id,
|
|
2387
|
-
"name": name,
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
if description is not None:
|
|
2391
|
-
payload["description"] = description
|
|
2392
|
-
|
|
2393
|
-
response = self.client.request(
|
|
2394
|
-
method="POST",
|
|
2395
|
-
endpoint=f"{self.endpoint}/versions",
|
|
2396
|
-
payload=payload,
|
|
2397
|
-
)
|
|
2398
|
-
|
|
2399
|
-
return Version.from_dict(response.json())
|
|
2400
|
-
|
|
2401
|
-
def push(
|
|
2402
|
-
self,
|
|
2403
|
-
manifest: Manifest | None = None,
|
|
2404
|
-
app_dir: str | None = None,
|
|
2405
|
-
verbose: bool = False,
|
|
2406
|
-
model: Model | None = None,
|
|
2407
|
-
model_configuration: ModelConfiguration | None = None,
|
|
2408
|
-
) -> None:
|
|
2409
|
-
"""
|
|
2410
|
-
Push an app to Nextmv Cloud.
|
|
2411
|
-
|
|
2412
|
-
If the manifest is not provided, an `app.yaml` file will be searched
|
|
2413
|
-
for in the provided path. If there is no manifest file found, an
|
|
2414
|
-
exception will be raised.
|
|
2415
|
-
|
|
2416
|
-
There are two ways to push an app to Nextmv Cloud:
|
|
2417
|
-
1. Specifying `app_dir`, which is the path to an app's root directory.
|
|
2418
|
-
This acts as an external strategy, where the app is composed of files
|
|
2419
|
-
in a directory and those apps are packaged and pushed to Nextmv Cloud.
|
|
2420
|
-
2. Specifying a `model` and `model_configuration`. This acts as an
|
|
2421
|
-
internal (or Python-native) strategy, where the app is actually a
|
|
2422
|
-
`nextmv.Model`. The model is encoded, some dependencies and
|
|
2423
|
-
accompanying files are packaged, and the app is pushed to Nextmv Cloud.
|
|
2424
|
-
|
|
2425
|
-
Parameters
|
|
2426
|
-
----------
|
|
2427
|
-
manifest : Optional[Manifest], default=None
|
|
2428
|
-
The manifest for the app. If None, an `app.yaml` file in the provided
|
|
2429
|
-
app directory will be used.
|
|
2430
|
-
app_dir : Optional[str], default=None
|
|
2431
|
-
The path to the app's root directory. If None, the current directory
|
|
2432
|
-
will be used. This is for the external strategy approach.
|
|
2433
|
-
verbose : bool, default=False
|
|
2434
|
-
Whether to print verbose output during the push process.
|
|
2435
|
-
model : Optional[Model], default=None
|
|
2436
|
-
The Python-native model to push. Must be specified together with
|
|
2437
|
-
`model_configuration`. This is for the internal strategy approach.
|
|
2438
|
-
model_configuration : Optional[ModelConfiguration], default=None
|
|
2439
|
-
Configuration for the Python-native model. Must be specified together
|
|
2440
|
-
with `model`.
|
|
2441
|
-
|
|
2442
|
-
Returns
|
|
2443
|
-
-------
|
|
2444
|
-
None
|
|
2445
|
-
|
|
2446
|
-
Raises
|
|
2447
|
-
------
|
|
2448
|
-
ValueError
|
|
2449
|
-
If neither app_dir nor model/model_configuration is provided correctly,
|
|
2450
|
-
or if only one of model and model_configuration is provided.
|
|
2451
|
-
TypeError
|
|
2452
|
-
If model is not an instance of nextmv.Model or if model_configuration
|
|
2453
|
-
is not an instance of nextmv.ModelConfiguration.
|
|
2454
|
-
Exception
|
|
2455
|
-
If there's an error in the build, packaging, or cleanup process.
|
|
2456
|
-
|
|
2457
|
-
Examples
|
|
2458
|
-
--------
|
|
2459
|
-
1. Push an app using an external strategy (directory-based):
|
|
2460
|
-
|
|
2461
|
-
>>> import os
|
|
2462
|
-
>>> from nextmv import cloud
|
|
2463
|
-
>>> client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
|
|
2464
|
-
>>> app = cloud.Application(client=client, id="<YOUR-APP-ID>")
|
|
2465
|
-
>>> app.push() # Use verbose=True for step-by-step output.
|
|
2466
|
-
|
|
2467
|
-
2. Push an app using an internal strategy (Python-native model):
|
|
2468
|
-
|
|
2469
|
-
>>> import os
|
|
2470
|
-
>>> import nextroute
|
|
2471
|
-
>>> import nextmv
|
|
2472
|
-
>>> import nextmv.cloud
|
|
2473
|
-
>>>
|
|
2474
|
-
>>> # Define the model that makes decisions
|
|
2475
|
-
>>> class DecisionModel(nextmv.Model):
|
|
2476
|
-
... def solve(self, input: nextmv.Input) -> nextmv.Output:
|
|
2477
|
-
... nextroute_input = nextroute.schema.Input.from_dict(input.data)
|
|
2478
|
-
... nextroute_options = nextroute.Options.extract_from_dict(input.options.to_dict())
|
|
2479
|
-
... nextroute_output = nextroute.solve(nextroute_input, nextroute_options)
|
|
2480
|
-
...
|
|
2481
|
-
... return nextmv.Output(
|
|
2482
|
-
... options=input.options,
|
|
2483
|
-
... solution=nextroute_output.solutions[0].to_dict(),
|
|
2484
|
-
... statistics=nextroute_output.statistics.to_dict(),
|
|
2485
|
-
... )
|
|
2486
|
-
>>>
|
|
2487
|
-
>>> # Define the options that the model needs
|
|
2488
|
-
>>> opt = []
|
|
2489
|
-
>>> default_options = nextroute.Options()
|
|
2490
|
-
>>> for name, default_value in default_options.to_dict().items():
|
|
2491
|
-
... opt.append(nextmv.Option(name.lower(), type(default_value), default_value, name, False))
|
|
2492
|
-
>>> options = nextmv.Options(*opt)
|
|
2493
|
-
>>>
|
|
2494
|
-
>>> # Instantiate the model and model configuration
|
|
2495
|
-
>>> model = DecisionModel()
|
|
2496
|
-
>>> model_configuration = nextmv.ModelConfiguration(
|
|
2497
|
-
... name="python_nextroute_model",
|
|
2498
|
-
... requirements=[
|
|
2499
|
-
... "nextroute==1.8.1",
|
|
2500
|
-
... "nextmv==0.14.0.dev1",
|
|
2501
|
-
... ],
|
|
2502
|
-
... options=options,
|
|
2503
|
-
... )
|
|
2504
|
-
>>>
|
|
2505
|
-
>>> # Push the model to Nextmv Cloud
|
|
2506
|
-
>>> client = cloud.Client(api_key=os.getenv("NEXTMV_API_KEY"))
|
|
2507
|
-
>>> app = cloud.Application(client=client, id="<YOUR-APP-ID>")
|
|
2508
|
-
>>> manifest = nextmv.cloud.default_python_manifest()
|
|
2509
|
-
>>> app.push(
|
|
2510
|
-
... manifest=manifest,
|
|
2511
|
-
... verbose=True,
|
|
2512
|
-
... model=model,
|
|
2513
|
-
... model_configuration=model_configuration,
|
|
2514
|
-
... )
|
|
2515
|
-
"""
|
|
2516
|
-
|
|
2517
|
-
if verbose:
|
|
2518
|
-
log("💽 Starting build for Nextmv application.")
|
|
2519
|
-
|
|
2520
|
-
if app_dir is None or app_dir == "":
|
|
2521
|
-
app_dir = "."
|
|
2522
|
-
|
|
2523
|
-
if manifest is None:
|
|
2524
|
-
manifest = Manifest.from_yaml(app_dir)
|
|
2525
|
-
|
|
2526
|
-
if model is not None and not isinstance(model, Model):
|
|
2527
|
-
raise TypeError("model must be an instance of nextmv.Model")
|
|
2528
|
-
|
|
2529
|
-
if model_configuration is not None and not isinstance(model_configuration, ModelConfiguration):
|
|
2530
|
-
raise TypeError("model_configuration must be an instance of nextmv.ModelConfiguration")
|
|
2531
|
-
|
|
2532
|
-
if (model is None and model_configuration is not None) or (model is not None and model_configuration is None):
|
|
2533
|
-
raise ValueError("model and model_configuration must be provided together")
|
|
2534
|
-
|
|
2535
|
-
package._run_build_command(app_dir, manifest.build, verbose)
|
|
2536
|
-
package._run_pre_push_command(app_dir, manifest.pre_push, verbose)
|
|
2537
|
-
tar_file, output_dir = package._package(app_dir, manifest, model, model_configuration, verbose)
|
|
2538
|
-
self.__update_app_binary(tar_file, manifest, verbose)
|
|
2539
|
-
|
|
2540
|
-
try:
|
|
2541
|
-
shutil.rmtree(output_dir)
|
|
2542
|
-
except OSError as e:
|
|
2543
|
-
raise Exception(f"error deleting output directory: {e}") from e
|
|
2544
|
-
|
|
2545
|
-
def list_assets(self, run_id: str) -> list[RunAsset]:
|
|
2546
|
-
"""
|
|
2547
|
-
List the assets of a run.
|
|
2548
|
-
|
|
2549
|
-
Retrieves a list of assets associated with a specific run. This method ONLY
|
|
2550
|
-
returns the asset metadata, the content needs to be fetched via the
|
|
2551
|
-
`download_asset_content` method.
|
|
2552
|
-
|
|
2553
|
-
Parameters
|
|
2554
|
-
----------
|
|
2555
|
-
run_id : str
|
|
2556
|
-
ID of the run to list assets for.
|
|
2557
|
-
|
|
2558
|
-
Returns
|
|
2559
|
-
-------
|
|
2560
|
-
list[RunAsset]
|
|
2561
|
-
List of assets associated with the run.
|
|
2562
|
-
|
|
2563
|
-
Raises
|
|
2564
|
-
------
|
|
2565
|
-
requests.HTTPError
|
|
2566
|
-
If the response status code is not 2xx.
|
|
2567
|
-
|
|
2568
|
-
Examples
|
|
2569
|
-
--------
|
|
2570
|
-
>>> assets = app.list_assets("run-123")
|
|
2571
|
-
>>> for asset in assets:
|
|
2572
|
-
... print(asset.id, asset.name)
|
|
2573
|
-
b459daa6-1c13-48c6-b4c3-a262ea94cd04 clustering_polygons
|
|
2574
|
-
a1234567-89ab-cdef-0123-456789abcdef histogram
|
|
2575
|
-
"""
|
|
2576
|
-
response = self.client.request(
|
|
2577
|
-
method="GET",
|
|
2578
|
-
endpoint=f"{self.endpoint}/runs/{run_id}/assets",
|
|
2579
|
-
)
|
|
2580
|
-
assets_data = response.json().get("items", [])
|
|
2581
|
-
for asset_data in assets_data:
|
|
2582
|
-
asset_data["run_id"] = run_id
|
|
2583
|
-
return [RunAsset.from_dict(asset) for asset in assets_data]
|
|
2584
|
-
|
|
2585
|
-
def download_asset_content(
|
|
2586
|
-
self,
|
|
2587
|
-
asset: RunAsset,
|
|
2588
|
-
destination: str | pathlib.Path | io.BytesIO | None = None,
|
|
2589
|
-
) -> Any | None:
|
|
2590
|
-
"""
|
|
2591
|
-
Downloads an asset's content to a specified destination.
|
|
2592
|
-
|
|
2593
|
-
Parameters
|
|
2594
|
-
----------
|
|
2595
|
-
asset : RunAsset
|
|
2596
|
-
The asset to be downloaded.
|
|
2597
|
-
destination : Union[str, pathlib.Path, io.BytesIO, None]
|
|
2598
|
-
The destination where the asset will be saved. This can be a file path
|
|
2599
|
-
(as a string or pathlib.Path) or an io.BytesIO object. If None, the asset
|
|
2600
|
-
content will not be saved to a file, but returned immediately. If the asset
|
|
2601
|
-
type is JSON, the content will be returned as a dict.
|
|
2602
|
-
|
|
2603
|
-
Returns
|
|
2604
|
-
-------
|
|
2605
|
-
Any or None
|
|
2606
|
-
If `destination` is None, returns the asset content: for JSON assets, a
|
|
2607
|
-
`dict` parsed from the JSON response; for other asset types, the raw
|
|
2608
|
-
`bytes` content. If `destination` is provided, the content is written
|
|
2609
|
-
to the given destination and the method returns `None`.
|
|
2610
|
-
|
|
2611
|
-
Raises
|
|
2612
|
-
------
|
|
2613
|
-
requests.HTTPError
|
|
2614
|
-
If the response status code is not 2xx.
|
|
2615
|
-
|
|
2616
|
-
Examples
|
|
2617
|
-
--------
|
|
2618
|
-
>>> assets = app.list_assets("run-123")
|
|
2619
|
-
>>> asset = assets[0] # Assume we want to download the first asset
|
|
2620
|
-
>>> # Download to a file path
|
|
2621
|
-
>>> app.download_asset_content(asset, "polygons.geojson")
|
|
2622
|
-
>>> # Download to an in-memory bytes buffer
|
|
2623
|
-
>>> import io
|
|
2624
|
-
>>> buffer = io.BytesIO()
|
|
2625
|
-
>>> app.download_asset_content(asset, buffer)
|
|
2626
|
-
>>> # Download and get content directly (for JSON assets)
|
|
2627
|
-
>>> content = app.download_asset_content(asset)
|
|
2628
|
-
>>> print(content)
|
|
2629
|
-
{'type': 'FeatureCollection', 'features': [...]}
|
|
2630
|
-
"""
|
|
2631
|
-
# First, get the download_url for the asset.
|
|
2632
|
-
download_url_response = self.client.request(
|
|
2633
|
-
method="GET",
|
|
2634
|
-
endpoint=f"{self.endpoint}/runs/{asset.run_id}/assets/{asset.id}",
|
|
2635
|
-
).json()
|
|
2636
|
-
download_url = download_url_response["download_url"]
|
|
2637
|
-
asset_type = download_url_response.get("type", "json")
|
|
2638
|
-
|
|
2639
|
-
# Now, download the asset content using the download_url.
|
|
2640
|
-
download_response = self.client.request(
|
|
2641
|
-
method="GET",
|
|
2642
|
-
endpoint=download_url,
|
|
2643
|
-
headers={"Content-Type": "application/json" if asset_type == "json" else "application/octet-stream"},
|
|
2644
|
-
)
|
|
2645
|
-
|
|
2646
|
-
# Save the content to the specified destination.
|
|
2647
|
-
if destination is None:
|
|
2648
|
-
if asset_type == "json":
|
|
2649
|
-
return download_response.json()
|
|
2650
|
-
return download_response.content
|
|
2651
|
-
elif isinstance(destination, io.BytesIO):
|
|
2652
|
-
destination.write(download_response.content)
|
|
2653
|
-
return None
|
|
2654
|
-
else:
|
|
2655
|
-
with open(destination, "wb") as file:
|
|
2656
|
-
file.write(download_response.content)
|
|
2657
|
-
return None
|
|
2658
|
-
|
|
2659
|
-
def run_input(self, run_id: str) -> dict[str, Any]:
|
|
2660
|
-
"""
|
|
2661
|
-
Get the input of a run.
|
|
2662
|
-
|
|
2663
|
-
Retrieves the input data that was used for a specific run. This method
|
|
2664
|
-
handles both small and large inputs automatically - if the input size
|
|
2665
|
-
exceeds the maximum allowed size, it will fetch the input from a
|
|
2666
|
-
download URL.
|
|
2667
|
-
|
|
2668
|
-
Parameters
|
|
2669
|
-
----------
|
|
2670
|
-
run_id : str
|
|
2671
|
-
ID of the run to retrieve the input for.
|
|
2672
|
-
|
|
2673
|
-
Returns
|
|
2674
|
-
-------
|
|
2675
|
-
dict[str, Any]
|
|
2676
|
-
Input data of the run as a dictionary.
|
|
2677
|
-
|
|
2678
|
-
Raises
|
|
2679
|
-
------
|
|
2680
|
-
requests.HTTPError
|
|
2681
|
-
If the response status code is not 2xx.
|
|
2682
|
-
|
|
2683
|
-
Examples
|
|
2684
|
-
--------
|
|
2685
|
-
>>> input_data = app.run_input("run-123")
|
|
2686
|
-
>>> print(input_data)
|
|
2687
|
-
{'locations': [...], 'vehicles': [...]}
|
|
2688
|
-
"""
|
|
2689
|
-
run_information = self.run_metadata(run_id=run_id)
|
|
2690
|
-
|
|
2691
|
-
query_params = None
|
|
2692
|
-
large = False
|
|
2693
|
-
if run_information.metadata.input_size > _MAX_RUN_SIZE:
|
|
2694
|
-
query_params = {"format": "url"}
|
|
2695
|
-
large = True
|
|
2696
|
-
|
|
2697
|
-
response = self.client.request(
|
|
2698
|
-
method="GET",
|
|
2699
|
-
endpoint=f"{self.endpoint}/runs/{run_id}/input",
|
|
2700
|
-
query_params=query_params,
|
|
2701
|
-
)
|
|
2702
|
-
if not large:
|
|
2703
|
-
return response.json()
|
|
2704
|
-
|
|
2705
|
-
download_url = DownloadURL.from_dict(response.json())
|
|
2706
|
-
download_response = self.client.request(
|
|
2707
|
-
method="GET",
|
|
2708
|
-
endpoint=download_url.url,
|
|
2709
|
-
headers={"Content-Type": "application/json"},
|
|
2710
|
-
)
|
|
2711
|
-
|
|
2712
|
-
return download_response.json()
|
|
2713
|
-
|
|
2714
|
-
def run_metadata(self, run_id: str) -> RunInformation:
|
|
2715
|
-
"""
|
|
2716
|
-
Get the metadata of a run.
|
|
2717
|
-
|
|
2718
|
-
Retrieves information about a run without including the run output.
|
|
2719
|
-
This is useful when you only need the run's status and metadata.
|
|
2720
|
-
|
|
2721
|
-
Parameters
|
|
2722
|
-
----------
|
|
2723
|
-
run_id : str
|
|
2724
|
-
ID of the run to retrieve metadata for.
|
|
2725
|
-
|
|
2726
|
-
Returns
|
|
2727
|
-
-------
|
|
2728
|
-
RunInformation
|
|
2729
|
-
Metadata of the run (run information without output).
|
|
2730
|
-
|
|
2731
|
-
Raises
|
|
2732
|
-
------
|
|
2733
|
-
requests.HTTPError
|
|
2734
|
-
If the response status code is not 2xx.
|
|
2735
|
-
|
|
2736
|
-
Examples
|
|
2737
|
-
--------
|
|
2738
|
-
>>> metadata = app.run_metadata("run-123")
|
|
2739
|
-
>>> print(metadata.metadata.status_v2)
|
|
2740
|
-
StatusV2.succeeded
|
|
2741
|
-
"""
|
|
2742
|
-
|
|
2743
|
-
response = self.client.request(
|
|
2744
|
-
method="GET",
|
|
2745
|
-
endpoint=f"{self.endpoint}/runs/{run_id}/metadata",
|
|
2746
|
-
)
|
|
2747
|
-
|
|
2748
|
-
info = RunInformation.from_dict(response.json())
|
|
2749
|
-
info.console_url = self.__console_url(info.id)
|
|
2750
|
-
|
|
2751
|
-
return info
|
|
2752
|
-
|
|
2753
|
-
def run_logs(self, run_id: str) -> RunLog:
|
|
2754
|
-
"""
|
|
2755
|
-
Get the logs of a run.
|
|
2756
|
-
|
|
2757
|
-
Parameters
|
|
2758
|
-
----------
|
|
2759
|
-
run_id : str
|
|
2760
|
-
ID of the run to get logs for.
|
|
2761
|
-
|
|
2762
|
-
Returns
|
|
2763
|
-
-------
|
|
2764
|
-
RunLog
|
|
2765
|
-
Logs of the run.
|
|
2766
|
-
|
|
2767
|
-
Raises
|
|
2768
|
-
------
|
|
2769
|
-
requests.HTTPError
|
|
2770
|
-
If the response status code is not 2xx.
|
|
2771
|
-
|
|
2772
|
-
Examples
|
|
2773
|
-
--------
|
|
2774
|
-
>>> logs = app.run_logs("run-123")
|
|
2775
|
-
>>> print(logs.stderr)
|
|
2776
|
-
'Warning: resource usage exceeded'
|
|
2777
|
-
"""
|
|
2778
|
-
response = self.client.request(
|
|
2779
|
-
method="GET",
|
|
2780
|
-
endpoint=f"{self.endpoint}/runs/{run_id}/logs",
|
|
2781
|
-
)
|
|
2782
|
-
return RunLog.from_dict(response.json())
|
|
2783
|
-
|
|
2784
|
-
def run_result(self, run_id: str, output_dir_path: str | None = ".") -> RunResult:
|
|
2785
|
-
"""
|
|
2786
|
-
Get the result of a run.
|
|
2787
|
-
|
|
2788
|
-
Retrieves the complete result of a run, including the run output.
|
|
2789
|
-
|
|
2790
|
-
Parameters
|
|
2791
|
-
----------
|
|
2792
|
-
run_id : str
|
|
2793
|
-
ID of the run to get results for.
|
|
2794
|
-
output_dir_path : Optional[str], default="."
|
|
2795
|
-
Path to a directory where non-JSON output files will be saved. This is
|
|
2796
|
-
required if the output is non-JSON. If the directory does not exist, it
|
|
2797
|
-
will be created. Uses the current directory by default.
|
|
2798
|
-
|
|
2799
|
-
Returns
|
|
2800
|
-
-------
|
|
2801
|
-
RunResult
|
|
2802
|
-
Result of the run, including output.
|
|
2803
|
-
|
|
2804
|
-
Raises
|
|
2805
|
-
------
|
|
2806
|
-
requests.HTTPError
|
|
2807
|
-
If the response status code is not 2xx.
|
|
2808
|
-
|
|
2809
|
-
Examples
|
|
2810
|
-
--------
|
|
2811
|
-
>>> result = app.run_result("run-123")
|
|
2812
|
-
>>> print(result.metadata.status_v2)
|
|
2813
|
-
'succeeded'
|
|
2814
|
-
"""
|
|
2815
|
-
|
|
2816
|
-
run_information = self.run_metadata(run_id=run_id)
|
|
2817
|
-
|
|
2818
|
-
return self.__run_result(
|
|
2819
|
-
run_id=run_id,
|
|
2820
|
-
run_information=run_information,
|
|
2821
|
-
output_dir_path=output_dir_path,
|
|
2822
|
-
)
|
|
2823
|
-
|
|
2824
|
-
def run_result_with_polling(
|
|
2825
|
-
self,
|
|
2826
|
-
run_id: str,
|
|
2827
|
-
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2828
|
-
output_dir_path: str | None = ".",
|
|
2829
|
-
) -> RunResult:
|
|
2830
|
-
"""
|
|
2831
|
-
Get the result of a run with polling.
|
|
2832
|
-
|
|
2833
|
-
Retrieves the result of a run including the run output. This method polls
|
|
2834
|
-
for the result until the run finishes executing or the polling strategy
|
|
2835
|
-
is exhausted.
|
|
2836
|
-
|
|
2837
|
-
Parameters
|
|
2838
|
-
----------
|
|
2839
|
-
run_id : str
|
|
2840
|
-
ID of the run to retrieve the result for.
|
|
2841
|
-
polling_options : PollingOptions, default=_DEFAULT_POLLING_OPTIONS
|
|
2842
|
-
Options to use when polling for the run result.
|
|
2843
|
-
output_dir_path : Optional[str], default="."
|
|
2844
|
-
Path to a directory where non-JSON output files will be saved. This is
|
|
2845
|
-
required if the output is non-JSON. If the directory does not exist, it
|
|
2846
|
-
will be created. Uses the current directory by default.
|
|
2847
|
-
|
|
2848
|
-
Returns
|
|
2849
|
-
-------
|
|
2850
|
-
RunResult
|
|
2851
|
-
Complete result of the run including output data.
|
|
2852
|
-
|
|
2853
|
-
Raises
|
|
2854
|
-
------
|
|
2855
|
-
requests.HTTPError
|
|
2856
|
-
If the response status code is not 2xx.
|
|
2857
|
-
TimeoutError
|
|
2858
|
-
If the run does not complete after the polling strategy is
|
|
2859
|
-
exhausted based on time duration.
|
|
2860
|
-
RuntimeError
|
|
2861
|
-
If the run does not complete after the polling strategy is
|
|
2862
|
-
exhausted based on number of tries.
|
|
2863
|
-
|
|
2864
|
-
Examples
|
|
2865
|
-
--------
|
|
2866
|
-
>>> from nextmv.cloud import PollingOptions
|
|
2867
|
-
>>> # Create custom polling options
|
|
2868
|
-
>>> polling_opts = PollingOptions(max_tries=50, max_duration=600)
|
|
2869
|
-
>>> # Get run result with polling
|
|
2870
|
-
>>> result = app.run_result_with_polling("run-123", polling_opts)
|
|
2871
|
-
>>> print(result.output)
|
|
2872
|
-
{'solution': {...}}
|
|
2873
|
-
"""
|
|
2874
|
-
|
|
2875
|
-
def polling_func() -> tuple[Any, bool]:
|
|
2876
|
-
run_information = self.run_metadata(run_id=run_id)
|
|
2877
|
-
if run_information.metadata.status_v2 in {
|
|
2878
|
-
StatusV2.succeeded,
|
|
2879
|
-
StatusV2.failed,
|
|
2880
|
-
StatusV2.canceled,
|
|
2881
|
-
}:
|
|
2882
|
-
return run_information, True
|
|
2883
|
-
|
|
2884
|
-
return None, False
|
|
2885
|
-
|
|
2886
|
-
run_information = poll(polling_options=polling_options, polling_func=polling_func)
|
|
2887
|
-
|
|
2888
|
-
return self.__run_result(
|
|
2889
|
-
run_id=run_id,
|
|
2890
|
-
run_information=run_information,
|
|
2891
|
-
output_dir_path=output_dir_path,
|
|
2892
|
-
)
|
|
2893
|
-
|
|
2894
|
-
def scenario_test(self, scenario_test_id: str) -> BatchExperiment:
|
|
2895
|
-
"""
|
|
2896
|
-
Get a scenario test.
|
|
2897
|
-
|
|
2898
|
-
Retrieves a scenario test by ID. Scenario tests are based on batch
|
|
2899
|
-
experiments, so this function returns the corresponding batch
|
|
2900
|
-
experiment associated with the scenario test.
|
|
2901
|
-
|
|
2902
|
-
Parameters
|
|
2903
|
-
----------
|
|
2904
|
-
scenario_test_id : str
|
|
2905
|
-
ID of the scenario test to retrieve.
|
|
2906
|
-
|
|
2907
|
-
Returns
|
|
2908
|
-
-------
|
|
2909
|
-
BatchExperiment
|
|
2910
|
-
The scenario test details as a batch experiment.
|
|
2911
|
-
|
|
2912
|
-
Raises
|
|
2913
|
-
------
|
|
2914
|
-
requests.HTTPError
|
|
2915
|
-
If the response status code is not 2xx.
|
|
2916
|
-
|
|
2917
|
-
Examples
|
|
2918
|
-
--------
|
|
2919
|
-
>>> test = app.scenario_test("scenario-123")
|
|
2920
|
-
>>> print(test.name)
|
|
2921
|
-
'My Scenario Test'
|
|
2922
|
-
>>> print(test.type)
|
|
2923
|
-
'scenario'
|
|
2924
|
-
"""
|
|
2925
|
-
|
|
2926
|
-
return self.batch_experiment(batch_id=scenario_test_id)
|
|
2927
|
-
|
|
2928
|
-
def scenario_test_metadata(self, scenario_test_id: str) -> BatchExperimentMetadata:
|
|
2929
|
-
"""
|
|
2930
|
-
Get the metadata for a scenario test, given its ID.
|
|
2931
|
-
|
|
2932
|
-
Scenario tests are based on batch experiments, so this function returns
|
|
2933
|
-
the corresponding batch experiment metadata associated with the
|
|
2934
|
-
scenario test.
|
|
2935
|
-
|
|
2936
|
-
Parameters
|
|
2937
|
-
----------
|
|
2938
|
-
scenario_test_id : str
|
|
2939
|
-
ID of the scenario test to retrieve.
|
|
2940
|
-
|
|
2941
|
-
Returns
|
|
2942
|
-
-------
|
|
2943
|
-
BatchExperimentMetadata
|
|
2944
|
-
The scenario test metadata as a batch experiment.
|
|
2945
|
-
|
|
2946
|
-
Raises
|
|
2947
|
-
------
|
|
2948
|
-
requests.HTTPError
|
|
2949
|
-
If the response status code is not 2xx.
|
|
2950
|
-
|
|
2951
|
-
Examples
|
|
2952
|
-
--------
|
|
2953
|
-
>>> metadata = app.scenario_test_metadata("scenario-123")
|
|
2954
|
-
>>> print(metadata.name)
|
|
2955
|
-
'My Scenario Test'
|
|
2956
|
-
>>> print(metadata.type)
|
|
2957
|
-
'scenario'
|
|
2958
|
-
"""
|
|
2959
|
-
|
|
2960
|
-
return self.batch_experiment_metadata(batch_id=scenario_test_id)
|
|
2961
|
-
|
|
2962
|
-
def scenario_test_with_polling(
|
|
2963
|
-
self,
|
|
2964
|
-
scenario_test_id: str,
|
|
2965
|
-
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
2966
|
-
) -> BatchExperiment:
|
|
2967
|
-
"""
|
|
2968
|
-
Get a scenario test with polling.
|
|
2969
|
-
|
|
2970
|
-
Retrieves the result of a scenario test. This method polls for the
|
|
2971
|
-
result until the test finishes executing or the polling strategy is
|
|
2972
|
-
exhausted.
|
|
2973
|
-
|
|
2974
|
-
The scenario tests uses the batch experiments API under the hood.
|
|
2975
|
-
|
|
2976
|
-
Parameters
|
|
2977
|
-
----------
|
|
2978
|
-
scenario_test_id : str
|
|
2979
|
-
ID of the scenario test to retrieve.
|
|
2980
|
-
polling_options : PollingOptions, default=_DEFAULT_POLLING_OPTIONS
|
|
2981
|
-
Options to use when polling for the scenario test result.
|
|
2982
|
-
|
|
2983
|
-
Returns
|
|
2984
|
-
-------
|
|
2985
|
-
BatchExperiment
|
|
2986
|
-
The scenario test details as a batch experiment.
|
|
2987
|
-
|
|
2988
|
-
Raises
|
|
2989
|
-
------
|
|
2990
|
-
requests.HTTPError
|
|
2991
|
-
If the response status code is not 2xx.
|
|
2992
|
-
|
|
2993
|
-
Examples
|
|
2994
|
-
--------
|
|
2995
|
-
>>> test = app.scenario_test_with_polling("scenario-123")
|
|
2996
|
-
>>> print(test.name)
|
|
2997
|
-
'My Scenario Test'
|
|
2998
|
-
>>> print(test.type)
|
|
2999
|
-
'scenario'
|
|
3000
|
-
"""
|
|
3001
|
-
|
|
3002
|
-
return self.batch_experiment_with_polling(batch_id=scenario_test_id, polling_options=polling_options)
|
|
3003
|
-
|
|
3004
|
-
def track_run( # noqa: C901
|
|
3005
|
-
self,
|
|
3006
|
-
tracked_run: TrackedRun,
|
|
3007
|
-
instance_id: str | None = None,
|
|
3008
|
-
configuration: RunConfiguration | dict[str, Any] | None = None,
|
|
3009
|
-
) -> str:
|
|
3010
|
-
"""
|
|
3011
|
-
Track an external run.
|
|
3012
|
-
|
|
3013
|
-
This method allows you to register in Nextmv a run that happened
|
|
3014
|
-
elsewhere, as though it were executed in the Nextmv platform. Having
|
|
3015
|
-
information about a run in Nextmv is useful for things like
|
|
3016
|
-
experimenting and testing.
|
|
3017
|
-
|
|
3018
|
-
Please read the documentation on the `TrackedRun` class carefully, as
|
|
3019
|
-
there are important considerations to take into account when using this
|
|
3020
|
-
method. For example, if you intend to upload JSON input/output, use the
|
|
3021
|
-
`input`/`output` attributes of the `TrackedRun` class. On the other
|
|
3022
|
-
hand, if you intend to track files-based input/output, use the
|
|
3023
|
-
`input_dir_path`/`output_dir_path` attributes of the `TrackedRun`
|
|
3024
|
-
class.
|
|
3025
|
-
|
|
3026
|
-
Parameters
|
|
3027
|
-
----------
|
|
3028
|
-
tracked_run : TrackedRun
|
|
3029
|
-
The run to track.
|
|
3030
|
-
instance_id : Optional[str], default=None
|
|
3031
|
-
Optional instance ID if you want to associate your tracked run with
|
|
3032
|
-
an instance.
|
|
3033
|
-
configuration: Optional[Union[RunConfiguration, dict[str, Any]]]
|
|
3034
|
-
Configuration to use for the run. This can be a
|
|
3035
|
-
`cloud.RunConfiguration` object or a dict. If the object is used,
|
|
3036
|
-
then the `.to_dict()` method is applied to extract the
|
|
3037
|
-
configuration.
|
|
3038
|
-
|
|
3039
|
-
Returns
|
|
3040
|
-
-------
|
|
3041
|
-
str
|
|
3042
|
-
The ID of the run that was tracked.
|
|
3043
|
-
|
|
3044
|
-
Raises
|
|
3045
|
-
------
|
|
3046
|
-
requests.HTTPError
|
|
3047
|
-
If the response status code is not 2xx.
|
|
3048
|
-
ValueError
|
|
3049
|
-
If the tracked run does not have an input or output.
|
|
3050
|
-
|
|
3051
|
-
Examples
|
|
3052
|
-
--------
|
|
3053
|
-
>>> from nextmv.cloud import Application
|
|
3054
|
-
>>> from nextmv import TrackedRun
|
|
3055
|
-
>>> app = Application(id="app_123")
|
|
3056
|
-
>>> tracked_run = TrackedRun(input={"data": [...]}, output={"solution": [...]})
|
|
3057
|
-
>>> run_id = app.track_run(tracked_run)
|
|
3058
|
-
"""
|
|
3059
|
-
|
|
3060
|
-
# Get the URL to upload the input to.
|
|
3061
|
-
url_input = self.upload_url()
|
|
3062
|
-
|
|
3063
|
-
# Handle the case where the input is being uploaded as files. We need
|
|
3064
|
-
# to tar them.
|
|
3065
|
-
input_tar_file = ""
|
|
3066
|
-
input_dir_path = tracked_run.input_dir_path
|
|
3067
|
-
if input_dir_path is not None and input_dir_path != "":
|
|
3068
|
-
if not os.path.exists(input_dir_path):
|
|
3069
|
-
raise ValueError(f"Directory {input_dir_path} does not exist.")
|
|
3070
|
-
|
|
3071
|
-
if not os.path.isdir(input_dir_path):
|
|
3072
|
-
raise ValueError(f"Path {input_dir_path} is not a directory.")
|
|
3073
|
-
|
|
3074
|
-
input_tar_file = self.__package_inputs(input_dir_path)
|
|
3075
|
-
|
|
3076
|
-
# Handle the case where the input is uploaded as Input or a dict.
|
|
3077
|
-
upload_input = tracked_run.input
|
|
3078
|
-
if upload_input is not None and isinstance(tracked_run.input, Input):
|
|
3079
|
-
upload_input = tracked_run.input.data
|
|
3080
|
-
|
|
3081
|
-
# Actually uploads de input.
|
|
3082
|
-
self.upload_large_input(input=upload_input, upload_url=url_input, tar_file=input_tar_file)
|
|
3083
|
-
|
|
3084
|
-
# Get the URL to upload the output to.
|
|
3085
|
-
url_output = self.upload_url()
|
|
3086
|
-
|
|
3087
|
-
# Handle the case where the output is being uploaded as files. We need
|
|
3088
|
-
# to tar them.
|
|
3089
|
-
output_tar_file = ""
|
|
3090
|
-
output_dir_path = tracked_run.output_dir_path
|
|
3091
|
-
if output_dir_path is not None and output_dir_path != "":
|
|
3092
|
-
if not os.path.exists(output_dir_path):
|
|
3093
|
-
raise ValueError(f"Directory {output_dir_path} does not exist.")
|
|
3094
|
-
|
|
3095
|
-
if not os.path.isdir(output_dir_path):
|
|
3096
|
-
raise ValueError(f"Path {output_dir_path} is not a directory.")
|
|
3097
|
-
|
|
3098
|
-
output_tar_file = self.__package_inputs(output_dir_path)
|
|
3099
|
-
|
|
3100
|
-
# Handle the case where the output is uploaded as Output or a dict.
|
|
3101
|
-
upload_output = tracked_run.output
|
|
3102
|
-
if upload_output is not None and isinstance(tracked_run.output, Output):
|
|
3103
|
-
upload_output = tracked_run.output.to_dict()
|
|
3104
|
-
|
|
3105
|
-
# Actually uploads the output.
|
|
3106
|
-
self.upload_large_input(input=upload_output, upload_url=url_output, tar_file=output_tar_file)
|
|
3107
|
-
|
|
3108
|
-
# Create the external run result and appends logs if required.
|
|
3109
|
-
external_result = ExternalRunResult(
|
|
3110
|
-
output_upload_id=url_output.upload_id,
|
|
3111
|
-
status=tracked_run.status.value,
|
|
3112
|
-
execution_duration=tracked_run.duration,
|
|
3113
|
-
)
|
|
3114
|
-
|
|
3115
|
-
# Handle the stderr logs if provided.
|
|
3116
|
-
if tracked_run.logs is not None:
|
|
3117
|
-
url_stderr = self.upload_url()
|
|
3118
|
-
self.upload_large_input(input=tracked_run.logs_text(), upload_url=url_stderr)
|
|
3119
|
-
external_result.error_upload_id = url_stderr.upload_id
|
|
3120
|
-
|
|
3121
|
-
if tracked_run.error is not None and tracked_run.error != "":
|
|
3122
|
-
external_result.error_message = tracked_run.error
|
|
3123
|
-
|
|
3124
|
-
# Handle the statistics upload if provided.
|
|
3125
|
-
stats = tracked_run.statistics
|
|
3126
|
-
if stats is not None:
|
|
3127
|
-
if isinstance(stats, Statistics):
|
|
3128
|
-
stats_dict = stats.to_dict()
|
|
3129
|
-
stats_dict = {STATISTICS_KEY: stats_dict}
|
|
3130
|
-
elif isinstance(stats, dict):
|
|
3131
|
-
stats_dict = stats
|
|
3132
|
-
if STATISTICS_KEY not in stats_dict:
|
|
3133
|
-
stats_dict = {STATISTICS_KEY: stats_dict}
|
|
3134
|
-
else:
|
|
3135
|
-
raise ValueError("tracked_run.statistics must be either a `Statistics` or `dict` object")
|
|
3136
|
-
|
|
3137
|
-
url_stats = self.upload_url()
|
|
3138
|
-
self.upload_large_input(input=stats_dict, upload_url=url_stats)
|
|
3139
|
-
external_result.statistics_upload_id = url_stats.upload_id
|
|
3140
|
-
|
|
3141
|
-
# Handle the assets upload if provided.
|
|
3142
|
-
assets = tracked_run.assets
|
|
3143
|
-
if assets is not None:
|
|
3144
|
-
if isinstance(assets, list):
|
|
3145
|
-
assets_list = []
|
|
3146
|
-
for ix, asset in enumerate(assets):
|
|
3147
|
-
if isinstance(asset, Asset):
|
|
3148
|
-
assets_list.append(asset.to_dict())
|
|
3149
|
-
elif isinstance(asset, dict):
|
|
3150
|
-
assets_list.append(asset)
|
|
3151
|
-
else:
|
|
3152
|
-
raise ValueError(f"tracked_run.assets, index {ix} must be an `Asset` or `dict` object")
|
|
3153
|
-
assets_dict = {ASSETS_KEY: assets_list}
|
|
3154
|
-
elif isinstance(assets, dict):
|
|
3155
|
-
assets_dict = assets
|
|
3156
|
-
if ASSETS_KEY not in assets_dict:
|
|
3157
|
-
assets_dict = {ASSETS_KEY: assets_dict}
|
|
3158
|
-
else:
|
|
3159
|
-
raise ValueError("tracked_run.assets must be either a `list[Asset]`, `list[dict]`, or `dict` object")
|
|
3160
|
-
|
|
3161
|
-
url_assets = self.upload_url()
|
|
3162
|
-
self.upload_large_input(input=assets_dict, upload_url=url_assets)
|
|
3163
|
-
external_result.assets_upload_id = url_assets.upload_id
|
|
3164
|
-
|
|
3165
|
-
return self.new_run(
|
|
3166
|
-
upload_id=url_input.upload_id,
|
|
3167
|
-
external_result=external_result,
|
|
3168
|
-
instance_id=instance_id,
|
|
3169
|
-
name=tracked_run.name,
|
|
3170
|
-
description=tracked_run.description,
|
|
3171
|
-
configuration=configuration,
|
|
3172
|
-
)
|
|
3173
|
-
|
|
3174
|
-
def track_run_with_result(
|
|
3175
|
-
self,
|
|
3176
|
-
tracked_run: TrackedRun,
|
|
3177
|
-
polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
|
|
3178
|
-
instance_id: str | None = None,
|
|
3179
|
-
output_dir_path: str | None = ".",
|
|
3180
|
-
configuration: RunConfiguration | dict[str, Any] | None = None,
|
|
3181
|
-
) -> RunResult:
|
|
3182
|
-
"""
|
|
3183
|
-
Track an external run and poll for the result. This is a convenience
|
|
3184
|
-
method that combines the `track_run` and `run_result_with_polling`
|
|
3185
|
-
methods. It applies polling logic to check when the run was
|
|
3186
|
-
successfully registered.
|
|
3187
|
-
|
|
3188
|
-
Parameters
|
|
3189
|
-
----------
|
|
3190
|
-
tracked_run : TrackedRun
|
|
3191
|
-
The run to track.
|
|
3192
|
-
polling_options : PollingOptions
|
|
3193
|
-
Options to use when polling for the run result.
|
|
3194
|
-
instance_id: Optional[str]
|
|
3195
|
-
Optional instance ID if you want to associate your tracked run with
|
|
3196
|
-
an instance.
|
|
3197
|
-
output_dir_path : Optional[str], default="."
|
|
3198
|
-
Path to a directory where non-JSON output files will be saved. This is
|
|
3199
|
-
required if the output is non-JSON. If the directory does not exist, it
|
|
3200
|
-
will be created. Uses the current directory by default.
|
|
3201
|
-
configuration: Optional[Union[RunConfiguration, dict[str, Any]]]
|
|
3202
|
-
Configuration to use for the run. This can be a
|
|
3203
|
-
`cloud.RunConfiguration` object or a dict. If the object is used,
|
|
3204
|
-
then the `.to_dict()` method is applied to extract the
|
|
3205
|
-
configuration.
|
|
3206
|
-
|
|
3207
|
-
Returns
|
|
3208
|
-
-------
|
|
3209
|
-
RunResult
|
|
3210
|
-
Result of the run.
|
|
3211
|
-
|
|
3212
|
-
Raises
|
|
3213
|
-
------
|
|
3214
|
-
requests.HTTPError
|
|
3215
|
-
If the response status code is not 2xx.
|
|
3216
|
-
ValueError
|
|
3217
|
-
If the tracked run does not have an input or output.
|
|
3218
|
-
TimeoutError
|
|
3219
|
-
If the run does not succeed after the polling strategy is
|
|
3220
|
-
exhausted based on time duration.
|
|
3221
|
-
RuntimeError
|
|
3222
|
-
If the run does not succeed after the polling strategy is
|
|
3223
|
-
exhausted based on number of tries.
|
|
3224
|
-
"""
|
|
3225
|
-
run_id = self.track_run(
|
|
3226
|
-
tracked_run=tracked_run,
|
|
3227
|
-
instance_id=instance_id,
|
|
3228
|
-
configuration=configuration,
|
|
3229
|
-
)
|
|
3230
|
-
|
|
3231
|
-
return self.run_result_with_polling(
|
|
3232
|
-
run_id=run_id,
|
|
3233
|
-
polling_options=polling_options,
|
|
3234
|
-
output_dir_path=output_dir_path,
|
|
3235
|
-
)
|
|
3236
|
-
|
|
3237
|
-
def update_batch_experiment(
|
|
3238
|
-
self,
|
|
3239
|
-
batch_experiment_id: str,
|
|
3240
|
-
name: str | None = None,
|
|
3241
|
-
description: str | None = None,
|
|
3242
|
-
) -> BatchExperimentInformation:
|
|
3243
|
-
"""
|
|
3244
|
-
Update a batch experiment.
|
|
3245
|
-
|
|
3246
|
-
Parameters
|
|
3247
|
-
----------
|
|
3248
|
-
batch_experiment_id : str
|
|
3249
|
-
ID of the batch experiment to update.
|
|
3250
|
-
name : Optional[str], default=None
|
|
3251
|
-
Optional name of the batch experiment.
|
|
3252
|
-
description : Optional[str], default=None
|
|
3253
|
-
Optional description of the batch experiment.
|
|
3254
|
-
|
|
3255
|
-
Returns
|
|
3256
|
-
-------
|
|
3257
|
-
BatchExperimentInformation
|
|
3258
|
-
The information with the updated batch experiment.
|
|
3259
|
-
|
|
3260
|
-
Raises
|
|
3261
|
-
------
|
|
3262
|
-
requests.HTTPError
|
|
3263
|
-
If the response status code is not 2xx.
|
|
3264
|
-
"""
|
|
3265
|
-
|
|
3266
|
-
payload = {}
|
|
3267
|
-
|
|
3268
|
-
if name is not None:
|
|
3269
|
-
payload["name"] = name
|
|
3270
|
-
if description is not None:
|
|
3271
|
-
payload["description"] = description
|
|
3272
|
-
|
|
3273
|
-
response = self.client.request(
|
|
3274
|
-
method="PATCH",
|
|
3275
|
-
endpoint=f"{self.experiments_endpoint}/batch/{batch_experiment_id}",
|
|
3276
|
-
payload=payload,
|
|
3277
|
-
)
|
|
3278
|
-
|
|
3279
|
-
return BatchExperimentInformation.from_dict(response.json())
|
|
3280
|
-
|
|
3281
|
-
def update_ensemble_definition(
|
|
3282
|
-
self,
|
|
3283
|
-
id: str,
|
|
3284
|
-
name: str | None = None,
|
|
3285
|
-
description: str | None = None,
|
|
3286
|
-
) -> EnsembleDefinition:
|
|
3287
|
-
"""
|
|
3288
|
-
Update an ensemble definition.
|
|
3289
|
-
|
|
3290
|
-
Parameters
|
|
3291
|
-
----------
|
|
3292
|
-
id : str
|
|
3293
|
-
ID of the ensemble definition to update.
|
|
3294
|
-
name : Optional[str], default=None
|
|
3295
|
-
Optional name of the ensemble definition.
|
|
3296
|
-
description : Optional[str], default=None
|
|
3297
|
-
Optional description of the ensemble definition.
|
|
3298
|
-
|
|
3299
|
-
Returns
|
|
3300
|
-
-------
|
|
3301
|
-
EnsembleDefinition
|
|
3302
|
-
The updated ensemble definition.
|
|
3303
|
-
|
|
3304
|
-
Raises
|
|
3305
|
-
------
|
|
3306
|
-
ValueError
|
|
3307
|
-
If neither name nor description is updated
|
|
3308
|
-
requests.HTTPError
|
|
3309
|
-
If the response status code is not 2xx.
|
|
3310
|
-
"""
|
|
3311
|
-
|
|
3312
|
-
payload = {}
|
|
3313
|
-
|
|
3314
|
-
if name is None and description is None:
|
|
3315
|
-
raise ValueError("Must define at least one value among name and description to modify")
|
|
3316
|
-
if name is not None:
|
|
3317
|
-
payload["name"] = name
|
|
3318
|
-
if description is not None:
|
|
3319
|
-
payload["description"] = description
|
|
3320
|
-
|
|
3321
|
-
response = self.client.request(
|
|
3322
|
-
method="PATCH",
|
|
3323
|
-
endpoint=f"{self.ensembles_endpoint}/{id}",
|
|
3324
|
-
payload=payload,
|
|
3325
|
-
)
|
|
3326
|
-
|
|
3327
|
-
return EnsembleDefinition.from_dict(response.json())
|
|
3328
|
-
|
|
3329
|
-
def update_instance(
|
|
3330
|
-
self,
|
|
3331
|
-
id: str,
|
|
3332
|
-
name: str | None = None,
|
|
3333
|
-
version_id: str | None = None,
|
|
3334
|
-
description: str | None = None,
|
|
3335
|
-
configuration: InstanceConfiguration | dict[str, Any] | None = None,
|
|
3336
|
-
) -> Instance:
|
|
3337
|
-
"""
|
|
3338
|
-
Update an instance.
|
|
3339
|
-
|
|
3340
|
-
Parameters
|
|
3341
|
-
----------
|
|
3342
|
-
id : str
|
|
3343
|
-
ID of the instance to update.
|
|
3344
|
-
name : Optional[str], default=None
|
|
3345
|
-
Optional name of the instance.
|
|
3346
|
-
version_id : Optional[str], default=None
|
|
3347
|
-
Optional ID of the version to associate the instance with.
|
|
3348
|
-
description : Optional[str], default=None
|
|
3349
|
-
Optional description of the instance.
|
|
3350
|
-
configuration : Optional[InstanceConfiguration | dict[str, Any]], default=None
|
|
3351
|
-
Optional configuration to use for the instance.
|
|
3352
|
-
|
|
3353
|
-
Returns
|
|
3354
|
-
-------
|
|
3355
|
-
Instance
|
|
3356
|
-
The updated instance.
|
|
3357
|
-
|
|
3358
|
-
Raises
|
|
3359
|
-
------
|
|
3360
|
-
requests.HTTPError
|
|
3361
|
-
If the response status code is not 2xx.
|
|
3362
|
-
"""
|
|
3363
|
-
|
|
3364
|
-
# Get the instance as it currently exsits.
|
|
3365
|
-
instance = self.instance(id)
|
|
3366
|
-
instance_dict = instance.to_dict()
|
|
3367
|
-
payload = instance_dict
|
|
3368
|
-
|
|
3369
|
-
if name is not None:
|
|
3370
|
-
payload["name"] = name
|
|
3371
|
-
if version_id is not None:
|
|
3372
|
-
payload["version_id"] = version_id
|
|
3373
|
-
if description is not None:
|
|
3374
|
-
payload["description"] = description
|
|
3375
|
-
if configuration is not None:
|
|
3376
|
-
if isinstance(configuration, dict):
|
|
3377
|
-
config_dict = configuration
|
|
3378
|
-
elif isinstance(configuration, InstanceConfiguration):
|
|
3379
|
-
config_dict = configuration.to_dict()
|
|
3380
|
-
else:
|
|
3381
|
-
raise TypeError("configuration must be either a dict or InstanceConfiguration object")
|
|
3382
|
-
|
|
3383
|
-
payload["configuration"] = config_dict
|
|
3384
|
-
|
|
3385
|
-
response = self.client.request(
|
|
3386
|
-
method="PUT",
|
|
3387
|
-
endpoint=f"{self.endpoint}/instances/{id}",
|
|
3388
|
-
payload=payload,
|
|
3389
|
-
)
|
|
3390
|
-
|
|
3391
|
-
return Instance.from_dict(response.json())
|
|
3392
|
-
|
|
3393
|
-
def update_managed_input(
|
|
3394
|
-
self,
|
|
3395
|
-
managed_input_id: str,
|
|
3396
|
-
name: str | None = None,
|
|
3397
|
-
description: str | None = None,
|
|
3398
|
-
) -> ManagedInput:
|
|
3399
|
-
"""
|
|
3400
|
-
Update a managed input.
|
|
3401
|
-
|
|
3402
|
-
Parameters
|
|
3403
|
-
----------
|
|
3404
|
-
managed_input_id : str
|
|
3405
|
-
ID of the managed input to update.
|
|
3406
|
-
name : Optional[str], default=None
|
|
3407
|
-
Optional new name for the managed input.
|
|
3408
|
-
description : Optional[str], default=None
|
|
3409
|
-
Optional new description for the managed input.
|
|
3410
|
-
|
|
3411
|
-
Returns
|
|
3412
|
-
-------
|
|
3413
|
-
ManagedInput
|
|
3414
|
-
The updated managed input.
|
|
3415
|
-
|
|
3416
|
-
Raises
|
|
3417
|
-
------
|
|
3418
|
-
requests.HTTPError
|
|
3419
|
-
If the response status code is not 2xx.
|
|
3420
|
-
"""
|
|
3421
|
-
|
|
3422
|
-
managed_input = self.managed_input(managed_input_id)
|
|
3423
|
-
managed_input_dict = managed_input.to_dict()
|
|
3424
|
-
payload = managed_input_dict
|
|
3425
|
-
|
|
3426
|
-
if name is not None:
|
|
3427
|
-
payload["name"] = name
|
|
3428
|
-
if description is not None:
|
|
3429
|
-
payload["description"] = description
|
|
3430
|
-
|
|
3431
|
-
response = self.client.request(
|
|
3432
|
-
method="PUT",
|
|
3433
|
-
endpoint=f"{self.endpoint}/inputs/{managed_input_id}",
|
|
3434
|
-
payload=payload,
|
|
3435
|
-
)
|
|
3436
|
-
|
|
3437
|
-
return ManagedInput.from_dict(response.json())
|
|
3438
|
-
|
|
3439
|
-
def update_scenario_test(
|
|
3440
|
-
self,
|
|
3441
|
-
scenario_test_id: str,
|
|
3442
|
-
name: str | None = None,
|
|
3443
|
-
description: str | None = None,
|
|
3444
|
-
) -> BatchExperimentInformation:
|
|
3445
|
-
"""
|
|
3446
|
-
Update a scenario test.
|
|
3447
|
-
|
|
3448
|
-
Updates a scenario test with new name and description. Scenario tests
|
|
3449
|
-
use the batch experiments API, so this method calls the
|
|
3450
|
-
`update_batch_experiment` method, and thus the return type is the same.
|
|
3451
|
-
|
|
3452
|
-
Parameters
|
|
3453
|
-
----------
|
|
3454
|
-
scenario_test_id : str
|
|
3455
|
-
ID of the scenario test to update.
|
|
3456
|
-
name : Optional[str], default=None
|
|
3457
|
-
Optional new name for the scenario test.
|
|
3458
|
-
description : Optional[str], default=None
|
|
3459
|
-
Optional new description for the scenario test.
|
|
3460
|
-
|
|
3461
|
-
Returns
|
|
3462
|
-
-------
|
|
3463
|
-
BatchExperimentInformation
|
|
3464
|
-
The information about the updated scenario test.
|
|
3465
|
-
|
|
3466
|
-
Raises
|
|
3467
|
-
------
|
|
3468
|
-
requests.HTTPError
|
|
3469
|
-
If the response status code is not 2xx.
|
|
3470
|
-
|
|
3471
|
-
Examples
|
|
3472
|
-
--------
|
|
3473
|
-
>>> info = app.update_scenario_test(
|
|
3474
|
-
... scenario_test_id="scenario-123",
|
|
3475
|
-
... name="Updated Test Name",
|
|
3476
|
-
... description="Updated description for this test"
|
|
3477
|
-
... )
|
|
3478
|
-
>>> print(info.name)
|
|
3479
|
-
'Updated Test Name'
|
|
3480
|
-
"""
|
|
3481
|
-
|
|
3482
|
-
return self.update_batch_experiment(
|
|
3483
|
-
batch_experiment_id=scenario_test_id,
|
|
3484
|
-
name=name,
|
|
3485
|
-
description=description,
|
|
3486
|
-
)
|
|
3487
|
-
|
|
3488
|
-
def update_secrets_collection(
|
|
3489
|
-
self,
|
|
3490
|
-
secrets_collection_id: str,
|
|
3491
|
-
name: str | None = None,
|
|
3492
|
-
description: str | None = None,
|
|
3493
|
-
secrets: list[Secret | dict[str, Any]] | None = None,
|
|
3494
|
-
) -> SecretsCollectionSummary:
|
|
3495
|
-
"""
|
|
3496
|
-
Update a secrets collection.
|
|
3497
|
-
|
|
3498
|
-
This method updates an existing secrets collection with new values for name,
|
|
3499
|
-
description, and secrets. A secrets collection is a group of key-value pairs
|
|
3500
|
-
that can be used by your application instances during execution.
|
|
3501
|
-
|
|
3502
|
-
Parameters
|
|
3503
|
-
----------
|
|
3504
|
-
secrets_collection_id : str
|
|
3505
|
-
ID of the secrets collection to update.
|
|
3506
|
-
name : Optional[str], default=None
|
|
3507
|
-
Optional new name for the secrets collection.
|
|
3508
|
-
description : Optional[str], default=None
|
|
3509
|
-
Optional new description for the secrets collection.
|
|
3510
|
-
secrets : Optional[list[Secret | dict[str, Any]]], default=None
|
|
3511
|
-
Optional list of secrets to update. Each secret should be an
|
|
3512
|
-
instance of the Secret class containing a key and value.
|
|
3513
|
-
|
|
3514
|
-
Returns
|
|
3515
|
-
-------
|
|
3516
|
-
SecretsCollectionSummary
|
|
3517
|
-
Summary of the updated secrets collection including its metadata.
|
|
3518
|
-
|
|
3519
|
-
Raises
|
|
3520
|
-
------
|
|
3521
|
-
ValueError
|
|
3522
|
-
If no secrets are provided.
|
|
3523
|
-
requests.HTTPError
|
|
3524
|
-
If the response status code is not 2xx.
|
|
3525
|
-
|
|
3526
|
-
Examples
|
|
3527
|
-
--------
|
|
3528
|
-
>>> # Update an existing secrets collection
|
|
3529
|
-
>>> from nextmv.cloud import Secret
|
|
3530
|
-
>>> updated_secrets = [
|
|
3531
|
-
... Secret(key="API_KEY", value="new-api-key"),
|
|
3532
|
-
... Secret(key="DATABASE_URL", value="new-database-url")
|
|
3533
|
-
... ]
|
|
3534
|
-
>>> updated_collection = app.update_secrets_collection(
|
|
3535
|
-
... secrets_collection_id="api-secrets",
|
|
3536
|
-
... name="Updated API Secrets",
|
|
3537
|
-
... description="Updated collection of API secrets",
|
|
3538
|
-
... secrets=updated_secrets
|
|
3539
|
-
... )
|
|
3540
|
-
>>> print(updated_collection.id)
|
|
3541
|
-
'api-secrets'
|
|
3542
|
-
"""
|
|
3543
|
-
|
|
3544
|
-
collection = self.secrets_collection(secrets_collection_id)
|
|
3545
|
-
collection_dict = collection.to_dict()
|
|
3546
|
-
payload = collection_dict
|
|
3547
|
-
|
|
3548
|
-
if name is not None:
|
|
3549
|
-
payload["name"] = name
|
|
3550
|
-
if description is not None:
|
|
3551
|
-
payload["description"] = description
|
|
3552
|
-
if secrets is not None and len(secrets) > 0:
|
|
3553
|
-
secrets_dicts = []
|
|
3554
|
-
for ix, secret in enumerate(secrets):
|
|
3555
|
-
if isinstance(secret, dict):
|
|
3556
|
-
secrets_dicts.append(secret)
|
|
3557
|
-
elif isinstance(secret, Secret):
|
|
3558
|
-
secrets_dicts.append(secret.to_dict())
|
|
3559
|
-
else:
|
|
3560
|
-
raise ValueError(f"secret at index {ix} must be either a Secret or dict object")
|
|
3561
|
-
|
|
3562
|
-
payload["secrets"] = secrets_dicts
|
|
3563
|
-
|
|
3564
|
-
response = self.client.request(
|
|
3565
|
-
method="PUT",
|
|
3566
|
-
endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
|
|
3567
|
-
payload=payload,
|
|
3568
|
-
)
|
|
3569
|
-
|
|
3570
|
-
return SecretsCollectionSummary.from_dict(response.json())
|
|
3571
|
-
|
|
3572
|
-
def upload_large_input(
|
|
3573
|
-
self,
|
|
3574
|
-
input: dict[str, Any] | str | None,
|
|
3575
|
-
upload_url: UploadURL,
|
|
3576
|
-
json_configurations: dict[str, Any] | None = None,
|
|
3577
|
-
tar_file: str | None = None,
|
|
3578
|
-
) -> None:
|
|
3579
|
-
"""
|
|
3580
|
-
Upload large input data to the provided upload URL.
|
|
3581
|
-
|
|
3582
|
-
This method allows uploading large input data (either a dictionary or string)
|
|
3583
|
-
to a pre-signed URL. If the input is a dictionary, it will be converted to
|
|
3584
|
-
a JSON string before upload.
|
|
3585
|
-
|
|
3586
|
-
Parameters
|
|
3587
|
-
----------
|
|
3588
|
-
input : Optional[Union[dict[str, Any], str]]
|
|
3589
|
-
Input data to upload. Can be either a dictionary that will be
|
|
3590
|
-
converted to JSON, or a pre-formatted JSON string.
|
|
3591
|
-
upload_url : UploadURL
|
|
3592
|
-
Upload URL object containing the pre-signed URL to use for uploading.
|
|
3593
|
-
json_configurations : Optional[dict[str, Any]], default=None
|
|
3594
|
-
Optional configurations for JSON serialization. If provided, these
|
|
3595
|
-
configurations will be used when serializing the data via
|
|
3596
|
-
`json.dumps`.
|
|
3597
|
-
tar_file : Optional[str], default=None
|
|
3598
|
-
If provided, this will be used to upload a tar file instead of
|
|
3599
|
-
a JSON string or dictionary. This is useful for uploading large
|
|
3600
|
-
files that are already packaged as a tarball.
|
|
3601
|
-
|
|
3602
|
-
Returns
|
|
3603
|
-
-------
|
|
3604
|
-
None
|
|
3605
|
-
This method doesn't return anything.
|
|
3606
|
-
|
|
3607
|
-
Raises
|
|
3608
|
-
------
|
|
3609
|
-
requests.HTTPError
|
|
3610
|
-
If the response status code is not 2xx.
|
|
3611
|
-
|
|
3612
|
-
Examples
|
|
3613
|
-
--------
|
|
3614
|
-
>>> # Upload a dictionary as JSON
|
|
3615
|
-
>>> data = {"locations": [...], "vehicles": [...]}
|
|
3616
|
-
>>> url = app.upload_url()
|
|
3617
|
-
>>> app.upload_large_input(input=data, upload_url=url)
|
|
3618
|
-
>>>
|
|
3619
|
-
>>> # Upload a pre-formatted JSON string
|
|
3620
|
-
>>> json_str = '{"locations": [...], "vehicles": [...]}'
|
|
3621
|
-
>>> app.upload_large_input(input=json_str, upload_url=url)
|
|
3622
|
-
"""
|
|
3623
|
-
|
|
3624
|
-
if input is not None and isinstance(input, dict):
|
|
3625
|
-
input = deflated_serialize_json(input, json_configurations=json_configurations)
|
|
3626
|
-
|
|
3627
|
-
self.client.upload_to_presigned_url(
|
|
3628
|
-
url=upload_url.upload_url,
|
|
3629
|
-
data=input,
|
|
3630
|
-
tar_file=tar_file,
|
|
3631
|
-
)
|
|
3632
|
-
|
|
3633
|
-
def upload_url(self) -> UploadURL:
|
|
3634
|
-
"""
|
|
3635
|
-
Get an upload URL to use for uploading a file.
|
|
3636
|
-
|
|
3637
|
-
This method generates a pre-signed URL that can be used to upload large files
|
|
3638
|
-
to Nextmv Cloud. It's primarily used for uploading large input data, output
|
|
3639
|
-
results, or log files that exceed the size limits for direct API calls.
|
|
3640
|
-
|
|
3641
|
-
Returns
|
|
3642
|
-
-------
|
|
3643
|
-
UploadURL
|
|
3644
|
-
An object containing both the upload URL and an upload ID for reference.
|
|
3645
|
-
The upload URL is a pre-signed URL that allows temporary write access.
|
|
3646
|
-
|
|
3647
|
-
Raises
|
|
3648
|
-
------
|
|
3649
|
-
requests.HTTPError
|
|
3650
|
-
If the response status code is not 2xx.
|
|
3651
|
-
|
|
3652
|
-
Examples
|
|
3653
|
-
--------
|
|
3654
|
-
>>> # Get an upload URL and upload large input data
|
|
3655
|
-
>>> upload_url = app.upload_url()
|
|
3656
|
-
>>> large_input = {"locations": [...], "vehicles": [...]}
|
|
3657
|
-
>>> app.upload_large_input(input=large_input, upload_url=upload_url)
|
|
3658
|
-
"""
|
|
3659
|
-
|
|
3660
|
-
response = self.client.request(
|
|
3661
|
-
method="POST",
|
|
3662
|
-
endpoint=f"{self.endpoint}/runs/uploadurl",
|
|
3663
|
-
)
|
|
3664
|
-
|
|
3665
|
-
return UploadURL.from_dict(response.json())
|
|
3666
|
-
|
|
3667
|
-
def secrets_collection(self, secrets_collection_id: str) -> SecretsCollection:
|
|
3668
|
-
"""
|
|
3669
|
-
Get a secrets collection.
|
|
3670
|
-
|
|
3671
|
-
This method retrieves a secrets collection by its ID. A secrets collection
|
|
3672
|
-
is a group of key-value pairs that can be used by your application
|
|
3673
|
-
instances during execution.
|
|
3674
|
-
|
|
3675
|
-
Parameters
|
|
3676
|
-
----------
|
|
3677
|
-
secrets_collection_id : str
|
|
3678
|
-
ID of the secrets collection to retrieve.
|
|
3679
|
-
|
|
3680
|
-
Returns
|
|
3681
|
-
-------
|
|
3682
|
-
SecretsCollection
|
|
3683
|
-
The requested secrets collection, including all secret values
|
|
3684
|
-
and metadata.
|
|
3685
|
-
|
|
3686
|
-
Raises
|
|
3687
|
-
------
|
|
3688
|
-
requests.HTTPError
|
|
3689
|
-
If the response status code is not 2xx.
|
|
3690
|
-
|
|
3691
|
-
Examples
|
|
3692
|
-
--------
|
|
3693
|
-
>>> # Retrieve a secrets collection
|
|
3694
|
-
>>> collection = app.secrets_collection("api-secrets")
|
|
3695
|
-
>>> print(collection.name)
|
|
3696
|
-
'API Secrets'
|
|
3697
|
-
>>> print(len(collection.secrets))
|
|
3698
|
-
2
|
|
3699
|
-
>>> for secret in collection.secrets:
|
|
3700
|
-
... print(secret.location)
|
|
3701
|
-
'API_KEY'
|
|
3702
|
-
'DATABASE_URL'
|
|
3703
|
-
"""
|
|
3704
|
-
|
|
3705
|
-
response = self.client.request(
|
|
3706
|
-
method="GET",
|
|
3707
|
-
endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
|
|
3708
|
-
)
|
|
3709
|
-
|
|
3710
|
-
return SecretsCollection.from_dict(response.json())
|
|
3711
|
-
|
|
3712
|
-
def version(self, version_id: str) -> Version:
|
|
3713
|
-
"""
|
|
3714
|
-
Get a version.
|
|
3715
|
-
|
|
3716
|
-
Retrieves a specific version of the application by its ID. Application versions
|
|
3717
|
-
represent different iterations of your application's code and configuration.
|
|
3718
|
-
|
|
3719
|
-
Parameters
|
|
3720
|
-
----------
|
|
3721
|
-
version_id : str
|
|
3722
|
-
ID of the version to retrieve.
|
|
3723
|
-
|
|
3724
|
-
Returns
|
|
3725
|
-
-------
|
|
3726
|
-
Version
|
|
3727
|
-
The version object containing details about the requested application version.
|
|
3728
|
-
|
|
3729
|
-
Raises
|
|
3730
|
-
------
|
|
3731
|
-
requests.HTTPError
|
|
3732
|
-
If the response status code is not 2xx.
|
|
3733
|
-
|
|
3734
|
-
Examples
|
|
3735
|
-
--------
|
|
3736
|
-
>>> # Retrieve a specific version
|
|
3737
|
-
>>> version = app.version("v1.0.0")
|
|
3738
|
-
>>> print(version.id)
|
|
3739
|
-
'v1.0.0'
|
|
3740
|
-
>>> print(version.name)
|
|
3741
|
-
'Initial Release'
|
|
3742
|
-
"""
|
|
3743
|
-
|
|
3744
|
-
response = self.client.request(
|
|
3745
|
-
method="GET",
|
|
3746
|
-
endpoint=f"{self.endpoint}/versions/{version_id}",
|
|
3747
|
-
)
|
|
3748
|
-
|
|
3749
|
-
return Version.from_dict(response.json())
|
|
3750
|
-
|
|
3751
|
-
def version_exists(self, version_id: str) -> bool:
|
|
3752
|
-
"""
|
|
3753
|
-
Check if a version exists.
|
|
3754
|
-
|
|
3755
|
-
This method checks if a specific version of the application exists by
|
|
3756
|
-
attempting to retrieve it. It handles HTTP errors for non-existent versions
|
|
3757
|
-
and returns a boolean indicating existence.
|
|
3758
|
-
|
|
3759
|
-
Parameters
|
|
3760
|
-
----------
|
|
3761
|
-
version_id : str
|
|
3762
|
-
ID of the version to check for existence.
|
|
3763
|
-
|
|
3764
|
-
Returns
|
|
3765
|
-
-------
|
|
3766
|
-
bool
|
|
3767
|
-
True if the version exists, False otherwise.
|
|
3768
|
-
|
|
3769
|
-
Raises
|
|
3770
|
-
------
|
|
3771
|
-
requests.HTTPError
|
|
3772
|
-
If an HTTP error occurs that is not related to the non-existence
|
|
3773
|
-
of the version.
|
|
3774
|
-
|
|
3775
|
-
Examples
|
|
3776
|
-
--------
|
|
3777
|
-
>>> # Check if a version exists
|
|
3778
|
-
>>> exists = app.version_exists("v1.0.0")
|
|
3779
|
-
>>> if exists:
|
|
3780
|
-
... print("Version exists!")
|
|
3781
|
-
... else:
|
|
3782
|
-
... print("Version does not exist.")
|
|
3783
|
-
"""
|
|
3784
|
-
|
|
3785
|
-
try:
|
|
3786
|
-
self.version(version_id=version_id)
|
|
3787
|
-
return True
|
|
3788
|
-
except requests.HTTPError as e:
|
|
3789
|
-
if _is_not_exist_error(e):
|
|
3790
|
-
return False
|
|
3791
|
-
raise e
|
|
3792
|
-
|
|
3793
|
-
def __run_result(
|
|
3794
|
-
self,
|
|
3795
|
-
run_id: str,
|
|
3796
|
-
run_information: RunInformation,
|
|
3797
|
-
output_dir_path: str | None = ".",
|
|
3798
|
-
) -> RunResult:
|
|
3799
|
-
"""
|
|
3800
|
-
Get the result of a run.
|
|
3801
|
-
|
|
3802
|
-
This is a private method that retrieves the complete result of a run,
|
|
3803
|
-
including the output data. It handles both small and large outputs,
|
|
3804
|
-
automatically using the appropriate API endpoints based on the output
|
|
3805
|
-
size. This method serves as the base implementation for retrieving
|
|
3806
|
-
run results, regardless of polling strategy.
|
|
3807
|
-
|
|
3808
|
-
Parameters
|
|
3809
|
-
----------
|
|
3810
|
-
run_id : str
|
|
3811
|
-
ID of the run to retrieve the result for.
|
|
3812
|
-
run_information : RunInformation
|
|
3813
|
-
Information about the run, including metadata such as output size.
|
|
3814
|
-
output_dir_path : Optional[str], default="."
|
|
3815
|
-
Path to a directory where non-JSON output files will be saved. This is
|
|
3816
|
-
required if the output is non-JSON. If the directory does not exist, it
|
|
3817
|
-
will be created. Uses the current directory by default.
|
|
3818
|
-
|
|
3819
|
-
Returns
|
|
3820
|
-
-------
|
|
3821
|
-
RunResult
|
|
3822
|
-
Result of the run, including all metadata and output data.
|
|
3823
|
-
For large outputs, the method will fetch the output from
|
|
3824
|
-
a download URL.
|
|
3825
|
-
|
|
3826
|
-
Raises
|
|
3827
|
-
------
|
|
3828
|
-
requests.HTTPError
|
|
3829
|
-
If the response status code is not 2xx.
|
|
3830
|
-
|
|
3831
|
-
Notes
|
|
3832
|
-
-----
|
|
3833
|
-
This method automatically handles large outputs by checking if the
|
|
3834
|
-
output size exceeds _MAX_RUN_SIZE. If it does, the method will request
|
|
3835
|
-
a download URL and fetch the output data separately.
|
|
3836
|
-
"""
|
|
3837
|
-
query_params = None
|
|
3838
|
-
use_presigned_url = False
|
|
3839
|
-
if (
|
|
3840
|
-
run_information.metadata.format.format_output.output_type != OutputFormat.JSON
|
|
3841
|
-
or run_information.metadata.output_size > _MAX_RUN_SIZE
|
|
3842
|
-
):
|
|
3843
|
-
query_params = {"format": "url"}
|
|
3844
|
-
use_presigned_url = True
|
|
3845
|
-
|
|
3846
|
-
response = self.client.request(
|
|
3847
|
-
method="GET",
|
|
3848
|
-
endpoint=f"{self.endpoint}/runs/{run_id}",
|
|
3849
|
-
query_params=query_params,
|
|
3850
|
-
)
|
|
3851
|
-
result = RunResult.from_dict(response.json())
|
|
3852
|
-
result.console_url = self.__console_url(result.id)
|
|
3853
|
-
|
|
3854
|
-
if not use_presigned_url or result.metadata.status_v2 != StatusV2.succeeded:
|
|
3855
|
-
return result
|
|
3856
|
-
|
|
3857
|
-
download_url = DownloadURL.from_dict(response.json()["output"])
|
|
3858
|
-
download_response = self.client.request(
|
|
3859
|
-
method="GET",
|
|
3860
|
-
endpoint=download_url.url,
|
|
3861
|
-
headers={"Content-Type": "application/json"},
|
|
3862
|
-
)
|
|
3863
|
-
|
|
3864
|
-
# See whether we can attach the output directly or need to save to the given
|
|
3865
|
-
# directory
|
|
3866
|
-
if run_information.metadata.format.format_output.output_type != OutputFormat.JSON:
|
|
3867
|
-
if not output_dir_path or output_dir_path == "":
|
|
3868
|
-
raise ValueError(
|
|
3869
|
-
"If the output format is not JSON, an output_dir_path must be provided.",
|
|
3870
|
-
)
|
|
3871
|
-
if not os.path.exists(output_dir_path):
|
|
3872
|
-
os.makedirs(output_dir_path, exist_ok=True)
|
|
3873
|
-
# Save .tar.gz file to a temp directory and extract contents to output_dir_path
|
|
3874
|
-
with tempfile.TemporaryDirectory() as tmpdirname:
|
|
3875
|
-
temp_tar_path = os.path.join(tmpdirname, f"{run_id}.tar.gz")
|
|
3876
|
-
with open(temp_tar_path, "wb") as f:
|
|
3877
|
-
f.write(download_response.content)
|
|
3878
|
-
shutil.unpack_archive(temp_tar_path, output_dir_path)
|
|
3879
|
-
else:
|
|
3880
|
-
result.output = download_response.json()
|
|
3881
|
-
|
|
3882
|
-
return result
|
|
3883
|
-
|
|
3884
|
-
@staticmethod
|
|
3885
|
-
def __convert_manifest_to_payload(manifest: Manifest) -> dict[str, Any]: # noqa: C901
|
|
3886
|
-
"""Converts a manifest to a payload dictionary for the API."""
|
|
3887
|
-
|
|
3888
|
-
activation_request = {
|
|
3889
|
-
"requirements": {
|
|
3890
|
-
"executable_type": manifest.type,
|
|
3891
|
-
"runtime": manifest.runtime,
|
|
3892
|
-
},
|
|
3893
|
-
}
|
|
3894
|
-
|
|
3895
|
-
if manifest.configuration is not None and manifest.configuration.content is not None:
|
|
3896
|
-
content = manifest.configuration.content
|
|
3897
|
-
io_config = {
|
|
3898
|
-
"format": content.format,
|
|
3899
|
-
}
|
|
3900
|
-
if content.multi_file is not None:
|
|
3901
|
-
multi_config = io_config["multi_file"] = {}
|
|
3902
|
-
if content.multi_file.input is not None:
|
|
3903
|
-
multi_config["input_path"] = content.multi_file.input.path
|
|
3904
|
-
if content.multi_file.output is not None:
|
|
3905
|
-
output_config = multi_config["output_configuration"] = {}
|
|
3906
|
-
if content.multi_file.output.statistics:
|
|
3907
|
-
output_config["statistics_path"] = content.multi_file.output.statistics
|
|
3908
|
-
if content.multi_file.output.assets:
|
|
3909
|
-
output_config["assets_path"] = content.multi_file.output.assets
|
|
3910
|
-
if content.multi_file.output.solutions:
|
|
3911
|
-
output_config["solutions_path"] = content.multi_file.output.solutions
|
|
3912
|
-
activation_request["requirements"]["io_configuration"] = io_config
|
|
3913
|
-
|
|
3914
|
-
if manifest.configuration is not None and manifest.configuration.options is not None:
|
|
3915
|
-
options = manifest.configuration.options.to_dict()
|
|
3916
|
-
if "format" in options and isinstance(options["format"], list):
|
|
3917
|
-
# the endpoint expects a dictionary with a template key having a list of strings
|
|
3918
|
-
# the app.yaml however defines format as a list of strings, so we need to convert it here
|
|
3919
|
-
options["format"] = {
|
|
3920
|
-
"template": options["format"],
|
|
3921
|
-
}
|
|
3922
|
-
activation_request["requirements"]["options"] = options
|
|
3923
|
-
|
|
3924
|
-
if manifest.execution is not None:
|
|
3925
|
-
if manifest.execution.entrypoint:
|
|
3926
|
-
activation_request["requirements"]["entrypoint"] = manifest.execution.entrypoint
|
|
3927
|
-
if manifest.execution.cwd:
|
|
3928
|
-
activation_request["requirements"]["working_directory"] = manifest.execution.cwd
|
|
3929
|
-
|
|
3930
|
-
return activation_request
|
|
3931
|
-
|
|
3932
|
-
def __update_app_binary(
|
|
3933
|
-
self,
|
|
3934
|
-
tar_file: str,
|
|
3935
|
-
manifest: Manifest,
|
|
3936
|
-
verbose: bool = False,
|
|
3937
|
-
) -> None:
|
|
3938
|
-
"""Updates the application binary in Cloud."""
|
|
3939
|
-
|
|
3940
|
-
if verbose:
|
|
3941
|
-
log(f'🌟 Pushing to application: "{self.id}".')
|
|
3942
|
-
|
|
3943
|
-
endpoint = f"{self.endpoint}/binary"
|
|
3944
|
-
response = self.client.request(
|
|
3945
|
-
method="GET",
|
|
3946
|
-
endpoint=endpoint,
|
|
3947
|
-
)
|
|
3948
|
-
upload_url = response.json()["upload_url"]
|
|
3949
|
-
|
|
3950
|
-
with open(tar_file, "rb") as f:
|
|
3951
|
-
response = self.client.request(
|
|
3952
|
-
method="PUT",
|
|
3953
|
-
endpoint=upload_url,
|
|
3954
|
-
data=f,
|
|
3955
|
-
headers={"Content-Type": "application/octet-stream"},
|
|
3956
|
-
)
|
|
3957
|
-
|
|
3958
|
-
response = self.client.request(
|
|
3959
|
-
method="PUT",
|
|
3960
|
-
endpoint=endpoint,
|
|
3961
|
-
payload=Application.__convert_manifest_to_payload(manifest=manifest),
|
|
3962
|
-
)
|
|
3963
|
-
|
|
3964
|
-
if verbose:
|
|
3965
|
-
log(f'💥️ Successfully pushed to application: "{self.id}".')
|
|
3966
|
-
log(
|
|
3967
|
-
json.dumps(
|
|
3968
|
-
{
|
|
3969
|
-
"app_id": self.id,
|
|
3970
|
-
"endpoint": self.client.url,
|
|
3971
|
-
"instance_url": f"{self.endpoint}/runs?instance_id=latest",
|
|
3972
|
-
},
|
|
3973
|
-
indent=2,
|
|
3974
|
-
)
|
|
3975
|
-
)
|
|
3976
|
-
|
|
3977
|
-
def __console_url(self, run_id: str) -> str:
|
|
3978
|
-
"""Auxiliary method to get the console URL for a run."""
|
|
3979
|
-
|
|
3980
|
-
return f"{self.client.console_url}/app/{self.id}/run/{run_id}?view=details"
|
|
3981
|
-
|
|
3982
|
-
def __input_set_for_scenario(self, scenario: Scenario, scenario_id: str) -> InputSet:
|
|
3983
|
-
# If working with an input set, there is no need to create one.
|
|
3984
|
-
if scenario.scenario_input.scenario_input_type == ScenarioInputType.INPUT_SET:
|
|
3985
|
-
input_set = self.input_set(input_set_id=scenario.scenario_input.scenario_input_data)
|
|
3986
|
-
return input_set
|
|
3987
|
-
|
|
3988
|
-
# If working with a list of managed inputs, we need to create an
|
|
3989
|
-
# input set.
|
|
3990
|
-
if scenario.scenario_input.scenario_input_type == ScenarioInputType.INPUT:
|
|
3991
|
-
name, id = safe_name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
3992
|
-
input_set = self.new_input_set(
|
|
3993
|
-
id=id,
|
|
3994
|
-
name=name,
|
|
3995
|
-
description=f"Automatically created from scenario test: {id}",
|
|
3996
|
-
maximum_runs=20,
|
|
3997
|
-
inputs=[
|
|
3998
|
-
ManagedInput.from_dict(data={"id": input_id})
|
|
3999
|
-
for input_id in scenario.scenario_input.scenario_input_data
|
|
4000
|
-
],
|
|
4001
|
-
)
|
|
4002
|
-
return input_set
|
|
4003
|
-
|
|
4004
|
-
# If working with new data, we need to create managed inputs, and then,
|
|
4005
|
-
# an input set.
|
|
4006
|
-
if scenario.scenario_input.scenario_input_type == ScenarioInputType.NEW:
|
|
4007
|
-
managed_inputs = []
|
|
4008
|
-
for data in scenario.scenario_input.scenario_input_data:
|
|
4009
|
-
upload_url = self.upload_url()
|
|
4010
|
-
self.upload_large_input(input=data, upload_url=upload_url)
|
|
4011
|
-
name, id = safe_name_and_id(prefix="man-input", entity_id=scenario_id)
|
|
4012
|
-
managed_input = self.new_managed_input(
|
|
4013
|
-
id=id,
|
|
4014
|
-
name=name,
|
|
4015
|
-
description=f"Automatically created from scenario test: {id}",
|
|
4016
|
-
upload_id=upload_url.upload_id,
|
|
4017
|
-
)
|
|
4018
|
-
managed_inputs.append(managed_input)
|
|
4019
|
-
|
|
4020
|
-
name, id = safe_name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
4021
|
-
input_set = self.new_input_set(
|
|
4022
|
-
id=id,
|
|
4023
|
-
name=name,
|
|
4024
|
-
description=f"Automatically created from scenario test: {id}",
|
|
4025
|
-
maximum_runs=20,
|
|
4026
|
-
inputs=managed_inputs,
|
|
4027
|
-
)
|
|
4028
|
-
return input_set
|
|
4029
|
-
|
|
4030
|
-
raise ValueError(f"Unknown scenario input type: {scenario.scenario_input.scenario_input_type}")
|
|
4031
|
-
|
|
4032
|
-
def __package_inputs(self, dir_path: str) -> str:
|
|
4033
|
-
"""
|
|
4034
|
-
This is an auxiliary function for packaging the inputs found in the
|
|
4035
|
-
provided `dir_path`. All the files found in the directory are tarred and
|
|
4036
|
-
g-zipped. This function returns the tar file path that contains the
|
|
4037
|
-
packaged inputs.
|
|
4038
|
-
"""
|
|
4039
|
-
|
|
4040
|
-
# Create a temporary directory for the output
|
|
4041
|
-
output_dir = tempfile.mkdtemp(prefix="nextmv-inputs-out-")
|
|
4042
|
-
|
|
4043
|
-
# Define the output tar file name and path
|
|
4044
|
-
tar_filename = "inputs.tar.gz"
|
|
4045
|
-
tar_file_path = os.path.join(output_dir, tar_filename)
|
|
4046
|
-
|
|
4047
|
-
# Create the tar.gz file
|
|
4048
|
-
with tarfile.open(tar_file_path, "w:gz") as tar:
|
|
4049
|
-
for root, _, files in os.walk(dir_path):
|
|
4050
|
-
for file in files:
|
|
4051
|
-
if file == tar_filename:
|
|
4052
|
-
continue
|
|
4053
|
-
|
|
4054
|
-
file_path = os.path.join(root, file)
|
|
4055
|
-
|
|
4056
|
-
# Skip directories, only process files
|
|
4057
|
-
if os.path.isdir(file_path):
|
|
4058
|
-
continue
|
|
4059
|
-
|
|
4060
|
-
# Create relative path for the archive
|
|
4061
|
-
arcname = os.path.relpath(file_path, start=dir_path)
|
|
4062
|
-
tar.add(file_path, arcname=arcname)
|
|
4063
|
-
|
|
4064
|
-
return tar_file_path
|
|
4065
|
-
|
|
4066
|
-
def __upload_url_required(
|
|
4067
|
-
self,
|
|
4068
|
-
upload_id_used: bool,
|
|
4069
|
-
input_size: int,
|
|
4070
|
-
tar_file: str,
|
|
4071
|
-
input: Input | dict[str, Any] | BaseModel | str = None,
|
|
4072
|
-
) -> bool:
|
|
4073
|
-
"""
|
|
4074
|
-
Auxiliary function to determine if an upload URL is required
|
|
4075
|
-
based on the input size, type, and configuration.
|
|
4076
|
-
"""
|
|
4077
|
-
|
|
4078
|
-
if upload_id_used:
|
|
4079
|
-
return False
|
|
4080
|
-
|
|
4081
|
-
non_json_payload = False
|
|
4082
|
-
if isinstance(input, str):
|
|
4083
|
-
non_json_payload = True
|
|
4084
|
-
elif isinstance(input, Input) and input.input_format != InputFormat.JSON:
|
|
4085
|
-
non_json_payload = True
|
|
4086
|
-
elif tar_file is not None and tar_file != "":
|
|
4087
|
-
non_json_payload = True
|
|
4088
|
-
|
|
4089
|
-
size_exceeds = input_size > _MAX_RUN_SIZE
|
|
4090
|
-
|
|
4091
|
-
return size_exceeds or non_json_payload
|
|
4092
|
-
|
|
4093
|
-
def __extract_input_data(
|
|
4094
|
-
self,
|
|
4095
|
-
input: Input | dict[str, Any] | BaseModel | str = None,
|
|
4096
|
-
) -> dict[str, Any] | str | None:
|
|
4097
|
-
"""
|
|
4098
|
-
Auxiliary function to extract the input data from the input, based on
|
|
4099
|
-
its type.
|
|
4100
|
-
"""
|
|
4101
|
-
|
|
4102
|
-
input_data = None
|
|
4103
|
-
if isinstance(input, BaseModel):
|
|
4104
|
-
input_data = input.to_dict()
|
|
4105
|
-
elif isinstance(input, dict) or isinstance(input, str):
|
|
4106
|
-
input_data = input
|
|
4107
|
-
elif isinstance(input, Input):
|
|
4108
|
-
input_data = input.data
|
|
4109
|
-
|
|
4110
|
-
return input_data
|
|
4111
|
-
|
|
4112
|
-
def __extract_options_dict(
|
|
4113
|
-
self,
|
|
4114
|
-
options: Options | dict[str, str] | None = None,
|
|
4115
|
-
json_configurations: dict[str, Any] | None = None,
|
|
4116
|
-
) -> dict[str, str]:
|
|
4117
|
-
"""
|
|
4118
|
-
Auxiliary function to extract the options that will be sent to the
|
|
4119
|
-
application for execution.
|
|
4120
|
-
"""
|
|
4121
|
-
|
|
4122
|
-
options_dict = {}
|
|
4123
|
-
if options is not None:
|
|
4124
|
-
if isinstance(options, Options):
|
|
4125
|
-
options_dict = options.to_dict_cloud()
|
|
4126
|
-
|
|
4127
|
-
elif isinstance(options, dict):
|
|
4128
|
-
for k, v in options.items():
|
|
4129
|
-
if isinstance(v, str):
|
|
4130
|
-
options_dict[k] = v
|
|
4131
|
-
continue
|
|
4132
|
-
|
|
4133
|
-
options_dict[k] = deflated_serialize_json(v, json_configurations=json_configurations)
|
|
4134
|
-
|
|
4135
|
-
return options_dict
|
|
4136
|
-
|
|
4137
|
-
def __extract_run_config(
|
|
4138
|
-
self,
|
|
4139
|
-
input: Input | dict[str, Any] | BaseModel | str = None,
|
|
4140
|
-
configuration: RunConfiguration | dict[str, Any] | None = None,
|
|
4141
|
-
dir_path: str | None = None,
|
|
4142
|
-
) -> dict[str, Any]:
|
|
4143
|
-
"""
|
|
4144
|
-
Auxiliary function to extract the run configuration that will be sent
|
|
4145
|
-
to the application for execution.
|
|
4146
|
-
"""
|
|
4147
|
-
|
|
4148
|
-
if configuration is not None:
|
|
4149
|
-
configuration_dict = (
|
|
4150
|
-
configuration.to_dict() if isinstance(configuration, RunConfiguration) else configuration
|
|
4151
|
-
)
|
|
4152
|
-
return configuration_dict
|
|
4153
|
-
|
|
4154
|
-
configuration = RunConfiguration()
|
|
4155
|
-
configuration.resolve(input=input, dir_path=dir_path)
|
|
4156
|
-
configuration_dict = configuration.to_dict()
|
|
4157
|
-
|
|
4158
|
-
return configuration_dict
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
def _is_not_exist_error(e: requests.HTTPError) -> bool:
|
|
4162
|
-
"""
|
|
4163
|
-
Check if the error is a known 404 Not Found error.
|
|
4164
|
-
|
|
4165
|
-
This is an internal helper function that examines HTTPError objects to determine
|
|
4166
|
-
if they represent a "Not Found" (404) condition, either directly or through a
|
|
4167
|
-
nested exception.
|
|
4168
|
-
|
|
4169
|
-
Parameters
|
|
4170
|
-
----------
|
|
4171
|
-
e : requests.HTTPError
|
|
4172
|
-
The HTTP error to check.
|
|
4173
|
-
|
|
4174
|
-
Returns
|
|
4175
|
-
-------
|
|
4176
|
-
bool
|
|
4177
|
-
True if the error is a 404 Not Found error, False otherwise.
|
|
4178
|
-
|
|
4179
|
-
Examples
|
|
4180
|
-
--------
|
|
4181
|
-
>>> try:
|
|
4182
|
-
... response = requests.get('https://api.example.com/nonexistent')
|
|
4183
|
-
... response.raise_for_status()
|
|
4184
|
-
... except requests.HTTPError as err:
|
|
4185
|
-
... if _is_not_exist_error(err):
|
|
4186
|
-
... print("Resource does not exist")
|
|
4187
|
-
... else:
|
|
4188
|
-
... print("Another error occurred")
|
|
4189
|
-
Resource does not exist
|
|
4190
|
-
"""
|
|
4191
|
-
if (
|
|
4192
|
-
# Check whether the error is caused by a 404 status code - meaning the app does not exist.
|
|
4193
|
-
(hasattr(e, "response") and hasattr(e.response, "status_code") and e.response.status_code == 404)
|
|
4194
|
-
or
|
|
4195
|
-
# Check a possibly nested exception as well.
|
|
4196
|
-
(
|
|
4197
|
-
hasattr(e, "__cause__")
|
|
4198
|
-
and hasattr(e.__cause__, "response")
|
|
4199
|
-
and hasattr(e.__cause__.response, "status_code")
|
|
4200
|
-
and e.__cause__.response.status_code == 404
|
|
4201
|
-
)
|
|
4202
|
-
):
|
|
4203
|
-
return True
|
|
4204
|
-
return False
|