nextmv 0.40.0__py3-none-any.whl → 1.0.0__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 (163) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +1 -2
  3. nextmv/__init__.py +2 -4
  4. nextmv/cli/CONTRIBUTING.md +583 -0
  5. nextmv/cli/cloud/__init__.py +49 -0
  6. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  7. nextmv/cli/cloud/acceptance/create.py +391 -0
  8. nextmv/cli/cloud/acceptance/delete.py +64 -0
  9. nextmv/cli/cloud/acceptance/get.py +103 -0
  10. nextmv/cli/cloud/acceptance/list.py +62 -0
  11. nextmv/cli/cloud/acceptance/update.py +95 -0
  12. nextmv/cli/cloud/account/__init__.py +28 -0
  13. nextmv/cli/cloud/account/create.py +83 -0
  14. nextmv/cli/cloud/account/delete.py +59 -0
  15. nextmv/cli/cloud/account/get.py +66 -0
  16. nextmv/cli/cloud/account/update.py +70 -0
  17. nextmv/cli/cloud/app/__init__.py +35 -0
  18. nextmv/cli/cloud/app/create.py +140 -0
  19. nextmv/cli/cloud/app/delete.py +57 -0
  20. nextmv/cli/cloud/app/exists.py +44 -0
  21. nextmv/cli/cloud/app/get.py +66 -0
  22. nextmv/cli/cloud/app/list.py +61 -0
  23. nextmv/cli/cloud/app/push.py +432 -0
  24. nextmv/cli/cloud/app/update.py +124 -0
  25. nextmv/cli/cloud/batch/__init__.py +29 -0
  26. nextmv/cli/cloud/batch/create.py +452 -0
  27. nextmv/cli/cloud/batch/delete.py +64 -0
  28. nextmv/cli/cloud/batch/get.py +104 -0
  29. nextmv/cli/cloud/batch/list.py +63 -0
  30. nextmv/cli/cloud/batch/metadata.py +66 -0
  31. nextmv/cli/cloud/batch/update.py +95 -0
  32. nextmv/cli/cloud/data/__init__.py +26 -0
  33. nextmv/cli/cloud/data/upload.py +162 -0
  34. nextmv/cli/cloud/ensemble/__init__.py +33 -0
  35. nextmv/cli/cloud/ensemble/create.py +413 -0
  36. nextmv/cli/cloud/ensemble/delete.py +63 -0
  37. nextmv/cli/cloud/ensemble/get.py +65 -0
  38. nextmv/cli/cloud/ensemble/list.py +63 -0
  39. nextmv/cli/cloud/ensemble/update.py +103 -0
  40. nextmv/cli/cloud/input_set/__init__.py +32 -0
  41. nextmv/cli/cloud/input_set/create.py +168 -0
  42. nextmv/cli/cloud/input_set/delete.py +64 -0
  43. nextmv/cli/cloud/input_set/get.py +63 -0
  44. nextmv/cli/cloud/input_set/list.py +63 -0
  45. nextmv/cli/cloud/input_set/update.py +123 -0
  46. nextmv/cli/cloud/instance/__init__.py +35 -0
  47. nextmv/cli/cloud/instance/create.py +289 -0
  48. nextmv/cli/cloud/instance/delete.py +61 -0
  49. nextmv/cli/cloud/instance/exists.py +39 -0
  50. nextmv/cli/cloud/instance/get.py +62 -0
  51. nextmv/cli/cloud/instance/list.py +60 -0
  52. nextmv/cli/cloud/instance/update.py +216 -0
  53. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  54. nextmv/cli/cloud/managed_input/create.py +144 -0
  55. nextmv/cli/cloud/managed_input/delete.py +64 -0
  56. nextmv/cli/cloud/managed_input/get.py +63 -0
  57. nextmv/cli/cloud/managed_input/list.py +60 -0
  58. nextmv/cli/cloud/managed_input/update.py +97 -0
  59. nextmv/cli/cloud/run/__init__.py +37 -0
  60. nextmv/cli/cloud/run/cancel.py +37 -0
  61. nextmv/cli/cloud/run/create.py +524 -0
  62. nextmv/cli/cloud/run/get.py +199 -0
  63. nextmv/cli/cloud/run/input.py +86 -0
  64. nextmv/cli/cloud/run/list.py +80 -0
  65. nextmv/cli/cloud/run/logs.py +166 -0
  66. nextmv/cli/cloud/run/metadata.py +67 -0
  67. nextmv/cli/cloud/run/track.py +500 -0
  68. nextmv/cli/cloud/scenario/__init__.py +29 -0
  69. nextmv/cli/cloud/scenario/create.py +451 -0
  70. nextmv/cli/cloud/scenario/delete.py +61 -0
  71. nextmv/cli/cloud/scenario/get.py +102 -0
  72. nextmv/cli/cloud/scenario/list.py +63 -0
  73. nextmv/cli/cloud/scenario/metadata.py +67 -0
  74. nextmv/cli/cloud/scenario/update.py +93 -0
  75. nextmv/cli/cloud/secrets/__init__.py +33 -0
  76. nextmv/cli/cloud/secrets/create.py +206 -0
  77. nextmv/cli/cloud/secrets/delete.py +63 -0
  78. nextmv/cli/cloud/secrets/get.py +66 -0
  79. nextmv/cli/cloud/secrets/list.py +60 -0
  80. nextmv/cli/cloud/secrets/update.py +144 -0
  81. nextmv/cli/cloud/shadow/__init__.py +33 -0
  82. nextmv/cli/cloud/shadow/create.py +184 -0
  83. nextmv/cli/cloud/shadow/delete.py +64 -0
  84. nextmv/cli/cloud/shadow/get.py +61 -0
  85. nextmv/cli/cloud/shadow/list.py +63 -0
  86. nextmv/cli/cloud/shadow/metadata.py +66 -0
  87. nextmv/cli/cloud/shadow/start.py +43 -0
  88. nextmv/cli/cloud/shadow/stop.py +53 -0
  89. nextmv/cli/cloud/shadow/update.py +96 -0
  90. nextmv/cli/cloud/switchback/__init__.py +33 -0
  91. nextmv/cli/cloud/switchback/create.py +151 -0
  92. nextmv/cli/cloud/switchback/delete.py +64 -0
  93. nextmv/cli/cloud/switchback/get.py +62 -0
  94. nextmv/cli/cloud/switchback/list.py +63 -0
  95. nextmv/cli/cloud/switchback/metadata.py +68 -0
  96. nextmv/cli/cloud/switchback/start.py +43 -0
  97. nextmv/cli/cloud/switchback/stop.py +53 -0
  98. nextmv/cli/cloud/switchback/update.py +96 -0
  99. nextmv/cli/cloud/upload/__init__.py +22 -0
  100. nextmv/cli/cloud/upload/create.py +39 -0
  101. nextmv/cli/cloud/version/__init__.py +33 -0
  102. nextmv/cli/cloud/version/create.py +96 -0
  103. nextmv/cli/cloud/version/delete.py +61 -0
  104. nextmv/cli/cloud/version/exists.py +39 -0
  105. nextmv/cli/cloud/version/get.py +62 -0
  106. nextmv/cli/cloud/version/list.py +60 -0
  107. nextmv/cli/cloud/version/update.py +92 -0
  108. nextmv/cli/community/__init__.py +24 -0
  109. nextmv/cli/community/clone.py +20 -204
  110. nextmv/cli/community/list.py +61 -126
  111. nextmv/cli/configuration/__init__.py +23 -0
  112. nextmv/cli/configuration/config.py +103 -6
  113. nextmv/cli/configuration/create.py +17 -18
  114. nextmv/cli/configuration/delete.py +25 -13
  115. nextmv/cli/configuration/list.py +4 -4
  116. nextmv/cli/confirm.py +34 -0
  117. nextmv/cli/main.py +68 -36
  118. nextmv/cli/message.py +170 -0
  119. nextmv/cli/options.py +196 -0
  120. nextmv/cli/version.py +20 -1
  121. nextmv/cloud/__init__.py +17 -38
  122. nextmv/cloud/acceptance_test.py +20 -83
  123. nextmv/cloud/account.py +269 -30
  124. nextmv/cloud/application/__init__.py +898 -0
  125. nextmv/cloud/application/_acceptance.py +424 -0
  126. nextmv/cloud/application/_batch_scenario.py +845 -0
  127. nextmv/cloud/application/_ensemble.py +251 -0
  128. nextmv/cloud/application/_input_set.py +263 -0
  129. nextmv/cloud/application/_instance.py +289 -0
  130. nextmv/cloud/application/_managed_input.py +227 -0
  131. nextmv/cloud/application/_run.py +1393 -0
  132. nextmv/cloud/application/_secrets.py +294 -0
  133. nextmv/cloud/application/_shadow.py +320 -0
  134. nextmv/cloud/application/_switchback.py +332 -0
  135. nextmv/cloud/application/_utils.py +54 -0
  136. nextmv/cloud/application/_version.py +304 -0
  137. nextmv/cloud/batch_experiment.py +6 -2
  138. nextmv/cloud/community.py +446 -0
  139. nextmv/cloud/instance.py +11 -1
  140. nextmv/cloud/integration.py +8 -5
  141. nextmv/cloud/package.py +50 -9
  142. nextmv/cloud/shadow.py +254 -0
  143. nextmv/cloud/switchback.py +228 -0
  144. nextmv/deprecated.py +5 -3
  145. nextmv/input.py +20 -88
  146. nextmv/local/application.py +3 -15
  147. nextmv/local/runner.py +1 -1
  148. nextmv/model.py +50 -11
  149. nextmv/options.py +11 -256
  150. nextmv/output.py +0 -62
  151. nextmv/polling.py +54 -16
  152. nextmv/run.py +84 -37
  153. nextmv/status.py +1 -51
  154. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/METADATA +37 -11
  155. nextmv-1.0.0.dist-info/RECORD +185 -0
  156. nextmv-1.0.0.dist-info/entry_points.txt +2 -0
  157. nextmv/cli/community/community.py +0 -24
  158. nextmv/cli/configuration/configuration.py +0 -23
  159. nextmv/cli/error.py +0 -22
  160. nextmv/cloud/application.py +0 -4204
  161. nextmv-0.40.0.dist-info/RECORD +0 -66
  162. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/WHEEL +0 -0
  163. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,7 +7,10 @@ from typing import Any
