nextmv 1.0.0.dev6__py3-none-any.whl → 1.0.0.dev8__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/_serialization.py +1 -1
- nextmv/cli/cloud/acceptance/create.py +12 -12
- nextmv/cli/cloud/app/push.py +15 -15
- nextmv/cli/cloud/input_set/__init__.py +0 -2
- nextmv/cli/cloud/run/create.py +9 -4
- nextmv/cli/cloud/shadow/stop.py +2 -14
- nextmv/cli/cloud/switchback/stop.py +2 -14
- nextmv/cli/community/clone.py +197 -11
- nextmv/cli/community/list.py +116 -46
- nextmv/cloud/__init__.py +0 -4
- nextmv/cloud/application/__init__.py +200 -1
- nextmv/cloud/application/_input_set.py +6 -42
- nextmv/cloud/application/_run.py +8 -1
- nextmv/cloud/application/_shadow.py +3 -9
- nextmv/cloud/application/_switchback.py +1 -10
- nextmv/cloud/batch_experiment.py +1 -3
- nextmv/cloud/client.py +1 -1
- nextmv/cloud/shadow.py +0 -25
- nextmv/default_app/main.py +4 -6
- nextmv/local/executor.py +83 -3
- nextmv/local/geojson_handler.py +1 -1
- nextmv/manifest.py +11 -7
- nextmv/model.py +2 -2
- nextmv/options.py +1 -1
- nextmv/output.py +57 -21
- nextmv/run.py +12 -3
- {nextmv-1.0.0.dev6.dist-info → nextmv-1.0.0.dev8.dist-info}/METADATA +1 -3
- {nextmv-1.0.0.dev6.dist-info → nextmv-1.0.0.dev8.dist-info}/RECORD +32 -34
- nextmv/cli/cloud/input_set/delete.py +0 -67
- nextmv/cloud/community.py +0 -441
- {nextmv-1.0.0.dev6.dist-info → nextmv-1.0.0.dev8.dist-info}/WHEEL +0 -0
- {nextmv-1.0.0.dev6.dist-info → nextmv-1.0.0.dev8.dist-info}/entry_points.txt +0 -0
- {nextmv-1.0.0.dev6.dist-info → nextmv-1.0.0.dev8.dist-info}/licenses/LICENSE +0 -0
nextmv/cli/community/list.py
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
This module defines the community list command for the Nextmv CLI.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import Annotated
|
|
5
|
+
from typing import Annotated, Any
|
|
6
6
|
|
|
7
|
+
import requests
|
|
7
8
|
import rich
|
|
8
9
|
import typer
|
|
10
|
+
import yaml
|
|
9
11
|
from rich.console import Console
|
|
10
12
|
from rich.table import Table
|
|
11
13
|
|
|
12
14
|
from nextmv.cli.configuration.config import build_client
|
|
13
15
|
from nextmv.cli.message import error
|
|
14
16
|
from nextmv.cli.options import ProfileOption
|
|
15
|
-
from nextmv.cloud.client import Client
|
|
16
|
-
from nextmv.cloud.community import CommunityApp, list_community_apps
|
|
17
17
|
|
|
18
18
|
# Set up subcommand application.
|
|
19
19
|
app = typer.Typer()
|
|
@@ -62,73 +62,96 @@ def list(
|
|
|
62
62
|
if app is not None and app == "":
|
|
63
63
|
error("The --app flag cannot be an empty string.")
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
manifest = download_manifest(profile=profile)
|
|
66
66
|
if flat and app is None:
|
|
67
|
-
|
|
67
|
+
apps_list(manifest)
|
|
68
68
|
raise typer.Exit()
|
|
69
69
|
elif not flat and app is None:
|
|
70
|
-
|
|
70
|
+
apps_table(manifest)
|
|
71
71
|
raise typer.Exit()
|
|
72
72
|
elif flat and app is not None and app != "":
|
|
73
|
-
|
|
73
|
+
versions_list(manifest, app)
|
|
74
74
|
raise typer.Exit()
|
|
75
75
|
elif not flat and app is not None and app != "":
|
|
76
|
-
|
|
76
|
+
versions_table(manifest, app)
|
|
77
77
|
raise typer.Exit()
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def
|
|
80
|
+
def download_manifest(profile: str | None = None) -> dict:
|
|
81
81
|
"""
|
|
82
|
-
|
|
82
|
+
Downloads and returns the community apps manifest.
|
|
83
83
|
|
|
84
84
|
Parameters
|
|
85
85
|
----------
|
|
86
|
-
|
|
87
|
-
The
|
|
86
|
+
profile : str | None
|
|
87
|
+
The profile name to use. If None, the default profile is used.
|
|
88
|
+
|
|
89
|
+
Returns
|
|
90
|
+
-------
|
|
91
|
+
dict
|
|
92
|
+
The community apps manifest as a dictionary.
|
|
93
|
+
|
|
94
|
+
Raises
|
|
95
|
+
requests.HTTPError
|
|
96
|
+
If the response status code is not 2xx.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
response = download_file(directory="community-apps", file="manifest.yml", profile=profile)
|
|
100
|
+
manifest = yaml.safe_load(response.text)
|
|
101
|
+
|
|
102
|
+
return manifest
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def apps_table(manifest: dict[str, Any]) -> None:
|
|
106
|
+
"""
|
|
107
|
+
This function prints a table of community apps from the manifest.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
manifest : dict[str, Any]
|
|
112
|
+
The community apps manifest.
|
|
88
113
|
"""
|
|
89
114
|
|
|
90
|
-
apps = list_community_apps(client)
|
|
91
115
|
table = Table("Name", "Type", "Latest", "Description", border_style="cyan", header_style="cyan")
|
|
92
|
-
for app in apps:
|
|
116
|
+
for app in manifest.get("apps", []):
|
|
93
117
|
table.add_row(
|
|
94
|
-
app.name,
|
|
95
|
-
app.
|
|
96
|
-
app.latest_app_version
|
|
97
|
-
app.description,
|
|
118
|
+
app.get("name", ""),
|
|
119
|
+
app.get("type", ""),
|
|
120
|
+
app.get("latest_app_version", ""),
|
|
121
|
+
app.get("description", ""),
|
|
98
122
|
)
|
|
99
123
|
|
|
100
124
|
console.print(table)
|
|
101
125
|
|
|
102
126
|
|
|
103
|
-
def
|
|
127
|
+
def apps_list(manifest: dict[str, Any]) -> None:
|
|
104
128
|
"""
|
|
105
|
-
This function prints a flat list of community app names.
|
|
129
|
+
This function prints a flat list of community app names from the manifest.
|
|
106
130
|
|
|
107
131
|
Parameters
|
|
108
132
|
----------
|
|
109
|
-
|
|
110
|
-
The
|
|
133
|
+
manifest : dict[str, Any]
|
|
134
|
+
The community apps manifest.
|
|
111
135
|
"""
|
|
112
136
|
|
|
113
|
-
|
|
114
|
-
names = [app.name for app in apps]
|
|
137
|
+
names = [app.get("name", "") for app in manifest.get("apps", [])]
|
|
115
138
|
print("\n".join(names))
|
|
116
139
|
|
|
117
140
|
|
|
118
|
-
def
|
|
141
|
+
def versions_table(manifest: dict[str, Any], app: str) -> None:
|
|
119
142
|
"""
|
|
120
143
|
This function prints a table of versions for a specific community app.
|
|
121
144
|
|
|
122
145
|
Parameters
|
|
123
146
|
----------
|
|
124
|
-
|
|
125
|
-
The
|
|
147
|
+
manifest : dict[str, Any]
|
|
148
|
+
The community apps manifest.
|
|
126
149
|
app : str
|
|
127
150
|
The name of the community app.
|
|
128
151
|
"""
|
|
129
152
|
|
|
130
|
-
|
|
131
|
-
latest_version =
|
|
153
|
+
app_obj = find_app(manifest, app)
|
|
154
|
+
latest_version = app_obj.get("latest_app_version", "")
|
|
132
155
|
|
|
133
156
|
# Add the latest version with indicator
|
|
134
157
|
table = Table("Version", "Latest?", border_style="cyan", header_style="cyan")
|
|
@@ -136,7 +159,7 @@ def _versions_table(client: Client, app: str) -> None:
|
|
|
136
159
|
table.add_row("", "") # Empty row to separate latest from others.
|
|
137
160
|
|
|
138
161
|
# Add all other versions (excluding the latest)
|
|
139
|
-
versions =
|
|
162
|
+
versions = app_obj.get("app_versions", [])
|
|
140
163
|
for version in versions:
|
|
141
164
|
if version != latest_version:
|
|
142
165
|
table.add_row(version, "")
|
|
@@ -144,52 +167,99 @@ def _versions_table(client: Client, app: str) -> None:
|
|
|
144
167
|
console.print(table)
|
|
145
168
|
|
|
146
169
|
|
|
147
|
-
def
|
|
170
|
+
def versions_list(manifest: dict[str, Any], app: str) -> None:
|
|
148
171
|
"""
|
|
149
172
|
This function prints a flat list of versions for a specific community app.
|
|
150
173
|
|
|
151
174
|
Parameters
|
|
152
175
|
----------
|
|
153
|
-
|
|
154
|
-
The
|
|
176
|
+
manifest : dict[str, Any]
|
|
177
|
+
The community apps manifest.
|
|
155
178
|
app : str
|
|
156
179
|
The name of the community app.
|
|
157
180
|
"""
|
|
158
181
|
|
|
159
|
-
|
|
160
|
-
versions =
|
|
182
|
+
app_obj = find_app(manifest, app)
|
|
183
|
+
versions = app_obj.get("app_versions", [])
|
|
161
184
|
|
|
162
185
|
versions_output = ""
|
|
163
186
|
for version in versions:
|
|
164
187
|
versions_output += f"{version}\n"
|
|
165
188
|
|
|
166
|
-
print("\n".join(
|
|
189
|
+
print("\n".join(app_obj.get("app_versions", [])))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def download_file(
|
|
193
|
+
directory: str,
|
|
194
|
+
file: str,
|
|
195
|
+
profile: str | None = None,
|
|
196
|
+
) -> requests.Response:
|
|
197
|
+
"""
|
|
198
|
+
Gets a file from an internal bucket and return it.
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
directory : str
|
|
203
|
+
The directory in the bucket where the file is located.
|
|
204
|
+
file : str
|
|
205
|
+
The name of the file to download.
|
|
206
|
+
profile : str | None
|
|
207
|
+
The profile name to use. If None, the default profile is used.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
requests.Response
|
|
212
|
+
The response object containing the file data.
|
|
213
|
+
|
|
214
|
+
Raises
|
|
215
|
+
requests.HTTPError
|
|
216
|
+
If the response status code is not 2xx.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
client = build_client(profile)
|
|
220
|
+
|
|
221
|
+
# Request the download URL for the file.
|
|
222
|
+
response = client.request(
|
|
223
|
+
method="GET",
|
|
224
|
+
endpoint="v0/internal/tools",
|
|
225
|
+
headers=client.headers | {"request-source": "cli"}, # Pass `client.headers` to preserve auth.
|
|
226
|
+
query_params={"file": f"{directory}/{file}"},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Use the URL obtained to download the file.
|
|
230
|
+
body = response.json()
|
|
231
|
+
download_response = client.request(
|
|
232
|
+
method="GET",
|
|
233
|
+
endpoint=body.get("url"),
|
|
234
|
+
headers={"Content-Type": "application/json"},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return download_response
|
|
167
238
|
|
|
168
239
|
|
|
169
|
-
def
|
|
240
|
+
def find_app(manifest: dict[str, Any], app: str) -> dict[str, Any] | None:
|
|
170
241
|
"""
|
|
171
242
|
Finds and returns a community app from the manifest by its name.
|
|
172
243
|
|
|
173
244
|
Parameters
|
|
174
245
|
----------
|
|
175
|
-
|
|
176
|
-
The
|
|
246
|
+
manifest : dict[str, Any]
|
|
247
|
+
The community apps manifest.
|
|
177
248
|
app : str
|
|
178
249
|
The name of the community app to find.
|
|
179
250
|
|
|
180
251
|
Returns
|
|
181
252
|
-------
|
|
182
|
-
|
|
183
|
-
The community app if found, otherwise None.
|
|
253
|
+
dict[str, Any] | None
|
|
254
|
+
The community app dictionary if found, otherwise None.
|
|
184
255
|
"""
|
|
185
256
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return comm_app
|
|
257
|
+
for manifest_app in manifest.get("apps", []):
|
|
258
|
+
if manifest_app.get("name", "") == app:
|
|
259
|
+
return manifest_app
|
|
190
260
|
|
|
191
261
|
# We don't use error() here to allow printing something before exiting.
|
|
192
262
|
rich.print(f"[red]Error:[/red] Community app [magenta]{app}[/magenta] was not found. Here are the available apps:")
|
|
193
|
-
|
|
263
|
+
apps_table(manifest)
|
|
194
264
|
|
|
195
265
|
raise typer.Exit(code=1)
|
nextmv/cloud/__init__.py
CHANGED
|
@@ -30,9 +30,6 @@ from .batch_experiment import BatchExperimentRun as BatchExperimentRun
|
|
|
30
30
|
from .batch_experiment import ExperimentStatus as ExperimentStatus
|
|
31
31
|
from .client import Client as Client
|
|
32
32
|
from .client import get_size as get_size
|
|
33
|
-
from .community import CommunityApp as CommunityApp
|
|
34
|
-
from .community import clone_community_app as clone_community_app
|
|
35
|
-
from .community import list_community_apps as list_community_apps
|
|
36
33
|
from .ensemble import EnsembleDefinition as EnsembleDefinition
|
|
37
34
|
from .ensemble import EvaluationRule as EvaluationRule
|
|
38
35
|
from .ensemble import RuleObjective as RuleObjective
|
|
@@ -58,7 +55,6 @@ from .secrets import SecretType as SecretType
|
|
|
58
55
|
from .shadow import ShadowTest as ShadowTest
|
|
59
56
|
from .shadow import ShadowTestMetadata as ShadowTestMetadata
|
|
60
57
|
from .shadow import StartEvents as StartEvents
|
|
61
|
-
from .shadow import StopIntent as StopIntent
|
|
62
58
|
from .shadow import TerminationEvents as TerminationEvents
|
|
63
59
|
from .shadow import TestComparison as TestComparison
|
|
64
60
|
from .switchback import SwitchbackPlan as SwitchbackPlan
|
|
@@ -21,7 +21,7 @@ list_application
|
|
|
21
21
|
import json
|
|
22
22
|
import shutil
|
|
23
23
|
import sys
|
|
24
|
-
from datetime import datetime
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
25
|
from enum import Enum
|
|
26
26
|
from typing import Any
|
|
27
27
|
|
|
@@ -46,7 +46,9 @@ from nextmv.cloud.application._switchback import ApplicationSwitchbackMixin
|
|
|
46
46
|
from nextmv.cloud.application._utils import _is_not_exist_error
|
|
47
47
|
from nextmv.cloud.application._version import ApplicationVersionMixin
|
|
48
48
|
from nextmv.cloud.client import Client
|
|
49
|
+
from nextmv.cloud.instance import Instance
|
|
49
50
|
from nextmv.cloud.url import UploadURL
|
|
51
|
+
from nextmv.cloud.version import Version
|
|
50
52
|
from nextmv.logger import log
|
|
51
53
|
from nextmv.manifest import Manifest
|
|
52
54
|
from nextmv.model import Model, ModelConfiguration
|
|
@@ -405,6 +407,14 @@ class Application(
|
|
|
405
407
|
model: Model | None = None,
|
|
406
408
|
model_configuration: ModelConfiguration | None = None,
|
|
407
409
|
rich_print: bool = False,
|
|
410
|
+
auto_create: bool = False,
|
|
411
|
+
version_id: str | None = None,
|
|
412
|
+
version_name: str | None = None,
|
|
413
|
+
version_description: str | None = None,
|
|
414
|
+
instance_id: str | None = None,
|
|
415
|
+
instance_name: str | None = None,
|
|
416
|
+
instance_description: str | None = None,
|
|
417
|
+
update_default_instance: bool = False,
|
|
408
418
|
) -> None:
|
|
409
419
|
"""
|
|
410
420
|
Push an app to Nextmv Cloud.
|
|
@@ -422,6 +432,17 @@ class Application(
|
|
|
422
432
|
`nextmv.Model`. The model is encoded, some dependencies and
|
|
423
433
|
accompanying files are packaged, and the app is pushed to Nextmv Cloud.
|
|
424
434
|
|
|
435
|
+
By default, this function only pushes the app. If you want to
|
|
436
|
+
automatically create a new version and instance after pushing, set
|
|
437
|
+
`auto_create=True`. The `version_id`, `version_name`, and
|
|
438
|
+
`version_description` arguments allow you to customize the new version
|
|
439
|
+
that is created. If not specified, defaults will be generated (random
|
|
440
|
+
ID, timestamped name/description). Similarly, `instance_id`,
|
|
441
|
+
`instance_name`, and `instance_description` can be used to customize
|
|
442
|
+
the new instance. If `update_default_instance` is True, the
|
|
443
|
+
application's default instance will be updated to the newly created
|
|
444
|
+
instance.
|
|
445
|
+
|
|
425
446
|
Parameters
|
|
426
447
|
----------
|
|
427
448
|
manifest : Optional[Manifest], default=None
|
|
@@ -440,6 +461,30 @@ class Application(
|
|
|
440
461
|
with `model`.
|
|
441
462
|
rich_print : bool, default=False
|
|
442
463
|
Whether to use rich printing when verbose output is enabled.
|
|
464
|
+
auto_create : bool, default=False
|
|
465
|
+
If True, automatically create a new version and instance after
|
|
466
|
+
pushing the app.
|
|
467
|
+
version_id : Optional[str], default=None
|
|
468
|
+
ID of the version to create after pushing the app. If None, a unique
|
|
469
|
+
ID will be generated.
|
|
470
|
+
version_name : Optional[str], default=None
|
|
471
|
+
Name of the version to create after pushing the app. If None, a
|
|
472
|
+
name will be generated.
|
|
473
|
+
version_description : Optional[str], default=None
|
|
474
|
+
Description of the version to create after pushing the app. If None, a
|
|
475
|
+
generic description with a timestamp will be generated.
|
|
476
|
+
instance_id : Optional[str], default=None
|
|
477
|
+
ID of the instance to create after pushing the app. If None, a unique
|
|
478
|
+
ID will be generated.
|
|
479
|
+
instance_name : Optional[str], default=None
|
|
480
|
+
Name of the instance to create after pushing the app. If None, a
|
|
481
|
+
name will be generated.
|
|
482
|
+
instance_description : Optional[str], default=None
|
|
483
|
+
Description of the instance to create after pushing the app. If None,
|
|
484
|
+
a generic description with a timestamp will be generated.
|
|
485
|
+
update_default_instance : bool, default=False
|
|
486
|
+
If True, update the application's default instance to the newly
|
|
487
|
+
created instance.
|
|
443
488
|
|
|
444
489
|
Raises
|
|
445
490
|
------
|
|
@@ -543,6 +588,44 @@ class Application(
|
|
|
543
588
|
except OSError as e:
|
|
544
589
|
raise Exception(f"error deleting output directory: {e}") from e
|
|
545
590
|
|
|
591
|
+
if not auto_create:
|
|
592
|
+
return
|
|
593
|
+
|
|
594
|
+
if verbose:
|
|
595
|
+
if rich_print:
|
|
596
|
+
rich.print(
|
|
597
|
+
f":hourglass_flowing_sand: Push completed for Nextmv application [magenta]{self.id}[/magenta], "
|
|
598
|
+
"creating a new version and instance...",
|
|
599
|
+
file=sys.stderr,
|
|
600
|
+
)
|
|
601
|
+
else:
|
|
602
|
+
log("⌛️ Push completed for the Nextmv application, creating a new version and instance...")
|
|
603
|
+
|
|
604
|
+
now = datetime.now(timezone.utc)
|
|
605
|
+
version = self.__version_on_push(
|
|
606
|
+
now=now,
|
|
607
|
+
version_id=version_id,
|
|
608
|
+
version_name=version_name,
|
|
609
|
+
version_description=version_description,
|
|
610
|
+
verbose=verbose,
|
|
611
|
+
rich_print=rich_print,
|
|
612
|
+
)
|
|
613
|
+
instance = self.__instance_on_push(
|
|
614
|
+
now=now,
|
|
615
|
+
version_id=version.id,
|
|
616
|
+
instance_id=instance_id,
|
|
617
|
+
instance_name=instance_name,
|
|
618
|
+
instance_description=instance_description,
|
|
619
|
+
verbose=verbose,
|
|
620
|
+
rich_print=rich_print,
|
|
621
|
+
)
|
|
622
|
+
self.__update_on_push(
|
|
623
|
+
instance=instance,
|
|
624
|
+
update_default_instance=update_default_instance,
|
|
625
|
+
verbose=verbose,
|
|
626
|
+
rich_print=rich_print,
|
|
627
|
+
)
|
|
628
|
+
|
|
546
629
|
def update(
|
|
547
630
|
self,
|
|
548
631
|
name: str | None = None,
|
|
@@ -784,6 +867,8 @@ class Application(
|
|
|
784
867
|
output_config = multi_config["output_configuration"] = {}
|
|
785
868
|
if content.multi_file.output.statistics:
|
|
786
869
|
output_config["statistics_path"] = content.multi_file.output.statistics
|
|
870
|
+
if content.multi_file.output.metrics:
|
|
871
|
+
output_config["metrics_path"] = content.multi_file.output.metrics
|
|
787
872
|
if content.multi_file.output.assets:
|
|
788
873
|
output_config["assets_path"] = content.multi_file.output.assets
|
|
789
874
|
if content.multi_file.output.solutions:
|
|
@@ -858,6 +943,120 @@ class Application(
|
|
|
858
943
|
log(f'💥️ Successfully pushed to application: "{self.id}".')
|
|
859
944
|
log(json.dumps(data, indent=2))
|
|
860
945
|
|
|
946
|
+
def __version_on_push(
|
|
947
|
+
self,
|
|
948
|
+
now: datetime,
|
|
949
|
+
version_id: str | None = None,
|
|
950
|
+
version_name: str | None = None,
|
|
951
|
+
version_description: str | None = None,
|
|
952
|
+
verbose: bool = False,
|
|
953
|
+
rich_print: bool = False,
|
|
954
|
+
) -> Version:
|
|
955
|
+
if version_description is None or version_description == "":
|
|
956
|
+
version_description = f"Version created automatically from push at {now.strftime('%Y-%m-%dT%H:%M:%SZ')}"
|
|
957
|
+
|
|
958
|
+
version = self.new_version(
|
|
959
|
+
id=version_id,
|
|
960
|
+
name=version_name,
|
|
961
|
+
description=version_description,
|
|
962
|
+
)
|
|
963
|
+
version_dict = version.to_dict()
|
|
964
|
+
|
|
965
|
+
if not verbose:
|
|
966
|
+
return version
|
|
967
|
+
|
|
968
|
+
if rich_print:
|
|
969
|
+
rich.print(
|
|
970
|
+
f":white_check_mark: Automatically created new version [magenta]{version.id}[/magenta].",
|
|
971
|
+
file=sys.stderr,
|
|
972
|
+
)
|
|
973
|
+
rich.print_json(data=version_dict)
|
|
974
|
+
|
|
975
|
+
return version
|
|
976
|
+
|
|
977
|
+
log(f'✅ Automatically created new version "{version.id}".')
|
|
978
|
+
log(json.dumps(version_dict, indent=2))
|
|
979
|
+
|
|
980
|
+
return version
|
|
981
|
+
|
|
982
|
+
def __instance_on_push(
|
|
983
|
+
self,
|
|
984
|
+
now: datetime,
|
|
985
|
+
version_id: str,
|
|
986
|
+
instance_id: str | None = None,
|
|
987
|
+
instance_name: str | None = None,
|
|
988
|
+
instance_description: str | None = None,
|
|
989
|
+
verbose: bool = False,
|
|
990
|
+
rich_print: bool = False,
|
|
991
|
+
) -> Instance:
|
|
992
|
+
if instance_description is None or instance_description == "":
|
|
993
|
+
instance_description = f"Instance created automatically from push at {now.strftime('%Y-%m-%dT%H:%M:%SZ')}"
|
|
994
|
+
|
|
995
|
+
instance = self.new_instance(
|
|
996
|
+
version_id=version_id,
|
|
997
|
+
id=instance_id,
|
|
998
|
+
name=instance_name,
|
|
999
|
+
description=instance_description,
|
|
1000
|
+
)
|
|
1001
|
+
instance_dict = instance.to_dict()
|
|
1002
|
+
|
|
1003
|
+
if not verbose:
|
|
1004
|
+
return instance
|
|
1005
|
+
|
|
1006
|
+
if rich_print:
|
|
1007
|
+
rich.print(
|
|
1008
|
+
f":white_check_mark: Automatically created new instance [magenta]{instance.id}[/magenta] "
|
|
1009
|
+
f"using version [magenta]{version_id}[/magenta].",
|
|
1010
|
+
file=sys.stderr,
|
|
1011
|
+
)
|
|
1012
|
+
rich.print_json(data=instance_dict)
|
|
1013
|
+
|
|
1014
|
+
return instance
|
|
1015
|
+
|
|
1016
|
+
log(f'✅ Automatically created new instance "{instance.id}" using version "{version_id}".')
|
|
1017
|
+
log(json.dumps(instance_dict, indent=2))
|
|
1018
|
+
|
|
1019
|
+
return instance
|
|
1020
|
+
|
|
1021
|
+
def __update_on_push(
|
|
1022
|
+
self,
|
|
1023
|
+
instance: Instance,
|
|
1024
|
+
update_default_instance: bool = False,
|
|
1025
|
+
verbose: bool = False,
|
|
1026
|
+
rich_print: bool = False,
|
|
1027
|
+
) -> None:
|
|
1028
|
+
if not update_default_instance:
|
|
1029
|
+
return
|
|
1030
|
+
|
|
1031
|
+
if verbose:
|
|
1032
|
+
if rich_print:
|
|
1033
|
+
rich.print(
|
|
1034
|
+
f":hourglass_flowing_sand: Updating default instance for app [magenta]{self.id}[/magenta]...",
|
|
1035
|
+
file=sys.stderr,
|
|
1036
|
+
)
|
|
1037
|
+
else:
|
|
1038
|
+
log("⌛️ Updating default instance for the Nextmv application...")
|
|
1039
|
+
|
|
1040
|
+
updated_app = self.update(default_instance_id=instance.id)
|
|
1041
|
+
if not verbose:
|
|
1042
|
+
return
|
|
1043
|
+
|
|
1044
|
+
if rich_print:
|
|
1045
|
+
rich.print(
|
|
1046
|
+
f":white_check_mark: Updated default instance to "
|
|
1047
|
+
f"[magenta]{updated_app.default_instance_id}[/magenta] for application "
|
|
1048
|
+
f"[magenta]{self.id}[/magenta].",
|
|
1049
|
+
file=sys.stderr,
|
|
1050
|
+
)
|
|
1051
|
+
rich.print_json(data=updated_app.to_dict())
|
|
1052
|
+
|
|
1053
|
+
return
|
|
1054
|
+
|
|
1055
|
+
log(
|
|
1056
|
+
f'✅ Updated default instance to "{updated_app.default_instance_id}" for application "{self.id}".',
|
|
1057
|
+
)
|
|
1058
|
+
log(json.dumps(updated_app.to_dict(), indent=2))
|
|
1059
|
+
|
|
861
1060
|
|
|
862
1061
|
def list_applications(client: Client) -> list[Application]:
|
|
863
1062
|
"""
|
|
@@ -6,7 +6,6 @@ from datetime import datetime
|
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
from nextmv.cloud.input_set import InputSet, ManagedInput
|
|
9
|
-
from nextmv.safe import safe_id
|
|
10
9
|
|
|
11
10
|
if TYPE_CHECKING:
|
|
12
11
|
from . import Application
|
|
@@ -17,32 +16,6 @@ class ApplicationInputSetMixin:
|
|
|
17
16
|
Mixin class for managing app input sets within an application.
|
|
18
17
|
"""
|
|
19
18
|
|
|
20
|
-
def delete_input_set(self: "Application", input_set_id: str) -> None:
|
|
21
|
-
"""
|
|
22
|
-
Delete an input set.
|
|
23
|
-
|
|
24
|
-
Deletes an input set along with all the associated information.
|
|
25
|
-
|
|
26
|
-
Parameters
|
|
27
|
-
----------
|
|
28
|
-
input_set_id : str
|
|
29
|
-
ID of the input set to delete.
|
|
30
|
-
|
|
31
|
-
Raises
|
|
32
|
-
------
|
|
33
|
-
requests.HTTPError
|
|
34
|
-
If the response status code is not 2xx.
|
|
35
|
-
|
|
36
|
-
Examples
|
|
37
|
-
--------
|
|
38
|
-
>>> app.delete_input_set("input-set-123")
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
_ = self.client.request(
|
|
42
|
-
method="DELETE",
|
|
43
|
-
endpoint=f"{self.experiments_endpoint}/inputsets/{input_set_id}",
|
|
44
|
-
)
|
|
45
|
-
|
|
46
19
|
def input_set(self: "Application", input_set_id: str) -> InputSet:
|
|
47
20
|
"""
|
|
48
21
|
Get an input set.
|
|
@@ -108,8 +81,8 @@ class ApplicationInputSetMixin:
|
|
|
108
81
|
|
|
109
82
|
def new_input_set(
|
|
110
83
|
self: "Application",
|
|
111
|
-
id: str
|
|
112
|
-
name: str
|
|
84
|
+
id: str,
|
|
85
|
+
name: str,
|
|
113
86
|
description: str | None = None,
|
|
114
87
|
end_time: datetime | None = None,
|
|
115
88
|
instance_id: str | None = None,
|
|
@@ -134,11 +107,10 @@ class ApplicationInputSetMixin:
|
|
|
134
107
|
|
|
135
108
|
Parameters
|
|
136
109
|
----------
|
|
137
|
-
id: str
|
|
138
|
-
ID of the input set
|
|
139
|
-
name: str
|
|
140
|
-
Name of the input set.
|
|
141
|
-
the name.
|
|
110
|
+
id: str
|
|
111
|
+
ID of the input set
|
|
112
|
+
name: str
|
|
113
|
+
Name of the input set.
|
|
142
114
|
description: Optional[str]
|
|
143
115
|
Optional description of the input set.
|
|
144
116
|
end_time: Optional[datetime]
|
|
@@ -173,14 +145,6 @@ class ApplicationInputSetMixin:
|
|
|
173
145
|
If the response status code is not 2xx.
|
|
174
146
|
"""
|
|
175
147
|
|
|
176
|
-
# Generate ID if not provided
|
|
177
|
-
if id is None or id == "":
|
|
178
|
-
id = safe_id("input-set")
|
|
179
|
-
|
|
180
|
-
# Use ID as name if name not provided
|
|
181
|
-
if name is None or name == "":
|
|
182
|
-
name = id
|
|
183
|
-
|
|
184
148
|
payload = {
|
|
185
149
|
"id": id,
|
|
186
150
|
"name": name,
|
nextmv/cloud/application/_run.py
CHANGED
|
@@ -21,7 +21,14 @@ from nextmv.cloud.url import DownloadURL
|
|
|
21
21
|
from nextmv.input import Input, InputFormat
|
|
22
22
|
from nextmv.logger import log
|
|
23
23
|
from nextmv.options import Options
|
|
24
|
-
from nextmv.output import
|
|
24
|
+
from nextmv.output import (
|
|
25
|
+
ASSETS_KEY,
|
|
26
|
+
STATISTICS_KEY,
|
|
27
|
+
Asset,
|
|
28
|
+
Output,
|
|
29
|
+
OutputFormat,
|
|
30
|
+
Statistics,
|
|
31
|
+
)
|
|
25
32
|
from nextmv.polling import DEFAULT_POLLING_OPTIONS, PollingOptions, poll
|
|
26
33
|
from nextmv.run import (
|
|
27
34
|
ExternalRunResult,
|
|
@@ -4,7 +4,7 @@ Application mixin for managing shadow tests.
|
|
|
4
4
|
|
|
5
5
|
from typing import TYPE_CHECKING
|
|
6
6
|
|
|
7
|
-
from nextmv.cloud.shadow import ShadowTest, ShadowTestMetadata, StartEvents,
|
|
7
|
+
from nextmv.cloud.shadow import ShadowTest, ShadowTestMetadata, StartEvents, TerminationEvents
|
|
8
8
|
from nextmv.run import Run
|
|
9
9
|
from nextmv.safe import safe_id
|
|
10
10
|
|
|
@@ -248,7 +248,7 @@ class ApplicationShadowMixin:
|
|
|
248
248
|
endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/start",
|
|
249
249
|
)
|
|
250
250
|
|
|
251
|
-
def stop_shadow_test(self: "Application", shadow_test_id: str
|
|
251
|
+
def stop_shadow_test(self: "Application", shadow_test_id: str) -> None:
|
|
252
252
|
"""
|
|
253
253
|
Stop a shadow test. The test should already have started before using
|
|
254
254
|
this method.
|
|
@@ -257,22 +257,16 @@ class ApplicationShadowMixin:
|
|
|
257
257
|
----------
|
|
258
258
|
shadow_test_id : str
|
|
259
259
|
ID of the shadow test to stop.
|
|
260
|
-
|
|
261
|
-
Intent for stopping the shadow test.
|
|
260
|
+
|
|
262
261
|
Raises
|
|
263
262
|
------
|
|
264
263
|
requests.HTTPError
|
|
265
264
|
If the response status code is not 2xx.
|
|
266
265
|
"""
|
|
267
266
|
|
|
268
|
-
payload = {
|
|
269
|
-
"intent": intent.value,
|
|
270
|
-
}
|
|
271
|
-
|
|
272
267
|
_ = self.client.request(
|
|
273
268
|
method="PUT",
|
|
274
269
|
endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/stop",
|
|
275
|
-
payload=payload,
|
|
276
270
|
)
|
|
277
271
|
|
|
278
272
|
def update_shadow_test(
|