nextmv 0.40.0__py3-none-any.whl → 1.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nextmv/__about__.py +1 -1
- nextmv/__init__.py +2 -0
- nextmv/cli/CONTRIBUTING.md +511 -0
- nextmv/cli/cloud/__init__.py +45 -0
- nextmv/cli/cloud/acceptance/__init__.py +27 -0
- nextmv/cli/cloud/acceptance/create.py +393 -0
- nextmv/cli/cloud/acceptance/delete.py +68 -0
- nextmv/cli/cloud/acceptance/get.py +104 -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 +60 -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 +141 -0
- nextmv/cli/cloud/app/delete.py +58 -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 +137 -0
- nextmv/cli/cloud/app/update.py +124 -0
- nextmv/cli/cloud/batch/__init__.py +29 -0
- nextmv/cli/cloud/batch/create.py +454 -0
- nextmv/cli/cloud/batch/delete.py +68 -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 +31 -0
- nextmv/cli/cloud/ensemble/create.py +414 -0
- nextmv/cli/cloud/ensemble/delete.py +67 -0
- nextmv/cli/cloud/ensemble/get.py +65 -0
- nextmv/cli/cloud/ensemble/update.py +103 -0
- nextmv/cli/cloud/input_set/__init__.py +30 -0
- nextmv/cli/cloud/input_set/create.py +168 -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 +290 -0
- nextmv/cli/cloud/instance/delete.py +62 -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 +146 -0
- nextmv/cli/cloud/managed_input/delete.py +65 -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 +530 -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 +167 -0
- nextmv/cli/cloud/run/metadata.py +67 -0
- nextmv/cli/cloud/run/track.py +501 -0
- nextmv/cli/cloud/scenario/__init__.py +29 -0
- nextmv/cli/cloud/scenario/create.py +451 -0
- nextmv/cli/cloud/scenario/delete.py +65 -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 +67 -0
- nextmv/cli/cloud/secrets/get.py +66 -0
- nextmv/cli/cloud/secrets/list.py +60 -0
- nextmv/cli/cloud/secrets/update.py +147 -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 +97 -0
- nextmv/cli/cloud/version/delete.py +62 -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 +3 -3
- nextmv/cli/community/list.py +1 -1
- nextmv/cli/configuration/__init__.py +23 -0
- nextmv/cli/configuration/config.py +68 -4
- nextmv/cli/configuration/create.py +14 -15
- nextmv/cli/configuration/delete.py +24 -12
- nextmv/cli/configuration/list.py +1 -1
- nextmv/cli/main.py +58 -16
- nextmv/cli/message.py +153 -0
- nextmv/cli/options.py +168 -0
- nextmv/cli/version.py +20 -1
- nextmv/cloud/__init__.py +4 -1
- nextmv/cloud/acceptance_test.py +19 -18
- nextmv/cloud/account.py +268 -24
- nextmv/cloud/application/__init__.py +955 -0
- nextmv/cloud/application/_acceptance.py +419 -0
- nextmv/cloud/application/_batch_scenario.py +860 -0
- nextmv/cloud/application/_ensemble.py +251 -0
- nextmv/cloud/application/_input_set.py +227 -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/_utils.py +54 -0
- nextmv/cloud/application/_version.py +303 -0
- nextmv/cloud/batch_experiment.py +3 -1
- nextmv/cloud/instance.py +11 -1
- nextmv/cloud/integration.py +1 -1
- nextmv/cloud/package.py +50 -9
- nextmv/input.py +20 -36
- nextmv/local/application.py +3 -15
- nextmv/polling.py +54 -16
- nextmv/run.py +83 -27
- {nextmv-0.40.0.dist-info → nextmv-1.0.0.dev0.dist-info}/METADATA +33 -8
- nextmv-1.0.0.dev0.dist-info/RECORD +158 -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.dev0.dist-info}/WHEEL +0 -0
- {nextmv-0.40.0.dist-info → nextmv-1.0.0.dev0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application mixin for managing app secrets.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
|
|
8
|
+
from nextmv.safe import safe_id
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from . import Application
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApplicationSecretsMixin:
|
|
15
|
+
"""
|
|
16
|
+
Mixin class for managing app secrets within an application.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def delete_secrets_collection(self: "Application", secrets_collection_id: str) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Delete a secrets collection.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
secrets_collection_id : str
|
|
26
|
+
ID of the secrets collection to delete.
|
|
27
|
+
|
|
28
|
+
Raises
|
|
29
|
+
------
|
|
30
|
+
requests.HTTPError
|
|
31
|
+
If the response status code is not 2xx.
|
|
32
|
+
|
|
33
|
+
Examples
|
|
34
|
+
--------
|
|
35
|
+
>>> app.delete_secrets_collection("secrets-123")
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_ = self.client.request(
|
|
39
|
+
method="DELETE",
|
|
40
|
+
endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def list_secrets_collections(self: "Application") -> list[SecretsCollectionSummary]:
|
|
44
|
+
"""
|
|
45
|
+
List all secrets collections.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
list[SecretsCollectionSummary]
|
|
50
|
+
List of all secrets collections associated with this application.
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
------
|
|
54
|
+
requests.HTTPError
|
|
55
|
+
If the response status code is not 2xx.
|
|
56
|
+
|
|
57
|
+
Examples
|
|
58
|
+
--------
|
|
59
|
+
>>> collections = app.list_secrets_collections()
|
|
60
|
+
>>> for collection in collections:
|
|
61
|
+
... print(collection.name)
|
|
62
|
+
'API Keys'
|
|
63
|
+
'Database Credentials'
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
response = self.client.request(
|
|
67
|
+
method="GET",
|
|
68
|
+
endpoint=f"{self.endpoint}/secrets",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return [SecretsCollectionSummary.from_dict(secrets) for secrets in response.json()["items"]]
|
|
72
|
+
|
|
73
|
+
def new_secrets_collection(
|
|
74
|
+
self: "Application",
|
|
75
|
+
secrets: list[Secret],
|
|
76
|
+
id: str | None = None,
|
|
77
|
+
name: str | None = None,
|
|
78
|
+
description: str | None = None,
|
|
79
|
+
) -> SecretsCollectionSummary:
|
|
80
|
+
"""
|
|
81
|
+
Create a new secrets collection.
|
|
82
|
+
|
|
83
|
+
This method creates a new secrets collection with the provided secrets.
|
|
84
|
+
A secrets collection is a group of key-value pairs that can be used by
|
|
85
|
+
your application instances during execution. If no secrets are
|
|
86
|
+
provided, a ValueError is raised. If the `id` or `name` parameters are
|
|
87
|
+
not provided, they will be generated based on a unique ID.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
secrets : list[Secret]
|
|
92
|
+
List of secrets to use for the secrets collection. Each secret
|
|
93
|
+
should be an instance of the Secret class containing a key and
|
|
94
|
+
value.
|
|
95
|
+
id : str | None, default=None
|
|
96
|
+
ID of the secrets collection. If not provided, a unique ID will be
|
|
97
|
+
generated.
|
|
98
|
+
name : str | None, default=None
|
|
99
|
+
Name of the secrets collection. If not provided, the ID will be
|
|
100
|
+
used.
|
|
101
|
+
description : Optional[str], default=None
|
|
102
|
+
Description of the secrets collection.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
SecretsCollectionSummary
|
|
107
|
+
Summary of the secrets collection including its metadata.
|
|
108
|
+
|
|
109
|
+
Raises
|
|
110
|
+
------
|
|
111
|
+
ValueError
|
|
112
|
+
If no secrets are provided.
|
|
113
|
+
requests.HTTPError
|
|
114
|
+
If the response status code is not 2xx.
|
|
115
|
+
|
|
116
|
+
Examples
|
|
117
|
+
--------
|
|
118
|
+
>>> # Create a new secrets collection with API keys
|
|
119
|
+
>>> from nextmv.cloud import Secret
|
|
120
|
+
>>> secrets = [
|
|
121
|
+
... Secret(
|
|
122
|
+
... location="API_KEY",
|
|
123
|
+
... value="your-api-key",
|
|
124
|
+
... secret_type=SecretType.ENV,
|
|
125
|
+
... ),
|
|
126
|
+
... Secret(
|
|
127
|
+
... location="DATABASE_URL",
|
|
128
|
+
... value="your-database-url",
|
|
129
|
+
... secret_type=SecretType.ENV,
|
|
130
|
+
... ),
|
|
131
|
+
... ]
|
|
132
|
+
>>> collection = app.new_secrets_collection(
|
|
133
|
+
... secrets=secrets,
|
|
134
|
+
... id="api-secrets",
|
|
135
|
+
... name="API Secrets",
|
|
136
|
+
... description="Collection of API secrets for external services"
|
|
137
|
+
... )
|
|
138
|
+
>>> print(collection.id)
|
|
139
|
+
'api-secrets'
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
if len(secrets) == 0:
|
|
143
|
+
raise ValueError("secrets must be provided")
|
|
144
|
+
|
|
145
|
+
if id is None or id == "":
|
|
146
|
+
id = safe_id(prefix="secrets")
|
|
147
|
+
if name is None or name == "":
|
|
148
|
+
name = id
|
|
149
|
+
|
|
150
|
+
payload = {
|
|
151
|
+
"id": id,
|
|
152
|
+
"name": name,
|
|
153
|
+
"secrets": [secret.to_dict() for secret in secrets],
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if description is not None:
|
|
157
|
+
payload["description"] = description
|
|
158
|
+
|
|
159
|
+
response = self.client.request(
|
|
160
|
+
method="POST",
|
|
161
|
+
endpoint=f"{self.endpoint}/secrets",
|
|
162
|
+
payload=payload,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return SecretsCollectionSummary.from_dict(response.json())
|
|
166
|
+
|
|
167
|
+
def secrets_collection(self: "Application", secrets_collection_id: str) -> SecretsCollection:
|
|
168
|
+
"""
|
|
169
|
+
Get a secrets collection.
|
|
170
|
+
|
|
171
|
+
This method retrieves a secrets collection by its ID. A secrets collection
|
|
172
|
+
is a group of key-value pairs that can be used by your application
|
|
173
|
+
instances during execution.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
secrets_collection_id : str
|
|
178
|
+
ID of the secrets collection to retrieve.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
SecretsCollection
|
|
183
|
+
The requested secrets collection, including all secret values
|
|
184
|
+
and metadata.
|
|
185
|
+
|
|
186
|
+
Raises
|
|
187
|
+
------
|
|
188
|
+
requests.HTTPError
|
|
189
|
+
If the response status code is not 2xx.
|
|
190
|
+
|
|
191
|
+
Examples
|
|
192
|
+
--------
|
|
193
|
+
>>> # Retrieve a secrets collection
|
|
194
|
+
>>> collection = app.secrets_collection("api-secrets")
|
|
195
|
+
>>> print(collection.name)
|
|
196
|
+
'API Secrets'
|
|
197
|
+
>>> print(len(collection.secrets))
|
|
198
|
+
2
|
|
199
|
+
>>> for secret in collection.secrets:
|
|
200
|
+
... print(secret.location)
|
|
201
|
+
'API_KEY'
|
|
202
|
+
'DATABASE_URL'
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
response = self.client.request(
|
|
206
|
+
method="GET",
|
|
207
|
+
endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return SecretsCollection.from_dict(response.json())
|
|
211
|
+
|
|
212
|
+
def update_secrets_collection(
|
|
213
|
+
self: "Application",
|
|
214
|
+
secrets_collection_id: str,
|
|
215
|
+
name: str | None = None,
|
|
216
|
+
description: str | None = None,
|
|
217
|
+
secrets: list[Secret | dict[str, Any]] | None = None,
|
|
218
|
+
) -> SecretsCollectionSummary:
|
|
219
|
+
"""
|
|
220
|
+
Update a secrets collection.
|
|
221
|
+
|
|
222
|
+
This method updates an existing secrets collection with new values for name,
|
|
223
|
+
description, and secrets. A secrets collection is a group of key-value pairs
|
|
224
|
+
that can be used by your application instances during execution.
|
|
225
|
+
|
|
226
|
+
Parameters
|
|
227
|
+
----------
|
|
228
|
+
secrets_collection_id : str
|
|
229
|
+
ID of the secrets collection to update.
|
|
230
|
+
name : Optional[str], default=None
|
|
231
|
+
Optional new name for the secrets collection.
|
|
232
|
+
description : Optional[str], default=None
|
|
233
|
+
Optional new description for the secrets collection.
|
|
234
|
+
secrets : Optional[list[Secret | dict[str, Any]]], default=None
|
|
235
|
+
Optional list of secrets to update. Each secret should be an
|
|
236
|
+
instance of the Secret class containing a key and value.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
SecretsCollectionSummary
|
|
241
|
+
Summary of the updated secrets collection including its metadata.
|
|
242
|
+
|
|
243
|
+
Raises
|
|
244
|
+
------
|
|
245
|
+
ValueError
|
|
246
|
+
If no secrets are provided.
|
|
247
|
+
requests.HTTPError
|
|
248
|
+
If the response status code is not 2xx.
|
|
249
|
+
|
|
250
|
+
Examples
|
|
251
|
+
--------
|
|
252
|
+
>>> # Update an existing secrets collection
|
|
253
|
+
>>> from nextmv.cloud import Secret
|
|
254
|
+
>>> updated_secrets = [
|
|
255
|
+
... Secret(key="API_KEY", value="new-api-key"),
|
|
256
|
+
... Secret(key="DATABASE_URL", value="new-database-url")
|
|
257
|
+
... ]
|
|
258
|
+
>>> updated_collection = app.update_secrets_collection(
|
|
259
|
+
... secrets_collection_id="api-secrets",
|
|
260
|
+
... name="Updated API Secrets",
|
|
261
|
+
... description="Updated collection of API secrets",
|
|
262
|
+
... secrets=updated_secrets
|
|
263
|
+
... )
|
|
264
|
+
>>> print(updated_collection.id)
|
|
265
|
+
'api-secrets'
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
collection = self.secrets_collection(secrets_collection_id)
|
|
269
|
+
collection_dict = collection.to_dict()
|
|
270
|
+
payload = collection_dict.copy()
|
|
271
|
+
|
|
272
|
+
if name is not None:
|
|
273
|
+
payload["name"] = name
|
|
274
|
+
if description is not None:
|
|
275
|
+
payload["description"] = description
|
|
276
|
+
if secrets is not None and len(secrets) > 0:
|
|
277
|
+
secrets_dicts = []
|
|
278
|
+
for ix, secret in enumerate(secrets):
|
|
279
|
+
if isinstance(secret, dict):
|
|
280
|
+
secrets_dicts.append(secret)
|
|
281
|
+
elif isinstance(secret, Secret):
|
|
282
|
+
secrets_dicts.append(secret.to_dict())
|
|
283
|
+
else:
|
|
284
|
+
raise ValueError(f"secret at index {ix} must be either a Secret or dict object")
|
|
285
|
+
|
|
286
|
+
payload["secrets"] = secrets_dicts
|
|
287
|
+
|
|
288
|
+
response = self.client.request(
|
|
289
|
+
method="PUT",
|
|
290
|
+
endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
|
|
291
|
+
payload=payload,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return SecretsCollectionSummary.from_dict(response.json())
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for general utility methods for Application.
|
|
3
|
+
|
|
4
|
+
Avoid abusing this module, as you should try to keep domain-specific
|
|
5
|
+
functionality together and not rely on generic utilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _is_not_exist_error(e: requests.HTTPError) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
Check if the error is a known 404 Not Found error.
|
|
14
|
+
|
|
15
|
+
This is an internal helper function that examines HTTPError objects to determine
|
|
16
|
+
if they represent a "Not Found" (404) condition, either directly or through a
|
|
17
|
+
nested exception.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
e : requests.HTTPError
|
|
22
|
+
The HTTP error to check.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
bool
|
|
27
|
+
True if the error is a 404 Not Found error, False otherwise.
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
--------
|
|
31
|
+
>>> try:
|
|
32
|
+
... response = requests.get('https://api.example.com/nonexistent')
|
|
33
|
+
... response.raise_for_status()
|
|
34
|
+
... except requests.HTTPError as err:
|
|
35
|
+
... if _is_not_exist_error(err):
|
|
36
|
+
... print("Resource does not exist")
|
|
37
|
+
... else:
|
|
38
|
+
... print("Another error occurred")
|
|
39
|
+
Resource does not exist
|
|
40
|
+
"""
|
|
41
|
+
if (
|
|
42
|
+
# Check whether the error is caused by a 404 status code - meaning the app does not exist.
|
|
43
|
+
(hasattr(e, "response") and hasattr(e.response, "status_code") and e.response.status_code == 404)
|
|
44
|
+
or
|
|
45
|
+
# Check a possibly nested exception as well.
|
|
46
|
+
(
|
|
47
|
+
hasattr(e, "__cause__")
|
|
48
|
+
and hasattr(e.__cause__, "response")
|
|
49
|
+
and hasattr(e.__cause__.response, "status_code")
|
|
50
|
+
and e.__cause__.response.status_code == 404
|
|
51
|
+
)
|
|
52
|
+
):
|
|
53
|
+
return True
|
|
54
|
+
return False
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Application mixin for managing app versions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from nextmv.cloud.application._utils import _is_not_exist_error
|
|
10
|
+
from nextmv.cloud.version import Version
|
|
11
|
+
from nextmv.safe import safe_id
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from . import Application
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ApplicationVersionMixin:
|
|
18
|
+
"""
|
|
19
|
+
Mixin class for managing app versions within an application.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def delete_version(self: "Application", version_id: str) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Delete a version.
|
|
25
|
+
|
|
26
|
+
Permanently removes the specified version from the application.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
version_id : str
|
|
31
|
+
ID of the version to delete.
|
|
32
|
+
|
|
33
|
+
Raises
|
|
34
|
+
------
|
|
35
|
+
requests.HTTPError
|
|
36
|
+
If the response status code is not 2xx.
|
|
37
|
+
|
|
38
|
+
Examples
|
|
39
|
+
--------
|
|
40
|
+
>>> app.delete_version("v1.0.0") # Permanently deletes the version
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
_ = self.client.request(
|
|
44
|
+
method="DELETE",
|
|
45
|
+
endpoint=f"{self.endpoint}/versions/{version_id}",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def list_versions(self: "Application") -> list[Version]:
|
|
49
|
+
"""
|
|
50
|
+
List all versions.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
list[Version]
|
|
55
|
+
List of all versions associated with this application.
|
|
56
|
+
|
|
57
|
+
Raises
|
|
58
|
+
------
|
|
59
|
+
requests.HTTPError
|
|
60
|
+
If the response status code is not 2xx.
|
|
61
|
+
|
|
62
|
+
Examples
|
|
63
|
+
--------
|
|
64
|
+
>>> versions = app.list_versions()
|
|
65
|
+
>>> for version in versions:
|
|
66
|
+
... print(version.name)
|
|
67
|
+
'v1.0.0'
|
|
68
|
+
'v1.1.0'
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
response = self.client.request(
|
|
72
|
+
method="GET",
|
|
73
|
+
endpoint=f"{self.endpoint}/versions",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return [Version.from_dict(version) for version in response.json()]
|
|
77
|
+
|
|
78
|
+
def new_version(
|
|
79
|
+
self: "Application",
|
|
80
|
+
id: str | None = None,
|
|
81
|
+
name: str | None = None,
|
|
82
|
+
description: str | None = None,
|
|
83
|
+
exist_ok: bool = False,
|
|
84
|
+
) -> Version:
|
|
85
|
+
"""
|
|
86
|
+
Create a new version using the latest pushed executable.
|
|
87
|
+
|
|
88
|
+
This method creates a new version of the application using the current development
|
|
89
|
+
binary. Application versions represent different iterations of your application's
|
|
90
|
+
code and configuration that can be deployed.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
id : Optional[str], default=None
|
|
95
|
+
ID of the version. If not provided, a unique ID will be generated.
|
|
96
|
+
name : Optional[str], default=None
|
|
97
|
+
Name of the version. If not provided, a name will be generated.
|
|
98
|
+
description : Optional[str], default=None
|
|
99
|
+
Description of the version. If not provided, a description will be generated.
|
|
100
|
+
exist_ok : bool, default=False
|
|
101
|
+
If True and a version with the same ID already exists,
|
|
102
|
+
return the existing version instead of creating a new one.
|
|
103
|
+
If True, the 'id' parameter must be provided.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
Version
|
|
108
|
+
The newly created (or existing) version.
|
|
109
|
+
|
|
110
|
+
Raises
|
|
111
|
+
------
|
|
112
|
+
ValueError
|
|
113
|
+
If exist_ok is True and id is None.
|
|
114
|
+
requests.HTTPError
|
|
115
|
+
If the response status code is not 2xx.
|
|
116
|
+
|
|
117
|
+
Examples
|
|
118
|
+
--------
|
|
119
|
+
>>> # Create a new version
|
|
120
|
+
>>> version = app.new_version(
|
|
121
|
+
... id="v1.0.0",
|
|
122
|
+
... name="Initial Release",
|
|
123
|
+
... description="First stable version"
|
|
124
|
+
... )
|
|
125
|
+
>>> print(version.id)
|
|
126
|
+
'v1.0.0'
|
|
127
|
+
|
|
128
|
+
>>> # Get or create a version with exist_ok
|
|
129
|
+
>>> version = app.new_version(
|
|
130
|
+
... id="v1.0.0",
|
|
131
|
+
... exist_ok=True
|
|
132
|
+
... )
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
if exist_ok and id is None:
|
|
136
|
+
raise ValueError("If exist_ok is True, id must be provided")
|
|
137
|
+
|
|
138
|
+
if exist_ok and self.version_exists(version_id=id):
|
|
139
|
+
return self.version(version_id=id)
|
|
140
|
+
|
|
141
|
+
if id is None:
|
|
142
|
+
id = safe_id(prefix="version")
|
|
143
|
+
if name is None:
|
|
144
|
+
name = id
|
|
145
|
+
|
|
146
|
+
payload = {
|
|
147
|
+
"id": id,
|
|
148
|
+
"name": name,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if description is not None:
|
|
152
|
+
payload["description"] = description
|
|
153
|
+
|
|
154
|
+
response = self.client.request(
|
|
155
|
+
method="POST",
|
|
156
|
+
endpoint=f"{self.endpoint}/versions",
|
|
157
|
+
payload=payload,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return Version.from_dict(response.json())
|
|
161
|
+
|
|
162
|
+
def update_version(
|
|
163
|
+
self: "Application",
|
|
164
|
+
version_id: str,
|
|
165
|
+
name: str | None = None,
|
|
166
|
+
description: str | None = None,
|
|
167
|
+
) -> Version:
|
|
168
|
+
"""
|
|
169
|
+
Update a version.
|
|
170
|
+
|
|
171
|
+
This method updates a specific version of the application. It mimics a
|
|
172
|
+
PATCH operation by allowing you to update only the name and/or description
|
|
173
|
+
fields while preserving all other fields.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
version_id : str
|
|
178
|
+
ID of the version to update.
|
|
179
|
+
name : Optional[str], default=None
|
|
180
|
+
Optional new name for the version.
|
|
181
|
+
description : Optional[str], default=None
|
|
182
|
+
Optional new description for the version.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
Version
|
|
187
|
+
The updated version object.
|
|
188
|
+
|
|
189
|
+
Raises
|
|
190
|
+
------
|
|
191
|
+
requests.HTTPError
|
|
192
|
+
If the response status code is not 2xx.
|
|
193
|
+
|
|
194
|
+
Examples
|
|
195
|
+
--------
|
|
196
|
+
>>> # Update a version's name
|
|
197
|
+
>>> updated = app.update_version("v1.0.0", name="Version 1.0")
|
|
198
|
+
>>> print(updated.name)
|
|
199
|
+
'Version 1.0'
|
|
200
|
+
|
|
201
|
+
>>> # Update a version's description
|
|
202
|
+
>>> updated = app.update_version("v1.0.0", description="Initial release")
|
|
203
|
+
>>> print(updated.description)
|
|
204
|
+
'Initial release'
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
version = self.version(version_id=version_id)
|
|
208
|
+
version_dict = version.to_dict()
|
|
209
|
+
payload = version_dict.copy()
|
|
210
|
+
|
|
211
|
+
if name is not None:
|
|
212
|
+
payload["name"] = name
|
|
213
|
+
if description is not None:
|
|
214
|
+
payload["description"] = description
|
|
215
|
+
|
|
216
|
+
response = self.client.request(
|
|
217
|
+
method="PUT",
|
|
218
|
+
endpoint=f"{self.endpoint}/versions/{version_id}",
|
|
219
|
+
payload=payload,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
return Version.from_dict(response.json())
|
|
223
|
+
|
|
224
|
+
def version(self: "Application", version_id: str) -> Version:
|
|
225
|
+
"""
|
|
226
|
+
Get a version.
|
|
227
|
+
|
|
228
|
+
Retrieves a specific version of the application by its ID. Application versions
|
|
229
|
+
represent different iterations of your application's code and configuration.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
version_id : str
|
|
234
|
+
ID of the version to retrieve.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
Version
|
|
239
|
+
The version object containing details about the requested application version.
|
|
240
|
+
|
|
241
|
+
Raises
|
|
242
|
+
------
|
|
243
|
+
requests.HTTPError
|
|
244
|
+
If the response status code is not 2xx.
|
|
245
|
+
|
|
246
|
+
Examples
|
|
247
|
+
--------
|
|
248
|
+
>>> # Retrieve a specific version
|
|
249
|
+
>>> version = app.version("v1.0.0")
|
|
250
|
+
>>> print(version.id)
|
|
251
|
+
'v1.0.0'
|
|
252
|
+
>>> print(version.name)
|
|
253
|
+
'Initial Release'
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
response = self.client.request(
|
|
257
|
+
method="GET",
|
|
258
|
+
endpoint=f"{self.endpoint}/versions/{version_id}",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return Version.from_dict(response.json())
|
|
262
|
+
|
|
263
|
+
def version_exists(self: "Application", version_id: str) -> bool:
|
|
264
|
+
"""
|
|
265
|
+
Check if a version exists.
|
|
266
|
+
|
|
267
|
+
This method checks if a specific version of the application exists by
|
|
268
|
+
attempting to retrieve it. It handles HTTP errors for non-existent versions
|
|
269
|
+
and returns a boolean indicating existence.
|
|
270
|
+
|
|
271
|
+
Parameters
|
|
272
|
+
----------
|
|
273
|
+
version_id : str
|
|
274
|
+
ID of the version to check for existence.
|
|
275
|
+
|
|
276
|
+
Returns
|
|
277
|
+
-------
|
|
278
|
+
bool
|
|
279
|
+
True if the version exists, False otherwise.
|
|
280
|
+
|
|
281
|
+
Raises
|
|
282
|
+
------
|
|
283
|
+
requests.HTTPError
|
|
284
|
+
If an HTTP error occurs that is not related to the non-existence
|
|
285
|
+
of the version.
|
|
286
|
+
|
|
287
|
+
Examples
|
|
288
|
+
--------
|
|
289
|
+
>>> # Check if a version exists
|
|
290
|
+
>>> exists = app.version_exists("v1.0.0")
|
|
291
|
+
>>> if exists:
|
|
292
|
+
... print("Version exists!")
|
|
293
|
+
... else:
|
|
294
|
+
... print("Version does not exist.")
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
self.version(version_id=version_id)
|
|
299
|
+
return True
|
|
300
|
+
except requests.HTTPError as e:
|
|
301
|
+
if _is_not_exist_error(e):
|
|
302
|
+
return False
|
|
303
|
+
raise e
|
nextmv/cloud/batch_experiment.py
CHANGED
|
@@ -223,7 +223,9 @@ class BatchExperimentRun(BaseModel):
|
|
|
223
223
|
Parameters
|
|
224
224
|
----------
|
|
225
225
|
input_id : str
|
|
226
|
-
ID of the input used for the experiment.
|
|
226
|
+
ID of the input used for the experiment. If a managed input is used,
|
|
227
|
+
this should be the ID of the managed input. If `input_set_id` is provided
|
|
228
|
+
for the run, this should be the ID of an input within that input set.
|
|
227
229
|
option_set : str
|
|
228
230
|
Option set used for the experiment. Defaults to None.
|
|
229
231
|
instance_id : str, optional
|
nextmv/cloud/instance.py
CHANGED
|
@@ -15,7 +15,7 @@ Instance
|
|
|
15
15
|
from datetime import datetime
|
|
16
16
|
|
|
17
17
|
from nextmv.base_model import BaseModel
|
|
18
|
-
from nextmv.run import RunQueuing
|
|
18
|
+
from nextmv.run import Format, RunQueuing
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class InstanceConfiguration(BaseModel):
|
|
@@ -54,6 +54,9 @@ class InstanceConfiguration(BaseModel):
|
|
|
54
54
|
|
|
55
55
|
execution_class: str | None = None
|
|
56
56
|
"""Execution class for the instance."""
|
|
57
|
+
format: Format | None = None
|
|
58
|
+
"""Input format for the instance, if applicable. When configuring an
|
|
59
|
+
instance, only the `format.format_input` attribute is used."""
|
|
57
60
|
options: dict | None = None
|
|
58
61
|
"""Options of the app that the instance uses."""
|
|
59
62
|
secrets_collection_id: str | None = None
|
|
@@ -82,6 +85,13 @@ class InstanceConfiguration(BaseModel):
|
|
|
82
85
|
|
|
83
86
|
self.execution_class = integration_val
|
|
84
87
|
|
|
88
|
+
# Processes the format to ensure only format_input is set.
|
|
89
|
+
if self.format is not None and self.format.format_input is not None:
|
|
90
|
+
final_format = Format(format_input=self.format.format_input)
|
|
91
|
+
else:
|
|
92
|
+
final_format = None
|
|
93
|
+
self.format = final_format
|
|
94
|
+
|
|
85
95
|
|
|
86
96
|
class Instance(BaseModel):
|
|
87
97
|
"""An instance of an application tied to a version with configuration.
|
nextmv/cloud/integration.py
CHANGED
|
@@ -429,7 +429,7 @@ class Integration(BaseModel):
|
|
|
429
429
|
|
|
430
430
|
integration = self.get(client=self.client, integration_id=self.integration_id)
|
|
431
431
|
integration_dict = integration.to_dict()
|
|
432
|
-
payload = integration_dict
|
|
432
|
+
payload = integration_dict.copy()
|
|
433
433
|
|
|
434
434
|
if name is not None:
|
|
435
435
|
payload["name"] = name
|