nextmv 1.0.0.dev8__py3-none-any.whl → 1.0.0.dev10__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.
Files changed (54) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/_serialization.py +1 -1
  3. nextmv/cli/CONTRIBUTING.md +31 -11
  4. nextmv/cli/cloud/acceptance/create.py +12 -12
  5. nextmv/cli/cloud/acceptance/delete.py +1 -4
  6. nextmv/cli/cloud/account/delete.py +1 -1
  7. nextmv/cli/cloud/app/delete.py +1 -1
  8. nextmv/cli/cloud/app/push.py +23 -42
  9. nextmv/cli/cloud/batch/delete.py +1 -4
  10. nextmv/cli/cloud/ensemble/delete.py +1 -4
  11. nextmv/cli/cloud/input_set/__init__.py +2 -0
  12. nextmv/cli/cloud/input_set/delete.py +64 -0
  13. nextmv/cli/cloud/instance/delete.py +1 -1
  14. nextmv/cli/cloud/managed_input/delete.py +1 -1
  15. nextmv/cli/cloud/run/create.py +4 -9
  16. nextmv/cli/cloud/scenario/delete.py +1 -4
  17. nextmv/cli/cloud/secrets/delete.py +1 -4
  18. nextmv/cli/cloud/shadow/delete.py +1 -4
  19. nextmv/cli/cloud/shadow/stop.py +14 -2
  20. nextmv/cli/cloud/switchback/delete.py +1 -4
  21. nextmv/cli/cloud/switchback/stop.py +14 -2
  22. nextmv/cli/cloud/version/delete.py +1 -1
  23. nextmv/cli/community/clone.py +11 -197
  24. nextmv/cli/community/list.py +51 -116
  25. nextmv/cli/configuration/create.py +4 -4
  26. nextmv/cli/configuration/delete.py +1 -1
  27. nextmv/cli/main.py +2 -3
  28. nextmv/cli/message.py +71 -54
  29. nextmv/cloud/__init__.py +4 -0
  30. nextmv/cloud/application/__init__.py +1 -200
  31. nextmv/cloud/application/_acceptance.py +13 -8
  32. nextmv/cloud/application/_input_set.py +42 -6
  33. nextmv/cloud/application/_run.py +1 -8
  34. nextmv/cloud/application/_shadow.py +9 -3
  35. nextmv/cloud/application/_switchback.py +11 -2
  36. nextmv/cloud/batch_experiment.py +3 -1
  37. nextmv/cloud/client.py +1 -1
  38. nextmv/cloud/community.py +446 -0
  39. nextmv/cloud/integration.py +7 -4
  40. nextmv/cloud/shadow.py +25 -0
  41. nextmv/cloud/switchback.py +2 -0
  42. nextmv/default_app/main.py +6 -4
  43. nextmv/local/executor.py +3 -83
  44. nextmv/local/geojson_handler.py +1 -1
  45. nextmv/manifest.py +7 -11
  46. nextmv/model.py +52 -13
  47. nextmv/options.py +1 -1
  48. nextmv/output.py +21 -57
  49. nextmv/run.py +3 -12
  50. {nextmv-1.0.0.dev8.dist-info → nextmv-1.0.0.dev10.dist-info}/METADATA +5 -4
  51. {nextmv-1.0.0.dev8.dist-info → nextmv-1.0.0.dev10.dist-info}/RECORD +54 -52
  52. {nextmv-1.0.0.dev8.dist-info → nextmv-1.0.0.dev10.dist-info}/WHEEL +0 -0
  53. {nextmv-1.0.0.dev8.dist-info → nextmv-1.0.0.dev10.dist-info}/entry_points.txt +0 -0
  54. {nextmv-1.0.0.dev8.dist-info → nextmv-1.0.0.dev10.dist-info}/licenses/LICENSE +0 -0
nextmv/cli/message.py CHANGED
@@ -11,106 +11,107 @@ import rich
11
11
  import typer
