nextmv 1.0.0.dev3__py3-none-any.whl → 1.0.0.dev5__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 +0 -4
- nextmv/_serialization.py +1 -1
- nextmv/cli/CONTRIBUTING.md +81 -29
- nextmv/cli/cloud/acceptance/create.py +24 -26
- nextmv/cli/cloud/acceptance/delete.py +7 -8
- nextmv/cli/cloud/acceptance/get.py +9 -10
- nextmv/cli/cloud/acceptance/list.py +3 -3
- nextmv/cli/cloud/acceptance/update.py +6 -6
- nextmv/cli/cloud/account/__init__.py +3 -3
- nextmv/cli/cloud/account/create.py +11 -11
- nextmv/cli/cloud/account/delete.py +6 -7
- nextmv/cli/cloud/account/get.py +3 -3
- nextmv/cli/cloud/account/update.py +5 -5
- nextmv/cli/cloud/app/create.py +25 -26
- nextmv/cli/cloud/app/delete.py +5 -6
- nextmv/cli/cloud/app/exists.py +2 -2
- nextmv/cli/cloud/app/get.py +2 -2
- nextmv/cli/cloud/app/list.py +3 -3
- nextmv/cli/cloud/app/push.py +368 -54
- nextmv/cli/cloud/app/update.py +12 -12
- nextmv/cli/cloud/batch/create.py +26 -28
- nextmv/cli/cloud/batch/delete.py +5 -6
- nextmv/cli/cloud/batch/get.py +8 -8
- nextmv/cli/cloud/batch/list.py +3 -3
- nextmv/cli/cloud/batch/metadata.py +4 -4
- nextmv/cli/cloud/batch/update.py +6 -6
- nextmv/cli/cloud/data/__init__.py +1 -1
- nextmv/cli/cloud/data/upload.py +15 -15
- nextmv/cli/cloud/ensemble/__init__.py +2 -0
- nextmv/cli/cloud/ensemble/create.py +21 -22
- nextmv/cli/cloud/ensemble/delete.py +5 -6
- nextmv/cli/cloud/ensemble/get.py +4 -4
- nextmv/cli/cloud/ensemble/list.py +63 -0
- nextmv/cli/cloud/ensemble/update.py +9 -9
- nextmv/cli/cloud/input_set/create.py +20 -22
- nextmv/cli/cloud/input_set/get.py +3 -3
- nextmv/cli/cloud/input_set/list.py +3 -3
- nextmv/cli/cloud/input_set/update.py +24 -24
- nextmv/cli/cloud/instance/create.py +14 -15
- nextmv/cli/cloud/instance/delete.py +5 -6
- nextmv/cli/cloud/instance/exists.py +2 -2
- nextmv/cli/cloud/instance/get.py +2 -2
- nextmv/cli/cloud/instance/list.py +3 -3
- nextmv/cli/cloud/instance/update.py +14 -14
- nextmv/cli/cloud/managed_input/create.py +14 -16
- nextmv/cli/cloud/managed_input/delete.py +6 -7
- nextmv/cli/cloud/managed_input/get.py +3 -3
- nextmv/cli/cloud/managed_input/list.py +3 -3
- nextmv/cli/cloud/managed_input/update.py +9 -9
- nextmv/cli/cloud/run/cancel.py +2 -2
- nextmv/cli/cloud/run/create.py +32 -33
- nextmv/cli/cloud/run/get.py +8 -8
- nextmv/cli/cloud/run/input.py +4 -4
- nextmv/cli/cloud/run/list.py +6 -6
- nextmv/cli/cloud/run/logs.py +9 -10
- nextmv/cli/cloud/run/metadata.py +4 -4
- nextmv/cli/cloud/run/track.py +32 -33
- nextmv/cli/cloud/scenario/create.py +21 -21
- nextmv/cli/cloud/scenario/delete.py +5 -6
- nextmv/cli/cloud/scenario/get.py +8 -8
- nextmv/cli/cloud/scenario/list.py +3 -3
- nextmv/cli/cloud/scenario/metadata.py +4 -4
- nextmv/cli/cloud/scenario/update.py +6 -6
- nextmv/cli/cloud/secrets/create.py +17 -17
- nextmv/cli/cloud/secrets/delete.py +5 -6
- nextmv/cli/cloud/secrets/get.py +4 -4
- nextmv/cli/cloud/secrets/list.py +3 -3
- nextmv/cli/cloud/secrets/update.py +17 -20
- nextmv/cli/cloud/shadow/create.py +31 -31
- nextmv/cli/cloud/shadow/delete.py +5 -6
- nextmv/cli/cloud/shadow/get.py +2 -2
- nextmv/cli/cloud/shadow/list.py +3 -3
- nextmv/cli/cloud/shadow/metadata.py +4 -4
- nextmv/cli/cloud/shadow/start.py +3 -3
- nextmv/cli/cloud/shadow/stop.py +4 -6
- nextmv/cli/cloud/shadow/update.py +6 -6
- nextmv/cli/cloud/switchback/create.py +19 -15
- nextmv/cli/cloud/switchback/delete.py +5 -6
- nextmv/cli/cloud/switchback/get.py +3 -3
- nextmv/cli/cloud/switchback/list.py +3 -3
- nextmv/cli/cloud/switchback/metadata.py +6 -6
- nextmv/cli/cloud/switchback/start.py +4 -4
- nextmv/cli/cloud/switchback/stop.py +4 -6
- nextmv/cli/cloud/switchback/update.py +6 -6
- nextmv/cli/cloud/upload/create.py +2 -2
- nextmv/cli/cloud/version/create.py +9 -10
- nextmv/cli/cloud/version/delete.py +5 -6
- nextmv/cli/cloud/version/exists.py +2 -2
- nextmv/cli/cloud/version/get.py +2 -2
- nextmv/cli/cloud/version/list.py +3 -3
- nextmv/cli/cloud/version/update.py +8 -8
- nextmv/cli/community/clone.py +12 -10
- nextmv/cli/community/list.py +9 -9
- nextmv/cli/configuration/config.py +43 -10
- nextmv/cli/configuration/create.py +3 -3
- nextmv/cli/configuration/delete.py +7 -7
- nextmv/cli/configuration/list.py +3 -3
- nextmv/cli/confirm.py +34 -0
- nextmv/cli/main.py +27 -36
- nextmv/cli/message.py +2 -2
- nextmv/cli/version.py +1 -1
- nextmv/cloud/__init__.py +0 -38
- nextmv/cloud/acceptance_test.py +1 -65
- nextmv/cloud/account.py +1 -6
- nextmv/cloud/application/__init__.py +192 -54
- nextmv/cloud/application/_batch_scenario.py +4 -19
- nextmv/cloud/application/_instance.py +3 -3
- nextmv/cloud/application/_managed_input.py +2 -2
- nextmv/cloud/application/_run.py +8 -1
- nextmv/cloud/application/_shadow.py +2 -2
- nextmv/cloud/application/_switchback.py +12 -4
- nextmv/cloud/application/_version.py +4 -3
- nextmv/cloud/client.py +1 -1
- nextmv/cloud/shadow.py +43 -4
- nextmv/cloud/switchback.py +46 -9
- nextmv/default_app/main.py +4 -6
- nextmv/deprecated.py +5 -3
- nextmv/input.py +0 -52
- nextmv/local/executor.py +83 -3
- nextmv/local/geojson_handler.py +1 -1
- nextmv/local/runner.py +1 -1
- nextmv/manifest.py +11 -7
- nextmv/model.py +2 -2
- nextmv/options.py +10 -255
- nextmv/output.py +57 -83
- nextmv/run.py +13 -13
- nextmv/status.py +1 -51
- {nextmv-1.0.0.dev3.dist-info → nextmv-1.0.0.dev5.dist-info}/METADATA +1 -1
- nextmv-1.0.0.dev5.dist-info/RECORD +183 -0
- nextmv-1.0.0.dev3.dist-info/RECORD +0 -181
- {nextmv-1.0.0.dev3.dist-info → nextmv-1.0.0.dev5.dist-info}/WHEEL +0 -0
- {nextmv-1.0.0.dev3.dist-info → nextmv-1.0.0.dev5.dist-info}/entry_points.txt +0 -0
- {nextmv-1.0.0.dev3.dist-info → nextmv-1.0.0.dev5.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
@@ -250,7 +252,7 @@ class Application(
|
|
|
250
252
|
def new(
|
|
251
253
|
cls,
|
|
252
254
|
client: Client,
|
|
253
|
-
name: str,
|
|
255
|
+
name: str | None = None,
|
|
254
256
|
id: str | None = None,
|
|
255
257
|
description: str | None = None,
|
|
256
258
|
is_workflow: bool | None = None,
|
|
@@ -268,13 +270,13 @@ class Application(
|
|
|
268
270
|
----------
|
|
269
271
|
client : Client
|
|
270
272
|
Client to use for interacting with the Nextmv Cloud API.
|
|
271
|
-
name : str
|
|
272
|
-
Name of the application.
|
|
273
|
-
id : str
|
|
273
|
+
name : str | None = None
|
|
274
|
+
Name of the application. Uses the ID as the name if not provided.
|
|
275
|
+
id : str | None = None
|
|
274
276
|
ID of the application. Will be generated if not provided.
|
|
275
|
-
description : str
|
|
277
|
+
description : str | None = None
|
|
276
278
|
Description of the application.
|
|
277
|
-
is_workflow : bool
|
|
279
|
+
is_workflow : bool | None = None
|
|
278
280
|
Whether the application is a Decision Workflow.
|
|
279
281
|
exist_ok : bool, default=False
|
|
280
282
|
If True and an application with the same ID already exists,
|
|
@@ -296,7 +298,10 @@ class Application(
|
|
|
296
298
|
>>> app = Application.new(client=client, name="My New App", id="my-app")
|
|
297
299
|
"""
|
|
298
300
|
|
|
299
|
-
if id is None:
|
|
301
|
+
if exist_ok and (id is None or id == ""):
|
|
302
|
+
raise ValueError("If exist_ok is True, id must be provided")
|
|
303
|
+
|
|
304
|
+
if id is None or id == "":
|
|
300
305
|
id = safe_id("app")
|
|
301
306
|
|
|
302
307
|
if exist_ok and cls.exists(client=client, id=id):
|
|
@@ -307,6 +312,9 @@ class Application(
|
|
|
307
312
|
|
|
308
313
|
return cls.from_dict({"client": client} | response.json())
|
|
309
314
|
|
|
315
|
+
if name is None or name == "":
|
|
316
|
+
name = id
|
|
317
|
+
|
|
310
318
|
payload = {
|
|
311
319
|
"name": name,
|
|
312
320
|
"id": id,
|
|
@@ -399,10 +407,14 @@ class Application(
|
|
|
399
407
|
model: Model | None = None,
|
|
400
408
|
model_configuration: ModelConfiguration | None = None,
|
|
401
409
|
rich_print: bool = False,
|
|
402
|
-
|
|
410
|
+
auto_create: bool = False,
|
|
403
411
|
version_id: str | None = None,
|
|
404
412
|
version_name: str | None = None,
|
|
405
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,
|
|
406
418
|
) -> None:
|
|
407
419
|
"""
|
|
408
420
|
Push an app to Nextmv Cloud.
|
|
@@ -420,14 +432,16 @@ class Application(
|
|
|
420
432
|
`nextmv.Model`. The model is encoded, some dependencies and
|
|
421
433
|
accompanying files are packaged, and the app is pushed to Nextmv Cloud.
|
|
422
434
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
that is created. If
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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.
|
|
431
445
|
|
|
432
446
|
Parameters
|
|
433
447
|
----------
|
|
@@ -447,21 +461,30 @@ class Application(
|
|
|
447
461
|
with `model`.
|
|
448
462
|
rich_print : bool, default=False
|
|
449
463
|
Whether to use rich printing when verbose output is enabled.
|
|
450
|
-
|
|
451
|
-
If True,
|
|
464
|
+
auto_create : bool, default=False
|
|
465
|
+
If True, automatically create a new version and instance after
|
|
466
|
+
pushing the app.
|
|
452
467
|
version_id : Optional[str], default=None
|
|
453
468
|
ID of the version to create after pushing the app. If None, a unique
|
|
454
469
|
ID will be generated.
|
|
455
470
|
version_name : Optional[str], default=None
|
|
456
|
-
Name of the version to create after pushing the app. If None, a
|
|
457
|
-
|
|
471
|
+
Name of the version to create after pushing the app. If None, a
|
|
472
|
+
name will be generated.
|
|
458
473
|
version_description : Optional[str], default=None
|
|
459
474
|
Description of the version to create after pushing the app. If None, a
|
|
460
475
|
generic description with a timestamp will be generated.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
None
|
|
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.
|
|
465
488
|
|
|
466
489
|
Raises
|
|
467
490
|
------
|
|
@@ -565,44 +588,43 @@ class Application(
|
|
|
565
588
|
except OSError as e:
|
|
566
589
|
raise Exception(f"error deleting output directory: {e}") from e
|
|
567
590
|
|
|
568
|
-
if
|
|
569
|
-
if verbose:
|
|
570
|
-
if rich_print:
|
|
571
|
-
rich.print(
|
|
572
|
-
f":white_check_mark: Push completed for Nextmv application [magenta]{self.id}[/magenta] "
|
|
573
|
-
"without creating a new version.",
|
|
574
|
-
file=sys.stderr,
|
|
575
|
-
)
|
|
576
|
-
else:
|
|
577
|
-
log("✅ Push completed without creating a new version for Nextmv application.")
|
|
578
|
-
|
|
591
|
+
if not auto_create:
|
|
579
592
|
return
|
|
580
593
|
|
|
581
|
-
now = datetime.now(timezone.utc)
|
|
582
|
-
if version_id is None:
|
|
583
|
-
version_id = safe_id(prefix="version") + f"-{now.strftime('%Y%m%d-%H%M%S')}"
|
|
584
|
-
if version_name is None:
|
|
585
|
-
version_name = f"Version {version_id}"
|
|
586
|
-
if version_description is None:
|
|
587
|
-
version_description = f"Version created automatically from push at {now.strftime('%Y-%m-%dT%H:%M:%SZ')}"
|
|
588
|
-
|
|
589
|
-
version = self.new_version(
|
|
590
|
-
id=version_id,
|
|
591
|
-
name=version_name,
|
|
592
|
-
description=version_description,
|
|
593
|
-
)
|
|
594
|
-
version_dict = version.to_dict()
|
|
595
|
-
|
|
596
594
|
if verbose:
|
|
597
595
|
if rich_print:
|
|
598
596
|
rich.print(
|
|
599
|
-
f":
|
|
597
|
+
f":hourglass_flowing_sand: Push completed for Nextmv application [magenta]{self.id}[/magenta], "
|
|
598
|
+
"creating a new version and instance...",
|
|
600
599
|
file=sys.stderr,
|
|
601
600
|
)
|
|
602
|
-
rich.print_json(data=version_dict)
|
|
603
601
|
else:
|
|
604
|
-
log(
|
|
605
|
-
|
|
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
|
+
)
|
|
606
628
|
|
|
607
629
|
def update(
|
|
608
630
|
self,
|
|
@@ -845,6 +867,8 @@ class Application(
|
|
|
845
867
|
output_config = multi_config["output_configuration"] = {}
|
|
846
868
|
if content.multi_file.output.statistics:
|
|
847
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
|
|
848
872
|
if content.multi_file.output.assets:
|
|
849
873
|
output_config["assets_path"] = content.multi_file.output.assets
|
|
850
874
|
if content.multi_file.output.solutions:
|
|
@@ -919,6 +943,120 @@ class Application(
|
|
|
919
943
|
log(f'💥️ Successfully pushed to application: "{self.id}".')
|
|
920
944
|
log(json.dumps(data, indent=2))
|
|
921
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
|
+
|
|
922
1060
|
|
|
923
1061
|
def list_applications(client: Client) -> list[Application]:
|
|
924
1062
|
"""
|
|
@@ -10,7 +10,6 @@ from nextmv.cloud.batch_experiment import (
|
|
|
10
10
|
BatchExperimentMetadata,
|
|
11
11
|
BatchExperimentRun,
|
|
12
12
|
ExperimentStatus,
|
|
13
|
-
to_runs,
|
|
14
13
|
)
|
|
15
14
|
from nextmv.cloud.input_set import InputSet, ManagedInput
|
|
16
15
|
from nextmv.cloud.scenario import Scenario, ScenarioInputType, _option_sets, _scenarios_by_id
|
|
@@ -258,7 +257,6 @@ class ApplicationBatchMixin:
|
|
|
258
257
|
self: "Application",
|
|
259
258
|
name: str | None = None,
|
|
260
259
|
input_set_id: str | None = None,
|
|
261
|
-
instance_ids: list[str] | None = None,
|
|
262
260
|
description: str | None = None,
|
|
263
261
|
id: str | None = None,
|
|
264
262
|
option_sets: dict[str, dict[str, str]] | None = None,
|
|
@@ -274,9 +272,6 @@ class ApplicationBatchMixin:
|
|
|
274
272
|
Name of the batch experiment. If not provided, the ID will be used as the name.
|
|
275
273
|
input_set_id: str | None
|
|
276
274
|
ID of the input set to use for the batch experiment.
|
|
277
|
-
instance_ids: list[str]
|
|
278
|
-
This argument is deprecated, use `runs` instead.
|
|
279
|
-
List of instance IDs to use for the batch experiment.
|
|
280
275
|
description: Optional[str]
|
|
281
276
|
Optional description of the batch experiment.
|
|
282
277
|
id: Optional[str]
|
|
@@ -304,11 +299,11 @@ class ApplicationBatchMixin:
|
|
|
304
299
|
"""
|
|
305
300
|
|
|
306
301
|
# Generate ID if not provided
|
|
307
|
-
if id is None:
|
|
302
|
+
if id is None or id == "":
|
|
308
303
|
id = safe_id("batch")
|
|
309
304
|
|
|
310
305
|
# Use ID as name if name not provided
|
|
311
|
-
if name is None:
|
|
306
|
+
if name is None or name == "":
|
|
312
307
|
name = id
|
|
313
308
|
|
|
314
309
|
payload = {
|
|
@@ -317,11 +312,6 @@ class ApplicationBatchMixin:
|
|
|
317
312
|
}
|
|
318
313
|
if input_set_id is not None:
|
|
319
314
|
payload["input_set_id"] = input_set_id
|
|
320
|
-
if instance_ids is not None:
|
|
321
|
-
input_set = self.input_set(input_set_id)
|
|
322
|
-
runs = to_runs(instance_ids, input_set)
|
|
323
|
-
payload_runs = [run.to_dict() for run in runs]
|
|
324
|
-
payload["runs"] = payload_runs
|
|
325
315
|
if description is not None:
|
|
326
316
|
payload["description"] = description
|
|
327
317
|
if option_sets is not None:
|
|
@@ -346,7 +336,6 @@ class ApplicationBatchMixin:
|
|
|
346
336
|
self: "Application",
|
|
347
337
|
name: str | None = None,
|
|
348
338
|
input_set_id: str | None = None,
|
|
349
|
-
instance_ids: list[str] | None = None,
|
|
350
339
|
description: str | None = None,
|
|
351
340
|
id: str | None = None,
|
|
352
341
|
option_sets: dict[str, dict[str, str]] | None = None,
|
|
@@ -368,9 +357,6 @@ class ApplicationBatchMixin:
|
|
|
368
357
|
Name of the batch experiment. If not provided, the ID will be used as the name.
|
|
369
358
|
input_set_id: str
|
|
370
359
|
ID of the input set to use for the batch experiment.
|
|
371
|
-
instance_ids: list[str]
|
|
372
|
-
List of instance IDs to use for the batch experiment. This argument
|
|
373
|
-
is deprecated, use `runs` instead.
|
|
374
360
|
description: Optional[str]
|
|
375
361
|
Optional description of the batch experiment.
|
|
376
362
|
id: Optional[str]
|
|
@@ -402,7 +388,6 @@ class ApplicationBatchMixin:
|
|
|
402
388
|
batch_id = self.new_batch_experiment(
|
|
403
389
|
name=name,
|
|
404
390
|
input_set_id=input_set_id,
|
|
405
|
-
instance_ids=instance_ids,
|
|
406
391
|
description=description,
|
|
407
392
|
id=id,
|
|
408
393
|
option_sets=option_sets,
|
|
@@ -481,11 +466,11 @@ class ApplicationBatchMixin:
|
|
|
481
466
|
raise ValueError("At least one scenario must be provided")
|
|
482
467
|
|
|
483
468
|
# Generate ID if not provided
|
|
484
|
-
if id is None:
|
|
469
|
+
if id is None or id == "":
|
|
485
470
|
id = safe_id("scenario")
|
|
486
471
|
|
|
487
472
|
# Use ID as name if name not provided
|
|
488
|
-
if name is None:
|
|
473
|
+
if name is None or name == "":
|
|
489
474
|
name = id
|
|
490
475
|
|
|
491
476
|
scenarios_by_id = _scenarios_by_id(scenarios)
|
|
@@ -194,15 +194,15 @@ class ApplicationInstanceMixin:
|
|
|
194
194
|
'Production Instance'
|
|
195
195
|
"""
|
|
196
196
|
|
|
197
|
-
if exist_ok and id is None:
|
|
197
|
+
if exist_ok and (id is None or id == ""):
|
|
198
198
|
raise ValueError("If exist_ok is True, id must be provided")
|
|
199
199
|
|
|
200
200
|
if exist_ok and self.instance_exists(instance_id=id):
|
|
201
201
|
return self.instance(instance_id=id)
|
|
202
202
|
|
|
203
|
-
if id is None:
|
|
203
|
+
if id is None or id == "":
|
|
204
204
|
id = safe_id(prefix="instance")
|
|
205
|
-
if name is None:
|
|
205
|
+
if name is None or name == "":
|
|
206
206
|
name = id
|
|
207
207
|
|
|
208
208
|
payload = {
|
|
@@ -147,9 +147,9 @@ class ApplicationManagedInputMixin:
|
|
|
147
147
|
if upload_id is None and run_id is None:
|
|
148
148
|
raise ValueError("Either upload_id or run_id must be specified")
|
|
149
149
|
|
|
150
|
-
if id is None:
|
|
150
|
+
if id is None or id == "":
|
|
151
151
|
id = safe_id(prefix="managed-input")
|
|
152
|
-
if name is None:
|
|
152
|
+
if name is None or name == "":
|
|
153
153
|
name = id
|
|
154
154
|
|
|
155
155
|
payload = {
|
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,
|
|
@@ -204,11 +204,11 @@ class ApplicationShadowMixin:
|
|
|
204
204
|
shadow_test_id = safe_id("shadow")
|
|
205
205
|
|
|
206
206
|
# Use ID as name if name not provided
|
|
207
|
-
if name is None:
|
|
207
|
+
if name is None or name == "":
|
|
208
208
|
name = shadow_test_id
|
|
209
209
|
|
|
210
210
|
payload = {
|
|
211
|
-
"id":
|
|
211
|
+
"id": shadow_test_id,
|
|
212
212
|
"name": name,
|
|
213
213
|
"comparisons": comparisons,
|
|
214
214
|
"termination_events": termination_events.to_dict(),
|
|
@@ -172,9 +172,11 @@ class ApplicationSwitchbackMixin:
|
|
|
172
172
|
comparison : TestComparisonSingle
|
|
173
173
|
Comparison defining the baseline and candidate instances.
|
|
174
174
|
unit_duration_minutes : float
|
|
175
|
-
Duration of each interval in minutes.
|
|
175
|
+
Duration of each interval in minutes. The value must be between 1
|
|
176
|
+
and 10080.
|
|
176
177
|
units : int
|
|
177
|
-
Total number of intervals in the switchback test.
|
|
178
|
+
Total number of intervals in the switchback test. The value must be
|
|
179
|
+
between 1 and 1000.
|
|
178
180
|
switchback_test_id : Optional[str], default=None
|
|
179
181
|
Optional ID for the switchback test. Will be generated if not
|
|
180
182
|
provided.
|
|
@@ -197,16 +199,22 @@ class ApplicationSwitchbackMixin:
|
|
|
197
199
|
If the response status code is not 2xx.
|
|
198
200
|
"""
|
|
199
201
|
|
|
202
|
+
if unit_duration_minutes < 1 or unit_duration_minutes > 10080:
|
|
203
|
+
raise ValueError("unit_duration_minutes must be between 1 and 10080")
|
|
204
|
+
|
|
205
|
+
if units < 1 or units > 1000:
|
|
206
|
+
raise ValueError("units must be between 1 and 1000")
|
|
207
|
+
|
|
200
208
|
# Generate ID if not provided
|
|
201
209
|
if switchback_test_id is None:
|
|
202
210
|
switchback_test_id = safe_id("switchback")
|
|
203
211
|
|
|
204
212
|
# Use ID as name if name not provided
|
|
205
|
-
if name is None:
|
|
213
|
+
if name is None or name == "":
|
|
206
214
|
name = switchback_test_id
|
|
207
215
|
|
|
208
216
|
payload = {
|
|
209
|
-
"id":
|
|
217
|
+
"id": switchback_test_id,
|
|
210
218
|
"name": name,
|
|
211
219
|
"comparison": comparison,
|
|
212
220
|
"generate_random_plan": {
|
|
@@ -132,15 +132,16 @@ class ApplicationVersionMixin:
|
|
|
132
132
|
... )
|
|
133
133
|
"""
|
|
134
134
|
|
|
135
|
-
if exist_ok and id is None:
|
|
135
|
+
if exist_ok and (id is None or id == ""):
|
|
136
136
|
raise ValueError("If exist_ok is True, id must be provided")
|
|
137
137
|
|
|
138
138
|
if exist_ok and self.version_exists(version_id=id):
|
|
139
139
|
return self.version(version_id=id)
|
|
140
140
|
|
|
141
|
-
if id is None:
|
|
141
|
+
if id is None or id == "":
|
|
142
142
|
id = safe_id(prefix="version")
|
|
143
|
-
|
|
143
|
+
|
|
144
|
+
if name is None or name == "":
|
|
144
145
|
name = id
|
|
145
146
|
|
|
146
147
|
payload = {
|
nextmv/cloud/client.py
CHANGED
|
@@ -303,7 +303,7 @@ class Client:
|
|
|
303
303
|
if data is not None:
|
|
304
304
|
kwargs["data"] = data
|
|
305
305
|
if payload is not None:
|
|
306
|
-
if isinstance(payload,
|
|
306
|
+
if isinstance(payload, dict | list):
|
|
307
307
|
data = deflated_serialize_json(payload, json_configurations=json_configurations)
|
|
308
308
|
kwargs["data"] = data
|
|
309
309
|
else:
|
nextmv/cloud/shadow.py
CHANGED
|
@@ -90,7 +90,7 @@ class TerminationEvents(BaseModel):
|
|
|
90
90
|
|
|
91
91
|
maximum_runs: int
|
|
92
92
|
"""
|
|
93
|
-
Maximum number of runs for the test.
|
|
93
|
+
Maximum number of runs for the test. Value must be between 1 and 300.
|
|
94
94
|
"""
|
|
95
95
|
time: datetime | None = None
|
|
96
96
|
"""
|
|
@@ -99,8 +99,8 @@ class TerminationEvents(BaseModel):
|
|
|
99
99
|
"""
|
|
100
100
|
|
|
101
101
|
def model_post_init(self, __context):
|
|
102
|
-
if self.maximum_runs < 1:
|
|
103
|
-
raise ValueError("maximum_runs must be
|
|
102
|
+
if self.maximum_runs < 1 or self.maximum_runs > 300:
|
|
103
|
+
raise ValueError("maximum_runs must be between 1 and 300")
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
class ShadowTestMetadata(BaseModel):
|
|
@@ -151,7 +151,10 @@ class ShadowTestMetadata(BaseModel):
|
|
|
151
151
|
"""The current status of the shadow test."""
|
|
152
152
|
|
|
153
153
|
|
|
154
|
-
class
|
|
154
|
+
# This class uses some fields defined in ShadowTestMetadata. We are not
|
|
155
|
+
# using inheritance to help the user understand the full structure when using
|
|
156
|
+
# tools like intellisense.
|
|
157
|
+
class ShadowTest(BaseModel):
|
|
155
158
|
"""
|
|
156
159
|
A Nextmv Cloud shadow test definition.
|
|
157
160
|
|
|
@@ -166,6 +169,20 @@ class ShadowTest(ShadowTestMetadata):
|
|
|
166
169
|
|
|
167
170
|
Parameters
|
|
168
171
|
----------
|
|
172
|
+
shadow_test_id : str, optional
|
|
173
|
+
The unique identifier of the shadow test.
|
|
174
|
+
name : str, optional
|
|
175
|
+
Name of the shadow test.
|
|
176
|
+
description : str, optional
|
|
177
|
+
Description of the shadow test.
|
|
178
|
+
app_id : str, optional
|
|
179
|
+
ID of the application to which the shadow test belongs.
|
|
180
|
+
created_at : datetime, optional
|
|
181
|
+
Creation date of the shadow test.
|
|
182
|
+
updated_at : datetime, optional
|
|
183
|
+
Last update date of the shadow test.
|
|
184
|
+
status : ExperimentStatus, optional
|
|
185
|
+
The current status of the shadow test.
|
|
169
186
|
completed_at : datetime, optional
|
|
170
187
|
Completion date of the shadow test, if applicable.
|
|
171
188
|
comparisons : list[TestComparison], optional
|
|
@@ -174,8 +191,30 @@ class ShadowTest(ShadowTestMetadata):
|
|
|
174
191
|
Start events for the shadow test.
|
|
175
192
|
termination_events : TerminationEvents, optional
|
|
176
193
|
Termination events for the shadow test.
|
|
194
|
+
grouped_distributional_summaries : list[dict[str, Any]], optional
|
|
195
|
+
Grouped distributional summaries of the shadow test.
|
|
196
|
+
runs : list[Run], optional
|
|
197
|
+
List of runs in the shadow test.
|
|
177
198
|
"""
|
|
178
199
|
|
|
200
|
+
shadow_test_id: str | None = Field(
|
|
201
|
+
serialization_alias="id",
|
|
202
|
+
validation_alias=AliasChoices("id", "shadow_test_id"),
|
|
203
|
+
default=None,
|
|
204
|
+
)
|
|
205
|
+
"""The unique identifier of the shadow test."""
|
|
206
|
+
name: str | None = None
|
|
207
|
+
"""Name of the shadow test."""
|
|
208
|
+
description: str | None = None
|
|
209
|
+
"""Description of the shadow test."""
|
|
210
|
+
app_id: str | None = None
|
|
211
|
+
"""ID of the application to which the shadow test belongs."""
|
|
212
|
+
created_at: datetime | None = None
|
|
213
|
+
"""Creation date of the shadow test."""
|
|
214
|
+
updated_at: datetime | None = None
|
|
215
|
+
"""Last update date of the shadow test."""
|
|
216
|
+
status: ExperimentStatus | None = None
|
|
217
|
+
"""The current status of the shadow test."""
|
|
179
218
|
completed_at: datetime | None = None
|
|
180
219
|
"""Completion date of the shadow test, if applicable."""
|
|
181
220
|
comparisons: list[TestComparison] | None = None
|