7
7
 
8
8
  import yaml
9
9
 
10
- from nextmv.cli.error import error
10
+ from nextmv.cli.confirm import get_confirmation
11
+ from nextmv.cli.message import error, success, warning
12
+ from nextmv.cloud.account import Account
13
+ from nextmv.cloud.application import Application
11
14
  from nextmv.cloud.client import Client
12
15
 
13
16
  # Some useful constants.
@@ -57,6 +60,18 @@ def save_config(config: dict[str, Any]) -> None:
57
60
  yaml.safe_dump(config, file)
58
61
 
59
62
 
63
+ def non_profile_keys() -> set[str]:
64
+ """
65
+ Returns the set of keys that are not profile names in the configuration.
66
+
67
+ Returns
68
+ -------
69
+ set[str]
70
+ The set of non-profile keys.
71
+ """
72
+ return {API_KEY_KEY, ENDPOINT_KEY}
73
+
74
+
60
75
  def build_client(profile: str | None = None) -> Client:
61
76
  """
62
77
  Builds a `cloud.Client` using the API key and endpoint for the given
@@ -89,27 +104,109 @@ def build_client(profile: str | None = None) -> Client:
89
104
 
90
105
  if profile is not None:
91
106
  if profile not in config:
92
- error(f"Profile [bold magenta]{profile}[/bold magenta] does not exist.")
107
+ error(
108
+ f"Profile [magenta]{profile}[/magenta] does not exist. "
109
+ "Create it using [code]nextmv configuration create[/code] with the --profile option."
110
+ )
93
111
 
94
112
  api_key = config[profile].get(API_KEY_KEY)
95
113
  if api_key is None or api_key == "":
96
- error(f"API key for profile [bold magenta]{profile}[/bold magenta] is not set or is empty.")
114
+ error(
115
+ f"API key for profile [magenta]{profile}[/magenta] is not set or is empty. "
116
+ "Set it using [code]nextmv configuration create[/code] with the --profile and --api-key options."
117
+ )
97
118
 
98
119
  endpoint = config[profile].get(ENDPOINT_KEY)
99
120
  if endpoint is None or endpoint == "":
100
- error(f"Endpoint for profile [bold magenta]{profile}[/bold magenta] is not set or is empty.")
121
+ error(
122
+ f"Endpoint for profile [magenta]{profile}[/magenta] is not set or is empty. "
123
+ "Please run [code]nextmv configuration create[/code]."
124
+ )
101
125
  else:
102
126
  api_key = config.get(API_KEY_KEY)
103
127
  if api_key is None or api_key == "":
104
- error("Default API key is not set or is empty.")
128
+ error(
129
+ "Default API key is not set or is empty. "
130
+ "Please run [code]nextmv configuration create[/code] with the --api-key option."
131
+ )
105
132
 
106
133
  endpoint = config.get(ENDPOINT_KEY)
107
134
  if endpoint is None or endpoint == "":
108
- error("Default endpoint is not set or is empty.")
135
+ error("Default endpoint is not set or is empty. Please run [code]nextmv configuration create[/code].")
109
136
 
110
137
  return Client(api_key=api_key, url=f"https://{endpoint}")
111
138
 
112
139
 
140
+ def build_app(app_id: str, profile: str | None = None) -> Application:
141
+ """
142
+ Builds a `cloud.Application` using the given application ID and the API
143
+ key and endpoint for the given profile. If no profile is given, the default
144
+ profile is used. If the application does not exist, an exception is raised.
145
+
146
+ Parameters
147
+ ----------
148
+ app_id : str
149
+ The application ID.
150
+ profile : str | None
151
+ The profile name to use. If None, the default profile is used.
152
+
153
+ Returns
154
+ -------
155
+ Application
156
+ An application object for the given application ID.
157
+
158
+ Raises
159
+ ------
160
+ typer.Exit
161
+ If the application does not exist.
162
+ """
163
+ client = build_client(profile)
164
+ exists = Application.exists(client=client, id=app_id)
165
+ if exists:
166
+ return Application(client=client, id=app_id)
167
+
168
+ warning(f"Application with ID [magenta]{app_id}[/magenta] does not exist.")
169
+ should_create = get_confirmation(f"Do you want to create a new application with ID [magenta]{app_id}[/magenta]?")
170
+ if not should_create:
171
+ error(
172
+ f"Application with ID [magenta]{app_id}[/magenta] was not created and does not exist. "
173
+ "Use [code]nextmv cloud app create[/code] to create a new app."
174
+ )
175
+
176
+ app = Application.new(client=client, id=app_id, name=app_id)
177
+ success(f"Application with ID and name [magenta]{app_id}[/magenta] created successfully.")
178
+
179
+ return app
180
+
181
+
182
+ def build_account(account_id: str | None = None, profile: str | None = None) -> Account:
183
+ """
184
+ Builds a `cloud.Account` using the API key and endpoint for the given
185
+ profile. If no profile is given, the default profile is used.
186
+
187
+ Parameters
188
+ ----------
189
+ account_id : str | None
190
+ The account ID. If None, no account ID is set.
191
+ profile : str | None
192
+ The profile name to use. If None, the default profile is used.
193
+
194
+ Returns
195
+ -------
196
+ Account
197
+ An account object for the configured profile.
198
+
199
+ Raises
200
+ ------
201
+ typer.Exit
202
+ If the configuration is invalid or missing.
203
+ """
204
+
205
+ client = build_client(profile)
206
+
207
+ return Account(account_id=account_id, client=client)
208
+
209
+
113
210
  def obscure_api_key(api_key: str) -> str:
114
211
  """