12
12
 
13
13
 
14
- def error(msg: str) -> None:
14
+ def message(msg: str, emoji: str | None = None) -> None:
15
15
  """
16
- Pretty-print an error message and exit with code 1. Your message should end
17
- with a period.
16
+ Pretty-print a message. Your message should end with a period. The use of
17
+ emojis is encouraged to give context to the message. An emoji should be a
18
+ string as specified in:
19
+ https://rich.readthedocs.io/en/latest/markup.html#emoji.
18
20
 
19
21
  Parameters
20
22
  ----------
21
23
  msg : str
22
- The error message to display.
23
-
24
- Raises
25
- ------
26
- typer.Exit
27
- Exits the program with code 1.
24
+ The message to display.
25
+ emoji : str | None
26
+ An optional emoji to prefix the message. If None, no emoji is used. The
27
+ emoji should be a string as specified in:
28
+ https://rich.readthedocs.io/en/latest/markup.html#emoji. For example:
29
+ `:hourglass_flowing_sand:`.
28
30
  """
29
31
 
30
- msg = msg.rstrip("\n")
31
- if not msg.endswith("."):
32
- msg += "."
33
-
34
- rich.print(f":x: [red]Error:[/red] {msg}", file=sys.stderr)
32
+ msg = _format(msg)
33
+ if emoji:
34
+ rich.print(f"{emoji} {msg}", file=sys.stderr)
35
+ return
35
36
 
36
- raise typer.Exit(code=1)
37
+ rich.print(msg, file=sys.stderr)
37
38
 
38
39
 
39
- def success(msg: str) -> None:
40
+ def info(msg: str) -> None:
40
41
  """
41
- Pretty-print a success message. Your message should end with a period.
42
+ Pretty-print an informational message. Your message should end with a
43
+ period.
42
44
 
43
45
  Parameters
44
46
  ----------
45
47
  msg : str
46
- The success message to display.
48
+ The informational message to display.
47
49
  """
48
50
 
49
- msg = msg.rstrip("\n")
50
- if not msg.endswith("."):
51
- msg += "."
51
+ message(msg, emoji=":bulb:")
52
52
 
53
- rich.print(f":white_check_mark: {msg}", file=sys.stderr)
54
53
 
55
-
56
- def warning(msg: str) -> None:
54
+ def in_progress(msg: str) -> None:
57
55
  """
58
- Pretty-print a warning message. Your message should end with a period.
56
+ Pretty-print an in-progress message with an hourglass emoji. Your message
57
+ should end with a period.
59
58
 
60
59
  Parameters
61
60
  ----------
62
61
  msg : str
63
- The warning message to display.
62
+ The in-progress message to display.
64
63
  """
65
64
 
66
- msg = msg.rstrip("\n")
67
- if not msg.endswith("."):
68
- msg += "."
69
-
70
- rich.print(f":construction: [yellow] Warning:[/yellow] {msg}", file=sys.stderr)
65
+ message(msg, emoji=":hourglass_flowing_sand:")
71
66
 
72
67
 
73
- def info(msg: str, emoji: str | None = None) -> None:
68
+ def success(msg: str) -> None:
74
69
  """
75
- Pretty-print an informational message. Your message should end with a
76
- period. The use of emojis is encouraged to give context to the message. An
77
- emoji should be a string as specified in:
78
- https://rich.readthedocs.io/en/latest/markup.html#emoji.
70
+ Pretty-print a success message. Your message should end with a period.
79
71
 
80
72
  Parameters
81
73
  ----------
82
74
  msg : str
83
- The informational message to display.
84
- emoji : str | None
85
- An optional emoji to prefix the message. If None, no emoji is used. The
86
- emoji should be a string as specified in:
87
- https://rich.readthedocs.io/en/latest/markup.html#emoji. For example:
88
- `:hourglass_flowing_sand:`.
75
+ The success message to display.
89
76
  """
