nextmv 1.0.0.dev9__py3-none-any.whl → 1.1.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.
Files changed (59) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/cli/CONTRIBUTING.md +31 -11
  3. nextmv/cli/cloud/__init__.py +2 -0
  4. nextmv/cli/cloud/acceptance/delete.py +1 -4
  5. nextmv/cli/cloud/account/__init__.py +5 -3
  6. nextmv/cli/cloud/account/create.py +4 -1
  7. nextmv/cli/cloud/account/delete.py +1 -1
  8. nextmv/cli/cloud/app/delete.py +1 -1
  9. nextmv/cli/cloud/app/push.py +23 -42
  10. nextmv/cli/cloud/batch/delete.py +1 -4
  11. nextmv/cli/cloud/ensemble/delete.py +1 -4
  12. nextmv/cli/cloud/input_set/__init__.py +2 -0
  13. nextmv/cli/cloud/input_set/delete.py +64 -0
  14. nextmv/cli/cloud/instance/delete.py +1 -1
  15. nextmv/cli/cloud/managed_input/delete.py +1 -1
  16. nextmv/cli/cloud/run/create.py +4 -9
  17. nextmv/cli/cloud/scenario/delete.py +1 -4
  18. nextmv/cli/cloud/secrets/delete.py +1 -4
  19. nextmv/cli/cloud/shadow/delete.py +1 -4
  20. nextmv/cli/cloud/shadow/stop.py +14 -2
  21. nextmv/cli/cloud/sso/__init__.py +32 -0
  22. nextmv/cli/cloud/sso/create.py +97 -0
  23. nextmv/cli/cloud/sso/delete.py +58 -0
  24. nextmv/cli/cloud/sso/disable.py +56 -0
  25. nextmv/cli/cloud/sso/enable.py +56 -0
  26. nextmv/cli/cloud/sso/get.py +61 -0
  27. nextmv/cli/cloud/sso/update.py +78 -0
  28. nextmv/cli/cloud/switchback/delete.py +1 -4
  29. nextmv/cli/cloud/switchback/stop.py +14 -2
  30. nextmv/cli/cloud/version/delete.py +1 -1
  31. nextmv/cli/community/__init__.py +1 -1
  32. nextmv/cli/community/clone.py +11 -198
  33. nextmv/cli/community/list.py +51 -116
  34. nextmv/cli/configuration/config.py +27 -0
  35. nextmv/cli/configuration/create.py +4 -4
  36. nextmv/cli/configuration/delete.py +1 -1
  37. nextmv/cli/main.py +2 -3
  38. nextmv/cli/message.py +71 -54
  39. nextmv/cloud/__init__.py +5 -0
  40. nextmv/cloud/account.py +12 -10
  41. nextmv/cloud/application/__init__.py +12 -204
  42. nextmv/cloud/application/_acceptance.py +13 -8
  43. nextmv/cloud/application/_input_set.py +42 -6
  44. nextmv/cloud/application/_run.py +2 -2
  45. nextmv/cloud/application/_shadow.py +9 -3
  46. nextmv/cloud/application/_switchback.py +11 -2
  47. nextmv/cloud/batch_experiment.py +3 -1
  48. nextmv/cloud/community.py +446 -0
  49. nextmv/cloud/integration.py +7 -4
  50. nextmv/cloud/shadow.py +25 -0
  51. nextmv/cloud/sso.py +248 -0
  52. nextmv/cloud/switchback.py +2 -0
  53. nextmv/model.py +40 -4
  54. nextmv/options.py +1 -1
  55. {nextmv-1.0.0.dev9.dist-info → nextmv-1.1.0.dev0.dist-info}/METADATA +3 -1
  56. {nextmv-1.0.0.dev9.dist-info → nextmv-1.1.0.dev0.dist-info}/RECORD +59 -49
  57. {nextmv-1.0.0.dev9.dist-info → nextmv-1.1.0.dev0.dist-info}/WHEEL +0 -0
  58. {nextmv-1.0.0.dev9.dist-info → nextmv-1.1.0.dev0.dist-info}/entry_points.txt +0 -0
  59. {nextmv-1.0.0.dev9.dist-info → nextmv-1.1.0.dev0.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,8 +58,10 @@ 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
64
+ from .sso import SSOConfiguration as SSOConfiguration
60
65
  from .switchback import SwitchbackPlan as SwitchbackPlan
61
66
  from .switchback import SwitchbackPlanUnit as SwitchbackPlanUnit
62
67
  from .switchback import SwitchbackTest as SwitchbackTest
nextmv/cloud/account.py CHANGED
@@ -188,30 +188,32 @@ class Account(BaseModel):
188
188
 
189
189
  You can import the `Account` class directly from `cloud`:
190
190
 
191
- ```python
192
- from nextmv.cloud import Account
193
- ```
191
+ ```python from nextmv.cloud import Account ```
194
192
 
195
193
  This class provides access to account-level operations in the Nextmv Cloud,
196
194
  such as retrieving the queue of runs.
197
195
 
198
- Note: It is recommended to use `Account.get()` or `Account.new()`
199
- instead of direct initialization to ensure proper setup.
196
+ Note: It is recommended to use `Account.get()` or `Account.new()` instead
197
+ of direct initialization to ensure proper setup.
200
198
 
201
- Parameters
199
+ Attributes
202
200
  ----------
203
- client : Client
204
- Client to use for interacting with the Nextmv Cloud API.
205
201
  account_id : str, optional
206
202
  ID of the account (organization).
207
203
  name : str, optional
208
204
  Name of the account (organization).
209
205
  members : list[AccountMember], optional
210
206
  List of members in the account (organization).
207
+ client : Client
208
+ Client to use for interacting with the Nextmv Cloud API. This is an
209
+ SDK-specific attribute and it is not part of the API representation of
210
+ an account.
211
211
  account_endpoint : str, default="v1/account"
212
- Base endpoint for the account (SDK-specific).
212
+ Base endpoint for the account. This is an SDK-specific attribute and it
213
+ is not part of the API representation of an account.
213
214
  organization_endpoint : str, default="v1/organization/{organization_id}"
214
- Base endpoint for organization operations (SDK-specific).
215
+ Base endpoint for organization operations. This is an SDK-specific
216
+ attribute and it is not part of the API representation of an account.
215
217
 
216
218
  Examples
217
219
  --------
@@ -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
@@ -123,10 +121,8 @@ class Application(
123
121
  Note: It is recommended to use `Application.get()` or `Application.new()`
124
122
  instead of direct initialization to ensure proper setup.
125
123
 
126
- Parameters
124
+ Attributes
127
125
  ----------
128
- client : Client
129
- Client to use for interacting with the Nextmv Cloud API.
130
126
  id : str
131
127
  ID of the application.
132
128
  name : str, optional
@@ -147,12 +143,19 @@ class Application(
147
143
  Creation timestamp of the application.
148
144
  updated_at : datetime, optional
149
145
  Last update timestamp of the application.
146
+ client : Client
147
+ Client to use for interacting with the Nextmv Cloud API. This is an
148
+ SDK-specific attribute and it is not part of the API representation of
149
+ an application.
150
150
  endpoint : str, default="v1/applications/{id}"
151
- Base endpoint for the application (SDK-specific).
151
+ Base endpoint for the application. This is an SDK-specific attribute
152
+ and it is not part of the API representation of an application.
152
153
  experiments_endpoint : str, default="{base}/experiments"
153
- Base endpoint for experiments (SDK-specific).
154
+ Base endpoint for experiments. This is an SDK-specific attribute and it
155
+ is not part of the API representation of an application.
154
156
  ensembles_endpoint : str, default="{base}/ensembles"
155
- Base endpoint for ensembles (SDK-specific).
157
+ Base endpoint for ensembles. This is an SDK-specific attribute and it
158
+ is not part of the API representation of an application.
156
159
 
157
160
  Examples
158
161
  --------
@@ -407,14 +410,6 @@ class Application(
407
410
  model: Model | None = None,
408
411
  model_configuration: ModelConfiguration | None = None,
409
412
  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
413
  ) -> None:
419
414
  """
420
415
  Push an app to Nextmv Cloud.
@@ -432,17 +427,6 @@ class Application(
432
427
  `nextmv.Model`. The model is encoded, some dependencies and
433
428
  accompanying files are packaged, and the app is pushed to Nextmv Cloud.
434
429
 
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
430
  Parameters
447
431
  ----------
448
432
  manifest : Optional[Manifest], default=None
@@ -461,30 +445,6 @@ class Application(
461
445
  with `model`.
462
446
  rich_print : bool, default=False
463
447
  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
448
 
489
449
  Raises
490
450
  ------
@@ -588,44 +548,6 @@ class Application(
588
548
  except OSError as e:
589
549
  raise Exception(f"error deleting output directory: {e}") from e
590
550
 
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
551
  def update(
630
552
  self,
631
553
  name: str | None = None,
@@ -941,120 +863,6 @@ class Application(
941
863
  log(f'💥️ Successfully pushed to application: "{self.id}".')
942
864
  log(json.dumps(data, indent=2))
943
865
 
944
- def __version_on_push(
945
- self,
946
- now: datetime,
947
- version_id: str | None = None,
948
- version_name: str | None = None,
949
- version_description: str | None = None,
950
- verbose: bool = False,
951
- rich_print: bool = False,
952
- ) -> Version:
953
- if version_description is None or version_description == "":
954
- version_description = f"Version created automatically from push at {now.strftime('%Y-%m-%dT%H:%M:%SZ')}"
955
-
956
- version = self.new_version(
957
- id=version_id,
958
- name=version_name,
959
- description=version_description,
960
- )
961
- version_dict = version.to_dict()
962
-
963
- if not verbose:
964
- return version
965
-
966
- if rich_print:
967
- rich.print(
968
- f":white_check_mark: Automatically created new version [magenta]{version.id}[/magenta].",
969
- file=sys.stderr,
970
- )
971
- rich.print_json(data=version_dict)
972
-
973
- return version
974
-
975
- log(f'✅ Automatically created new version "{version.id}".')
976
- log(json.dumps(version_dict, indent=2))
977
-
978
- return version
979
-
980
- def __instance_on_push(
981
- self,
982
- now: datetime,
983
- version_id: str,
984
- instance_id: str | None = None,
985
- instance_name: str | None = None,
986
- instance_description: str | None = None,
987
- verbose: bool = False,
988
- rich_print: bool = False,
989
- ) -> Instance:
990
- if instance_description is None or instance_description == "":
991
- instance_description = f"Instance created automatically from push at {now.strftime('%Y-%m-%dT%H:%M:%SZ')}"
992
-
993
- instance = self.new_instance(
994
- version_id=version_id,
995
- id=instance_id,
996
- name=instance_name,
997
- description=instance_description,
998
- )
999
- instance_dict = instance.to_dict()
1000
-
1001
- if not verbose:
1002
- return instance
1003
-
1004
- if rich_print:
1005
- rich.print(
1006
- f":white_check_mark: Automatically created new instance [magenta]{instance.id}[/magenta] "
1007
- f"using version [magenta]{version_id}[/magenta].",
1008
- file=sys.stderr,
1009
- )
1010
- rich.print_json(data=instance_dict)
1011
-
1012
- return instance
1013
-
1014
- log(f'✅ Automatically created new instance "{instance.id}" using version "{version_id}".')
1015
- log(json.dumps(instance_dict, indent=2))
1016
-
1017
- return instance
1018
-
1019
- def __update_on_push(
1020
- self,
1021
- instance: Instance,
1022
- update_default_instance: bool = False,
1023
- verbose: bool = False,
1024
- rich_print: bool = False,
1025
- ) -> None:
1026
- if not update_default_instance:
1027
- return
1028
-
1029
- if verbose:
1030
- if rich_print:
1031
- rich.print(
1032
- f":hourglass_flowing_sand: Updating default instance for app [magenta]{self.id}[/magenta]...",
1033
- file=sys.stderr,
1034
- )
1035
- else:
1036
- log("⌛️ Updating default instance for the Nextmv application...")
1037
-
1038
- updated_app = self.update(default_instance_id=instance.id)
1039
- if not verbose:
1040
- return
1041
-
1042
- if rich_print:
1043
- rich.print(
1044
- f":white_check_mark: Updated default instance to "
1045
- f"[magenta]{updated_app.default_instance_id}[/magenta] for application "
1046
- f"[magenta]{self.id}[/magenta].",
1047
- file=sys.stderr,
1048
- )
1049
- rich.print_json(data=updated_app.to_dict())
1050
-
1051
- return
1052
-
1053
- log(
1054
- f'✅ Updated default instance to "{updated_app.default_instance_id}" for application "{self.id}".',
1055
- )
1056
- log(json.dumps(updated_app.to_dict(), indent=2))
1057
-
1058
866
 
1059
867
  def list_applications(client: Client) -> list[Application]:
1060
868
  """
@@ -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,