115
212
  Obscure an API key for display purposes.
@@ -4,7 +4,6 @@ This module defines the configuration create command for the Nextmv CLI.
4
4
 
5
5
  from typing import Annotated
6
6
 
7
- import rich
8
7
  import typer
9
8
 
10
9
  from nextmv.cli.configuration.config import (
@@ -15,7 +14,7 @@ from nextmv.cli.configuration.config import (
15
14
  obscure_api_key,
16
15
  save_config,
17
16
  )
18
- from nextmv.cli.error import error
17
+ from nextmv.cli.message import error, message, success
19
18
 
20
19
  # Set up subcommand application.
21
20
  app = typer.Typer()
@@ -34,6 +33,14 @@ def create(
34
33
  metavar="NEXTMV_API_KEY",
35
34
  ),
36
35
  ],
36
+ endpoint: Annotated[ # Hidden because it is meant for internal use.
37
+ str | None,
38
+ typer.Option(
39
+ "--endpoint",
40
+ "-e",
41
+ hidden=True,
42
+ ),
43
+ ] = DEFAULT_ENDPOINT,
37
44
  profile: Annotated[ # Similar to nextmv.cli.options.ProfileOption but with different help text.
38
45
  str | None,
39
46
  typer.Option(
@@ -44,14 +51,6 @@ def create(
44
51
  metavar="PROFILE_NAME",
45
52
  ),
46
53
  ] = None,
47
- endpoint: Annotated[ # Hidden because it is meant for internal use.
48
- str | None,
49
- typer.Option(
50
- "--endpoint",
51
- "-e",
52
- hidden=True,
53
- ),
54
- ] = DEFAULT_ENDPOINT,
55
54
  ) -> None:
56
55
  """
57
56
  Create a new configuration or update an existing one.
@@ -59,14 +58,14 @@ def create(
59
58
  [bold][underline]Examples[/underline][/bold]
60
59
 
61
60
  - Default configuration.
62
- $ [green]nextmv configuration create --api-key NEXTMV_API_KEY[/green]
61
+ $ [dim]nextmv configuration create --api-key NEXTMV_API_KEY[/dim]
63
62
 
64
- - Configure a profile named [italic]hare[/italic].
65
- $ [green]nextmv configuration create --api-key NEXTMV_API_KEY --profile hare[/green]
63
+ - Configure a profile named [magenta]hare[/magenta].
64
+ $ [dim]nextmv configuration create --api-key NEXTMV_API_KEY --profile hare[/dim]
66
65
  """
67
66
 
68
67
  if profile is not None and profile.strip().lower() == "default":
69
- error("[code]default[/code] is a reserved profile name.")
68
+ error("[magenta]default[/magenta] is a reserved profile name.")
70
69
 
71
70
  endpoint = str(endpoint)
72
71
  if endpoint.startswith("https://"):
@@ -88,8 +87,8 @@ def create(
88
87
 
89
88
  save_config(config)
90
89
 
91
- rich.print(":white_check_mark: Configuration saved successfully.")
92
- rich.print(f"\t[bold]Profile[/bold]: {profile or 'Default'}")
93
- rich.print(f"\t[bold]API Key[/bold]: {obscure_api_key(api_key)}")
90
+ success("Configuration saved successfully.")
91
+ message(f"\t[bold]Profile[/bold]: [magenta]{profile or 'Default'}[/magenta]")
92
+ message(f"\t[bold]API Key[/bold]: [magenta]{obscure_api_key(api_key)}[/magenta]")
94
93
  if endpoint != DEFAULT_ENDPOINT:
95
- rich.print(f"\t[bold]Endpoint[/bold]: {endpoint}")
94
+ message(f"\t[bold]Endpoint[/bold]: [magenta]{endpoint}[/magenta]")
@@ -4,12 +4,11 @@ This module defines the configuration delete command for the Nextmv CLI.
4
4
 
5
5
  from typing import Annotated
6
6
 
7
- import rich
8
7
  import typer
9
- from rich.prompt import Confirm
10
8
 
11
9
  from nextmv.cli.configuration.config import load_config, save_config
12
- from nextmv.cli.error import error
10
+ from nextmv.cli.confirm import get_confirmation
11
+ from nextmv.cli.message import error, info, success
13
12
 
14
13
  # Set up subcommand application.
15
14
  app = typer.Typer()
@@ -27,29 +26,42 @@ def delete(
27
26
  metavar="PROFILE_NAME",
28
27
  ),
29
28
  ],