90
77
 
91
- msg = msg.rstrip("\n")
92
- if not msg.endswith("."):
93
- msg += "."
78
+ message(msg, emoji=":white_check_mark:")
94
79
 
95
- if emoji:
96
- rich.print(f"{emoji} {msg}", file=sys.stderr)
97
- return
98
80
 
99
- rich.print(msg, file=sys.stderr)
81
+ def warning(msg: str) -> None:
82
+ """
83
+ Pretty-print a warning message. Your message should end with a period.
100
84
 
85
+ Parameters
86
+ ----------
87
+ msg : str
88
+ The warning message to display.
89
+ """
101
90
 
102
- def in_progress(msg: str) -> None:
91
+ msg = _format(msg)
92
+ rich.print(f":construction: [yellow] Warning:[/yellow] {msg}", file=sys.stderr)
93
+
94
+
95
+ def error(msg: str) -> None:
103
96
  """
104
- Pretty-print an in-progress message with an hourglass emoji. Your message
105
- should end with a period.
97
+ Pretty-print an error message and exit with code 1. Your message should end
98
+ with a period.
106
99
 
107
100
  Parameters
108
101
  ----------
109
102
  msg : str
110
- The in-progress message to display.
103
+ The error message to display.
104
+
105
+ Raises
106
+ ------
107
+ typer.Exit
108
+ Exits the program with code 1.
111
109
  """
112
110
 
113
- info(msg, emoji=":hourglass_flowing_sand:")
111
+ msg = _format(msg)
112
+ rich.print(f":x: [red]Error:[/red] {msg}", file=sys.stderr)
113
+
114
+ raise typer.Exit(code=1)
114
115
 
115
116
 
116
117
  def print_json(data: dict[str, Any] | list[dict[str, Any]]) -> None:
@@ -151,3 +152,19 @@ def enum_values(enum_class: Enum) -> str:
151
152
  return " and ".join(values)
152
153
 
153
154
  return ", ".join(values[:-1]) + ", and " + values[-1]
155
+
156
+
157
+ def _format(msg: str) -> str:
158
+ """
159
+ Format a message to ensure it ends with a period.
160
+
161
+ Parameters
162
+ ----------
163
+ msg : str
164
+ The message to format.
165
+ """
166
+ msg = msg.rstrip("\n")
167
+ if not msg.endswith("."):
168
+ msg += "."
169
+
170
+ return msg
nextmv/cloud/__init__.py CHANGED
@@ -30,6 +30,9 @@ 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
33
36
  from .ensemble import EnsembleDefinition as EnsembleDefinition
34
37
  from .ensemble import EvaluationRule as EvaluationRule
35
38
  from .ensemble import RuleObjective as RuleObjective
@@ -55,6 +58,7 @@ from .secrets import SecretType as SecretType
55
58
  from .shadow import ShadowTest as ShadowTest
56
59
  from .shadow import ShadowTestMetadata as ShadowTestMetadata
57
60
  from .shadow import StartEvents as StartEvents
61
+ from .shadow import StopIntent as StopIntent
58
62
  from .shadow import TerminationEvents as TerminationEvents
59
63
  from .shadow import TestComparison as TestComparison
60
64
  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, timezone
24
+ from datetime import datetime
25
25
  from enum import Enum
26
26
  from typing import Any
27
27
 
@@ -46,9 +46,7 @@ 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
50
49
  from nextmv.cloud.url import UploadURL
51
- from nextmv.cloud.version import Version
52
50
  from nextmv.logger import log
53
51
  from nextmv.manifest import Manifest
54
52
  from nextmv.model import Model, ModelConfiguration
@@ -407,14 +405,6 @@ class Application(
407
405
  model: Model | None = None,
408
406
  model_configuration: ModelConfiguration | None = None,
409
407
  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,
418
408
  ) -> None:
419
409
  """
420
410
  Push an app to Nextmv Cloud.
@@ -432,17 +422,6 @@ class Application(
432
422
  `nextmv.Model`. The model is encoded, some dependencies and
433
423
  accompanying files are packaged, and the app is pushed to Nextmv Cloud.
434
424
 
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
-
446
425
  Parameters
447
426
  ----------
448
427
  manifest : Optional[Manifest], default=None
@@ -461,30 +440,6 @@ class Application(
461
440
  with `model`.
462
441
  rich_print : bool, default=False
463
442
  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.
488
443
 
489
444
  Raises
490
445
  ------
@@ -588,44 +543,6 @@ class Application(
588
543
  except OSError as e:
589
544
  raise Exception(f"error deleting output directory: {e}") from e
590
545
 
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
-
629
546
  def update(
630
547
  self,
631
548
  name: str | None = None,
@@ -867,8 +784,6 @@ class Application(
867
784
  output_config = multi_config["output_configuration"] = {}
868
785
  if content.multi_file.output.statistics:
869
786
  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
872
787
  if content.multi_file.output.assets:
873
788
  output_config["assets_path"] = content.multi_file.output.assets
874
789
  if content.multi_file.output.solutions:
@@ -943,120 +858,6 @@ class Application(
943
858
  log(f'💥️ Successfully pushed to application: "{self.id}".')
944
859
  log(json.dumps(data, indent=2))
945
860
 
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
-
1060
861
 
1061
862
  def list_applications(client: Client) -> list[Application]:
1062
863
  """
@@ -9,6 +9,7 @@ import requests
9
9
  from nextmv.cloud.acceptance_test import AcceptanceTest, Metric
10
10
  from nextmv.cloud.batch_experiment import BatchExperimentRun, ExperimentStatus
11
11
  from nextmv.polling import DEFAULT_POLLING_OPTIONS, PollingOptions, poll
12
+ from nextmv.safe import safe_id
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from . import Application
@@ -163,8 +164,8 @@ class ApplicationAcceptanceMixin:
163
164
  self: "Application",
164
165
  candidate_instance_id: str,
165
166
  baseline_instance_id: str,
166
- id: str,
167
167
  metrics: list[Metric | dict[str, Any]],
168
+ id: str | None = None,
168
169
  name: str | None = None,
169
170
  input_set_id: str | None = None,
170
171
  description: str | None = None,
@@ -185,8 +186,8 @@ class ApplicationAcceptanceMixin:
185
186
  ID of the candidate instance.
186
187
  baseline_instance_id : str
187
188
  ID of the baseline instance.
188
- id : str
189
- ID of the acceptance test.
189
+ id : str | None, default=None
190
+ ID of the acceptance test. Will be generated if not provided.
190
191
  metrics : list[Union[Metric, dict[str, Any]]]
191
192
  List of metrics to use for the acceptance test.
192
193
  name : Optional[str], default=None
@@ -210,6 +211,11 @@ class ApplicationAcceptanceMixin:
210
211
  If the batch experiment ID does not match the acceptance test ID.