29
+ yes: Annotated[
30
+ bool,
31
+ typer.Option(
32
+ "--yes",
33
+ "-y",
34
+ help="Agree to deletion confirmation prompt. Useful for non-interactive sessions.",
35
+ ),
36
+ ] = False,
30
37
  ) -> None:
31
38
  """
32
39
  Delete a profile from the configuration.
33
40
 
41
+ Use the --yes flag to skip the confirmation prompt.
42
+
34
43
  [bold][underline]Examples[/underline][/bold]
35
44
 
36
45
  - Delete a profile named [magenta]hare[/magenta].
37
- $ [green]nextmv configuration delete --profile hare[/green]
46
+ $ [dim]nextmv configuration delete --profile hare[/dim]
47
+
48
+ - Delete a profile named [magenta]hare[/magenta] without confirmation prompt.
49
+ $ [dim]nextmv configuration delete --profile hare --yes[/dim]
38
50
  """
39
51
  config = load_config()
40
52
  if profile not in config:
41
- error(f"Profile [bold magenta]{profile}[/bold magenta] does not exist.")
53
+ error(f"Profile [magenta]{profile}[/magenta] does not exist.")
42
54
 
43
- confirm = Confirm.ask(
44
- f"Are you sure you want to delete profile [bold magenta]{profile}[/bold magenta]? This action cannot be undone",
45
- default=False,
46
- )
55
+ if not yes:
56
+ confirm = get_confirmation(
57
+ f"Are you sure you want to delete profile [magenta]{profile}[/magenta]? This action cannot be undone.",
58
+ )
47
59
 
48
- if not confirm:
49
- rich.print(f":bulb: Profile [bold magenta]{profile}[/bold magenta] will not be deleted.")
50
- return
60
+ if not confirm:
61
+ info(f"Profile [magenta]{profile}[/magenta] will not be deleted.")
62
+ return
51
63
 
52
64
  del config[profile]
53
65
  save_config(config)
54
66
 
55
- rich.print(f":white_check_mark: Profile [bold magenta]{profile}[/bold magenta] deleted successfully.")
67
+ success(f"Profile [magenta]{profile}[/magenta] deleted successfully.")
@@ -6,8 +6,8 @@ import typer
6
6
  from rich.console import Console
7
7
  from rich.table import Table
8
8
 
9
- from nextmv.cli.configuration.config import API_KEY_KEY, ENDPOINT_KEY, load_config, obscure_api_key
10
- from nextmv.cli.error import error
9
+ from nextmv.cli.configuration.config import API_KEY_KEY, ENDPOINT_KEY, load_config, non_profile_keys, obscure_api_key
10
+ from nextmv.cli.message import error
11
11
 
12
12
  # Set up subcommand application.
13
13
  app = typer.Typer()
@@ -22,7 +22,7 @@ def list() -> None:
22
22
  [bold][underline]Examples[/underline][/bold]
23
23
 
24
24
  - Show current configuration and all profiles.
25
- $ [green]nextmv configuration list[/green]
25
+ $ [dim]nextmv configuration list[/dim]
26
26
  """
27
27
 
28
28
  config = load_config()
@@ -38,7 +38,7 @@ def list() -> None:
38
38
 
39
39
  for k, v in config.items():
40
40
  # Skip default configuration.
41
- if k in {API_KEY_KEY, ENDPOINT_KEY}:
41
+ if k in non_profile_keys():
42
42
  continue
43
43
 
44
44
  profile = {
nextmv/cli/confirm.py ADDED
@@ -0,0 +1,34 @@
1
+ import sys
2
+
3
+ from rich.prompt import Confirm
4
+
5
+
6
+ def get_confirmation(msg: str, default: bool = False) -> bool:
7
+ """
8
+ Method to get a yes/no confirmation from the user.
9
+
10
+ Parameters
11
+ ----------
12
+ msg : str
13
+ The message to display to the user.
14
+ default : bool, optional
15
+ The default value if the user just presses Enter. Default is False.
16
+
17
+ Returns
18
+ -------
19
+ bool
20
+ True if the user confirmed, False otherwise.
21
+ """
22
+
23
+ # If this is not an interactive terminal, do not ask for confirmation, to
24
+ # avoid hanging indefinitely waiting for a user response.
25
+ if not sys.stdin.isatty():
26
+ return default
27
+
28
+ return Confirm.ask(
29
+ msg,
30
+ default=default,
31
+ case_sensitive=False,
32
+ show_default=True,
33
+ show_choices=True,
34
+ )
nextmv/cli/main.py CHANGED
@@ -13,17 +13,24 @@ about the features used here. An example of Rich markup can be found in the
13
13
  epilog of the Typer application defined below.
14
14
  """