211
212
  """
212
213
 
214
+ # Generate ID if not provided
215
+ if id is None or id == "":
216
+ id = safe_id("acceptance")
217
+
218
+ # Use ID as name if name not provided
213
219
  if name is None or name == "":
214
220
  name = id
215
221
 
@@ -265,11 +271,10 @@ class ApplicationAcceptanceMixin:
265
271
  "metrics": payload_metrics,
266
272
  "experiment_id": batch_experiment_id,
267
273
  "name": name,
274
+ "id": id,
268
275
  }
269
276
  if description is not None:
270
277
  payload["description"] = description
271
- if id is not None:
272
- payload["id"] = id
273
278
 
274
279
  response = self.client.request(
275
280
  method="POST",
@@ -283,8 +288,8 @@ class ApplicationAcceptanceMixin:
283
288
  self: "Application",
284
289
  candidate_instance_id: str,
285
290
  baseline_instance_id: str,
286
- id: str,
287
291
  metrics: list[Metric | dict[str, Any]],
292
+ id: str | None = None,
288
293
  name: str | None = None,
289
294
  input_set_id: str | None = None,
290
295
  description: str | None = None,
@@ -302,8 +307,8 @@ class ApplicationAcceptanceMixin:
302
307
  ID of the candidate instance.
303
308
  baseline_instance_id : str
304
309
  ID of the baseline instance.
305
- id : str
306
- ID of the acceptance test.
310
+ id : str | None, default=None
311
+ ID of the acceptance test. Will be generated if not provided.
307
312
  metrics : list[Union[Metric, dict[str, Any]]]
308
313
  List of metrics to use for the acceptance test.
309
314
  name : Optional[str], default=None
@@ -6,6 +6,7 @@ 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
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from . import Application
@@ -16,6 +17,32 @@ class ApplicationInputSetMixin:
16
17
  Mixin class for managing app input sets within an application.
17
18
  """
18
19
 
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
+
19
46
  def input_set(self: "Application", input_set_id: str) -> InputSet:
20
47
  """
21
48
  Get an input set.
@@ -81,8 +108,8 @@ class ApplicationInputSetMixin:
81
108
 
82
109
  def new_input_set(
83
110
  self: "Application",
84
- id: str,
85
- name: str,
111
+ id: str | None = None,
112
+ name: str | None = None,
86
113
  description: str | None = None,
87
114
  end_time: datetime | None = None,
88
115
  instance_id: str | None = None,
@@ -107,10 +134,11 @@ class ApplicationInputSetMixin:
107
134
 
108
135
  Parameters
109
136
  ----------
110
- id: str
111
- ID of the input set
112
- name: str
113
- Name of the input set.
137
+ id: str | None = None
138
+ ID of the input set, will be generated if not provided.
139
+ name: str | None = None
140
+ Name of the input set. If not provided, the ID will be used as
141
+ the name.
114
142
  description: Optional[str]
115
143
  Optional description of the input set.
116
144
  end_time: Optional[datetime]
@@ -145,6 +173,14 @@ class ApplicationInputSetMixin:
145
173
  If the response status code is not 2xx.
146
174
  """
147
175
 
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
+
148
184
  payload = {
149
185
  "id": id,
150
186
  "name": name,
@@ -21,14 +21,7 @@ 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 (
25
- ASSETS_KEY,
26
- STATISTICS_KEY,
27
- Asset,
28
- Output,
29
- OutputFormat,
30
- Statistics,
31
- )
24
+ from nextmv.output import ASSETS_KEY, STATISTICS_KEY, Asset, Output, OutputFormat, Statistics
32
25
  from nextmv.polling import DEFAULT_POLLING_OPTIONS, PollingOptions, poll
33
26
  from nextmv.run import (
34
27
  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, TerminationEvents
7
+ from nextmv.cloud.shadow import ShadowTest, ShadowTestMetadata, StartEvents, StopIntent, 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) -> None:
251
+ def stop_shadow_test(self: "Application", shadow_test_id: str, intent: StopIntent) -> None:
252
252
  """
253
253
  Stop a shadow test. The test should already have started before using
254
254
  this method.
@@ -257,16 +257,22 @@ class ApplicationShadowMixin:
257
257
  ----------
258
258
  shadow_test_id : str
259
259
  ID of the shadow test to stop.
260
-
260
+ intent : StopIntent
261
+ Intent for stopping the shadow test.
261
262
  Raises
262
263
  ------
263
264
  requests.HTTPError
264
265
  If the response status code is not 2xx.
265
266
  """
266
267
 
268
+ payload = {
269
+ "intent": intent.value,
270
+ }
271
+
267
272
  _ = self.client.request(
268
273
  method="PUT",
269
274
  endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/stop",
275
+ payload=payload,
270
276
  )
271
277
 
272
278
  def update_shadow_test(