15
15
 
16
- import os
16
+ import sys
17
+ from typing import Annotated
17
18
 
18
19
  import rich
19
20
  import typer
20
- from rich.prompt import Confirm
21
+ from typer import rich_utils
21
22
 
22
- from nextmv.cli.community.community import app as community_app
23
+ from nextmv.cli.cloud import app as cloud_app
24
+ from nextmv.cli.community import app as community_app
25
+ from nextmv.cli.configuration import app as configuration_app
23
26
  from nextmv.cli.configuration.config import CONFIG_DIR, GO_CLI_PATH, load_config
24
- from nextmv.cli.configuration.configuration import app as configuration_app
25
- from nextmv.cli.error import error
27
+ from nextmv.cli.confirm import get_confirmation
28
+ from nextmv.cli.message import error, info, success, warning
26
29
  from nextmv.cli.version import app as version_app
30
+ from nextmv.cli.version import version_callback
31
+
32
+ # Disable dim text for the extended help of commands.
33
+ rich_utils.STYLE_HELPTEXT = ""
27
34
 
28
35
  # Main CLI application.
29
36
  app = typer.Typer(
@@ -32,22 +39,45 @@ app = typer.Typer(
32
39
  rich_markup_mode="rich",
33
40
  context_settings={"help_option_names": ["--help", "-h"]},
34
41
  no_args_is_help=True,
42
+ invoke_without_command=True,
43
+ pretty_exceptions_show_locals=False,
35
44
  )
36
45
 
37
46
  # Register subcommands. The `name` parameter is required when the subcommand
38
47
  # module has a callback function defined.
48
+ app.add_typer(cloud_app, name="cloud")
39
49
  app.add_typer(community_app, name="community")
40
50
  app.add_typer(configuration_app, name="configuration")
41
51
  app.add_typer(version_app)
42
52
 
43
53
 
44
54
  @app.callback()
45
- def callback(ctx: typer.Context) -> None:
55
+ def callback(
56
+ ctx: typer.Context,
57
+ version: Annotated[
58
+ bool | None,
59
+ typer.Option(
60
+ "--version",
61
+ "-v",
62
+ help="Show the current version of the Nextmv CLI.",
63
+ callback=version_callback,
64
+ ),
65
+ ] = None,
66
+ ) -> None:
46
67
  """
47
68
  Callback function that runs before any command. Useful for checks on the
48
69
  environment.
49
70
  """
50
71
 
72
+ # Skip checks for help commands.
73
+ if "--help" in sys.argv or "-h" in sys.argv:
74
+ return
75
+
76
+ # Skip checks for certain commands.
77
+ ignored_commands = {"configuration", "version"}
78
+ if ctx.invoked_subcommand in ignored_commands:
79
+ return
80
+
51
81
  handle_go_cli()
52
82
  handle_config_existence(ctx)
53
83
 
@@ -62,18 +92,20 @@ def handle_go_cli() -> None:
62
92
 
63
93
  exists = go_cli_exists()
64
94
  if exists:
65
- delete = Confirm.ask(
95
+ delete = get_confirmation(
66
96
  "Do you want to delete the [italic red]deprecated[/italic red] Nextmv CLI "
67
- f"at [italic]{GO_CLI_PATH}[/italic] now?",
68
- default=True,
97
+ f"at [magenta]{GO_CLI_PATH}[/magenta] now?"
69
98
  )
70
99
  if delete:
71
100
  remove_go_cli()
72
- else:
73
- rich.print(
74
- ":bulb: You can delete the [italic red]deprecated[/italic red] Nextmv CLI "
75
- f"later by removing [italic]{GO_CLI_PATH}[/italic]. Make sure you also clean up your [code]PATH[/code]."
76
- )
101
+ return
102
+
103
+ info(
104
+ "You can delete the [italic red]deprecated[/italic red] Nextmv CLI later by removing "
105
+ f"[magenta]{GO_CLI_PATH}[/magenta]. "
106
+ "Make sure you also clean up your [code]PATH[/code], "
107
+ f"by removing references to [magenta]{CONFIG_DIR}[/magenta] from it."
108
+ )
77
109
 
78
110
 
79
111
  def handle_config_existence(ctx: typer.Context) -> None:
@@ -86,10 +118,6 @@ def handle_config_existence(ctx: typer.Context) -> None:
86
118
  The Typer context object.
87
119
  """
88
120
 
89
- ignored_commands = {"configuration", "version"}
90
- if ctx.invoked_subcommand in ignored_commands:
91
- return
92
-
93
121
  config = load_config()
94
122
  if config == {}:
95
123
  error("No configuration found. Please run [code]nextmv configuration create[/code].")
@@ -109,13 +137,11 @@ def go_cli_exists() -> bool:
109
137
  # Check if the Go CLI executable exists
110
138
  exists = GO_CLI_PATH.exists()
111
139
  if exists:
112
- rich.print(
113
- ":construction: A [italic red]deprecated[/italic red] Nextmv CLI is installed at "
114
- f"[italic]{GO_CLI_PATH}[/italic]. You must delete it to avoid conflicts."
140
+ warning(
141
+ "A [italic red]deprecated[/italic red] Nextmv CLI is installed at "
142
+ f"[magenta]{GO_CLI_PATH}[/magenta]. You should delete it to avoid conflicts."
115
143
  )
116
144
 
117
- check_config_in_path()
118
-
119
145
  return exists
120
146
 
121
147
 
@@ -126,21 +152,27 @@ def remove_go_cli() -> None:
126
152
 
127
153
  if GO_CLI_PATH.exists():
128
154
  GO_CLI_PATH.unlink()
129
- rich.print(f":white_check_mark: Deleted deprecated {GO_CLI_PATH}.")
155
+ success(f"Deleted [italic red]deprecated[/italic red] [magenta]{GO_CLI_PATH}[/magenta].")
130
156
 
131
- check_config_in_path()
132
157
 
133
-
134
- def check_config_in_path() -> None:
135
- """
136
- Check if the configuration directory is in the PATH and notify the user.
158
+ def main() -> None:
137
159
  """
160
+ Entry point for the CLI with global exception handling.
138
161
 
139
- path_dirs = os.environ.get("PATH", "").split(os.pathsep)
140
- config_dir_str = str(CONFIG_DIR)
162
+ Catches all exceptions except Typer/Click exceptions (which handle their
163
+ own exit codes) and displays a clean error message instead of a traceback.
164
+ """
141
165
 
142
- if config_dir_str in path_dirs:
143
- rich.print(
144
- f":construction: [italic]{CONFIG_DIR}[/italic] was found in your [code]PATH[/code]. "
145
- f"You should remove any entries related to [italic]{CONFIG_DIR}[/italic] from your [code]PATH[/code]."
146
- )
166
+ try:
167
+ app()
168
+ except (typer.Exit, typer.Abort, SystemExit):
169
+ raise
170
+ except Exception as e:
171
+ # We do not use the messages.error function here because doing so would
172
+ # raise a Typer exception, which would print a traceback.
173
+ msg = str(e).rstrip("\n")
174
+ if not msg.endswith("."):
175
+ msg += "."
176
+
177
+ rich.print(f"[red]Error:[/red] {msg}", file=sys.stderr)
178
+ sys.exit(1)