nextmv 0.40.0__py3-none-any.whl → 1.0.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 (129) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__init__.py +2 -0
  3. nextmv/cli/CONTRIBUTING.md +511 -0
  4. nextmv/cli/cloud/__init__.py +45 -0
  5. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  6. nextmv/cli/cloud/acceptance/create.py +393 -0
  7. nextmv/cli/cloud/acceptance/delete.py +68 -0
  8. nextmv/cli/cloud/acceptance/get.py +104 -0
  9. nextmv/cli/cloud/acceptance/list.py +62 -0
  10. nextmv/cli/cloud/acceptance/update.py +95 -0
  11. nextmv/cli/cloud/account/__init__.py +28 -0
  12. nextmv/cli/cloud/account/create.py +83 -0
  13. nextmv/cli/cloud/account/delete.py +60 -0
  14. nextmv/cli/cloud/account/get.py +66 -0
  15. nextmv/cli/cloud/account/update.py +70 -0
  16. nextmv/cli/cloud/app/__init__.py +35 -0
  17. nextmv/cli/cloud/app/create.py +141 -0
  18. nextmv/cli/cloud/app/delete.py +58 -0
  19. nextmv/cli/cloud/app/exists.py +44 -0
  20. nextmv/cli/cloud/app/get.py +66 -0
  21. nextmv/cli/cloud/app/list.py +61 -0
  22. nextmv/cli/cloud/app/push.py +137 -0
  23. nextmv/cli/cloud/app/update.py +124 -0
  24. nextmv/cli/cloud/batch/__init__.py +29 -0
  25. nextmv/cli/cloud/batch/create.py +454 -0
  26. nextmv/cli/cloud/batch/delete.py +68 -0
  27. nextmv/cli/cloud/batch/get.py +104 -0
  28. nextmv/cli/cloud/batch/list.py +63 -0
  29. nextmv/cli/cloud/batch/metadata.py +66 -0
  30. nextmv/cli/cloud/batch/update.py +95 -0
  31. nextmv/cli/cloud/data/__init__.py +26 -0
  32. nextmv/cli/cloud/data/upload.py +162 -0
  33. nextmv/cli/cloud/ensemble/__init__.py +31 -0
  34. nextmv/cli/cloud/ensemble/create.py +414 -0
  35. nextmv/cli/cloud/ensemble/delete.py +67 -0
  36. nextmv/cli/cloud/ensemble/get.py +65 -0
  37. nextmv/cli/cloud/ensemble/update.py +103 -0
  38. nextmv/cli/cloud/input_set/__init__.py +30 -0
  39. nextmv/cli/cloud/input_set/create.py +168 -0
  40. nextmv/cli/cloud/input_set/get.py +63 -0
  41. nextmv/cli/cloud/input_set/list.py +63 -0
  42. nextmv/cli/cloud/input_set/update.py +123 -0
  43. nextmv/cli/cloud/instance/__init__.py +35 -0
  44. nextmv/cli/cloud/instance/create.py +290 -0
  45. nextmv/cli/cloud/instance/delete.py +62 -0
  46. nextmv/cli/cloud/instance/exists.py +39 -0
  47. nextmv/cli/cloud/instance/get.py +62 -0
  48. nextmv/cli/cloud/instance/list.py +60 -0
  49. nextmv/cli/cloud/instance/update.py +216 -0
  50. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  51. nextmv/cli/cloud/managed_input/create.py +146 -0
  52. nextmv/cli/cloud/managed_input/delete.py +65 -0
  53. nextmv/cli/cloud/managed_input/get.py +63 -0
  54. nextmv/cli/cloud/managed_input/list.py +60 -0
  55. nextmv/cli/cloud/managed_input/update.py +97 -0
  56. nextmv/cli/cloud/run/__init__.py +37 -0
  57. nextmv/cli/cloud/run/cancel.py +37 -0
  58. nextmv/cli/cloud/run/create.py +530 -0
  59. nextmv/cli/cloud/run/get.py +199 -0
  60. nextmv/cli/cloud/run/input.py +86 -0
  61. nextmv/cli/cloud/run/list.py +80 -0
  62. nextmv/cli/cloud/run/logs.py +167 -0
  63. nextmv/cli/cloud/run/metadata.py +67 -0
  64. nextmv/cli/cloud/run/track.py +501 -0
  65. nextmv/cli/cloud/scenario/__init__.py +29 -0
  66. nextmv/cli/cloud/scenario/create.py +451 -0
  67. nextmv/cli/cloud/scenario/delete.py +65 -0
  68. nextmv/cli/cloud/scenario/get.py +102 -0
  69. nextmv/cli/cloud/scenario/list.py +63 -0
  70. nextmv/cli/cloud/scenario/metadata.py +67 -0
  71. nextmv/cli/cloud/scenario/update.py +93 -0
  72. nextmv/cli/cloud/secrets/__init__.py +33 -0
  73. nextmv/cli/cloud/secrets/create.py +206 -0
  74. nextmv/cli/cloud/secrets/delete.py +67 -0
  75. nextmv/cli/cloud/secrets/get.py +66 -0
  76. nextmv/cli/cloud/secrets/list.py +60 -0
  77. nextmv/cli/cloud/secrets/update.py +147 -0
  78. nextmv/cli/cloud/upload/__init__.py +22 -0
  79. nextmv/cli/cloud/upload/create.py +39 -0
  80. nextmv/cli/cloud/version/__init__.py +33 -0
  81. nextmv/cli/cloud/version/create.py +97 -0
  82. nextmv/cli/cloud/version/delete.py +62 -0
  83. nextmv/cli/cloud/version/exists.py +39 -0
  84. nextmv/cli/cloud/version/get.py +62 -0
  85. nextmv/cli/cloud/version/list.py +60 -0
  86. nextmv/cli/cloud/version/update.py +92 -0
  87. nextmv/cli/community/__init__.py +24 -0
  88. nextmv/cli/community/clone.py +3 -3
  89. nextmv/cli/community/list.py +1 -1
  90. nextmv/cli/configuration/__init__.py +23 -0
  91. nextmv/cli/configuration/config.py +68 -4
  92. nextmv/cli/configuration/create.py +14 -15
  93. nextmv/cli/configuration/delete.py +24 -12
  94. nextmv/cli/configuration/list.py +1 -1
  95. nextmv/cli/main.py +58 -16
  96. nextmv/cli/message.py +153 -0
  97. nextmv/cli/options.py +168 -0
  98. nextmv/cli/version.py +20 -1
  99. nextmv/cloud/__init__.py +4 -1
  100. nextmv/cloud/acceptance_test.py +19 -18
  101. nextmv/cloud/account.py +268 -24
  102. nextmv/cloud/application/__init__.py +955 -0
  103. nextmv/cloud/application/_acceptance.py +419 -0
  104. nextmv/cloud/application/_batch_scenario.py +860 -0
  105. nextmv/cloud/application/_ensemble.py +251 -0
  106. nextmv/cloud/application/_input_set.py +227 -0
  107. nextmv/cloud/application/_instance.py +289 -0
  108. nextmv/cloud/application/_managed_input.py +227 -0
  109. nextmv/cloud/application/_run.py +1393 -0
  110. nextmv/cloud/application/_secrets.py +294 -0
  111. nextmv/cloud/application/_utils.py +54 -0
  112. nextmv/cloud/application/_version.py +303 -0
  113. nextmv/cloud/batch_experiment.py +3 -1
  114. nextmv/cloud/instance.py +11 -1
  115. nextmv/cloud/integration.py +1 -1
  116. nextmv/cloud/package.py +50 -9
  117. nextmv/input.py +20 -36
  118. nextmv/local/application.py +3 -15
  119. nextmv/polling.py +54 -16
  120. nextmv/run.py +83 -27
  121. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dev0.dist-info}/METADATA +33 -8
  122. nextmv-1.0.0.dev0.dist-info/RECORD +158 -0
  123. nextmv/cli/community/community.py +0 -24
  124. nextmv/cli/configuration/configuration.py +0 -23
  125. nextmv/cli/error.py +0 -22
  126. nextmv/cloud/application.py +0 -4204
  127. nextmv-0.40.0.dist-info/RECORD +0 -66
  128. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dev0.dist-info}/WHEEL +0 -0
  129. {nextmv-0.40.0.dist-info → nextmv-1.0.0.dev0.dist-info}/licenses/LICENSE +0 -0
@@ -13,7 +13,7 @@ import rich
13
13
  import typer
14
14
 
15
15
  from nextmv.cli.community.list import download_file, download_manifest, find_app, versions_table
16
- from nextmv.cli.error import error
16
+ from nextmv.cli.message import error, success
17
17
  from nextmv.cli.options import ProfileOption
18
18
 
19
19
  # Set up subcommand application.
@@ -134,8 +134,8 @@ def clone(
134
134
  # Remove the tarball after extraction
135
135
  os.remove(downloaded_object)
136
136
 
137
- rich.print(
138
- f":white_check_mark: Successfully cloned the [magenta]{app}[/magenta] community app, "
137
+ success(
138
+ f"Successfully cloned the [magenta]{app}[/magenta] community app, "
139
139
  f"using version [magenta]{original_version}[/magenta] in path: [magenta]{full_destination}[/magenta]."
140
140
  )
141
141
 
@@ -12,7 +12,7 @@ from rich.console import Console
12
12
  from rich.table import Table
13
13
 
14
14
  from nextmv.cli.configuration.config import build_client
15
- from nextmv.cli.error import error
15
+ from nextmv.cli.message import error
16
16
  from nextmv.cli.options import ProfileOption
17
17
 
18
18
  # Set up subcommand application.
@@ -0,0 +1,23 @@
1
+ """
2
+ This module defines the configuration command tree for the Nextmv CLI.
3
+ """
4
+
5
+ import typer
6
+
7
+ from nextmv.cli.configuration.create import app as create_app
8
+ from nextmv.cli.configuration.delete import app as delete_app
9
+ from nextmv.cli.configuration.list import app as list_app
10
+
11
+ # Set up subcommand application.
12
+ app = typer.Typer()
13
+ app.add_typer(create_app)
14
+ app.add_typer(delete_app)
15
+ app.add_typer(list_app)
16
+
17
+
18
+ @app.callback()
19
+ def callback() -> None:
20
+ """
21
+ Configure the CLI and manage profiles.
22
+ """
23
+ pass
@@ -7,7 +7,9 @@ from typing import Any
7
7
 
8
8
  import yaml
9
9
 
10
- from nextmv.cli.error import error
10
+ from nextmv.cli.message import error
11
+ from nextmv.cloud.account import Account
12
+ from nextmv.cloud.application import Application
11
13
  from nextmv.cloud.client import Client
12
14
 
13
15
  # Some useful constants.
@@ -89,15 +91,15 @@ def build_client(profile: str | None = None) -> Client:
89
91
 
90
92
  if profile is not None:
91
93
  if profile not in config:
92
- error(f"Profile [bold magenta]{profile}[/bold magenta] does not exist.")
94
+ error(f"Profile [magenta]{profile}[/magenta] does not exist.")
93
95
 
94
96
  api_key = config[profile].get(API_KEY_KEY)
95
97
  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.")
98
+ error(f"API key for profile [magenta]{profile}[/magenta] is not set or is empty.")
97
99
 
98
100
  endpoint = config[profile].get(ENDPOINT_KEY)
99
101
  if endpoint is None or endpoint == "":
100
- error(f"Endpoint for profile [bold magenta]{profile}[/bold magenta] is not set or is empty.")
102
+ error(f"Endpoint for profile [magenta]{profile}[/magenta] is not set or is empty.")
101
103
  else:
102
104
  api_key = config.get(API_KEY_KEY)
103
105
  if api_key is None or api_key == "":
@@ -110,6 +112,68 @@ def build_client(profile: str | None = None) -> Client:
110
112
  return Client(api_key=api_key, url=f"https://{endpoint}")
111
113
 
112
114
 
115
+ def build_app(app_id: str, profile: str | None = None) -> Application:
116
+ """
117
+ Builds a `cloud.Application` using the given application ID and the API
118
+ key and endpoint for the given profile. If no profile is given, the default
119
+ profile is used. If the application does not exist, an exception is raised.
120
+
121
+ Parameters
122
+ ----------
123
+ app_id : str
124
+ The application ID.
125
+ profile : str | None
126
+ The profile name to use. If None, the default profile is used.
127
+
128
+ Returns
129
+ -------
130
+ Application
131
+ An application object for the given application ID.
132
+
133
+ Raises
134
+ ------
135
+ typer.Exit
136
+ If the application does not exist.
137
+ """
138
+ client = build_client(profile)
139
+ exists = Application.exists(client=client, id=app_id)
140
+ if not exists:
141
+ error(
142
+ f"Application with ID [magenta]{app_id}[/magenta] does not exist. "
143
+ "Use [code]nextmv cloud app create[/code] to create a new application."
144
+ )
145
+
146
+ return Application(client=client, id=app_id)
147
+
148
+
149
+ def build_account(account_id: str | None = None, profile: str | None = None) -> Account:
150
+ """
151
+ Builds a `cloud.Account` using the API key and endpoint for the given
152
+ profile. If no profile is given, the default profile is used.
153
+
154
+ Parameters
155
+ ----------
156
+ account_id : str | None
157
+ The account ID. If None, no account ID is set.
158
+ profile : str | None
159
+ The profile name to use. If None, the default profile is used.
160
+
161
+ Returns
162
+ -------
163
+ Account
164
+ An account object for the configured profile.
165
+
166
+ Raises
167
+ ------
168
+ typer.Exit
169
+ If the configuration is invalid or missing.
170
+ """
171
+
172
+ client = build_client(profile)
173
+
174
+ return Account(account_id=account_id, client=client)
175
+
176
+
113
177
  def obscure_api_key(api_key: str) -> str:
114
178
  """
115
179
  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, info, 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.
@@ -61,7 +60,7 @@ def create(
61
60
  - Default configuration.
62
61
  $ [green]nextmv configuration create --api-key NEXTMV_API_KEY[/green]
63
62
 
64
- - Configure a profile named [italic]hare[/italic].
63
+ - Configure a profile named [magenta]hare[/magenta].
65
64
  $ [green]nextmv configuration create --api-key NEXTMV_API_KEY --profile hare[/green]
66
65
  """
67
66
 
@@ -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
+ info(f"\t[bold]Profile[/bold]: {profile or 'Default'}")
92
+ info(f"\t[bold]API Key[/bold]: {obscure_api_key(api_key)}")
94
93
  if endpoint != DEFAULT_ENDPOINT:
95
- rich.print(f"\t[bold]Endpoint[/bold]: {endpoint}")
94
+ info(f"\t[bold]Endpoint[/bold]: {endpoint}")
@@ -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
8
  from rich.prompt import Confirm
10
9
 
11
10
  from nextmv.cli.configuration.config import load_config, save_config
12
- from nextmv.cli.error import error
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
- Delete a profile from the configuration.
39
+ Delete a profile from the configuration. Use the [code]--yes[/code]
40
+ flag to skip the confirmation prompt.
33
41
 
34
42
  [bold][underline]Examples[/underline][/bold]
35
43
 
36
44
  - Delete a profile named [magenta]hare[/magenta].
37
45
  $ [green]nextmv configuration delete --profile hare[/green]
46
+
47
+ - Delete a profile named [magenta]hare[/magenta] without confirmation prompt.
48
+ $ [green]nextmv configuration delete --profile hare --yes[/green]
38
49
  """
39
50
  config = load_config()
40
51
  if profile not in config:
41
- error(f"Profile [bold magenta]{profile}[/bold magenta] does not exist.")
52
+ error(f"Profile [magenta]{profile}[/magenta] does not exist.")
42
53
 
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
- )
54
+ if not yes:
55
+ confirm = Confirm.ask(
56
+ f"Are you sure you want to delete profile [magenta]{profile}[/magenta]? This action cannot be undone.",
57
+ default=False,
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(msg=f"Profile [magenta]{profile}[/magenta] will not be deleted.", emoji=":bulb:")
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.")
@@ -7,7 +7,7 @@ from rich.console import Console
7
7
  from rich.table import Table
8
8
 
9
9
  from nextmv.cli.configuration.config import API_KEY_KEY, ENDPOINT_KEY, load_config, obscure_api_key
10
- from nextmv.cli.error import error
10
+ from nextmv.cli.message import error
11
11
 
12
12
  # Set up subcommand application.
13
13
  app = typer.Typer()
nextmv/cli/main.py CHANGED
@@ -14,16 +14,20 @@ epilog of the Typer application defined below.
14
14
  """
15
15
 
16
16
  import os
17
+ import sys
18
+ from typing import Annotated
17
19
 
18
20
  import rich
19
21
  import typer
20
22
  from rich.prompt import Confirm
21
23
 
22
- from nextmv.cli.community.community import app as community_app
24
+ from nextmv.cli.cloud import app as cloud_app
25
+ from nextmv.cli.community import app as community_app
26
+ from nextmv.cli.configuration import app as configuration_app
23
27
  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
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
27
31
 
28
32
  # Main CLI application.
29
33
  app = typer.Typer(
@@ -32,17 +36,31 @@ app = typer.Typer(
32
36
  rich_markup_mode="rich",
33
37
  context_settings={"help_option_names": ["--help", "-h"]},
34
38
  no_args_is_help=True,
39
+ invoke_without_command=True,
40
+ pretty_exceptions_show_locals=False,
35
41
  )
36
42
 
37
43
  # Register subcommands. The `name` parameter is required when the subcommand
38
44
  # module has a callback function defined.
45
+ app.add_typer(cloud_app, name="cloud")
39
46
  app.add_typer(community_app, name="community")
40
47
  app.add_typer(configuration_app, name="configuration")
41
48
  app.add_typer(version_app)
42
49
 
43
50
 
44
51
  @app.callback()
45
- def callback(ctx: typer.Context) -> None:
52
+ def callback(
53
+ ctx: typer.Context,
54
+ version: Annotated[
55
+ bool | None,
56
+ typer.Option(
57
+ "--version",
58
+ "-v",
59
+ help="Show the current version of the Nextmv CLI.",
60
+ callback=version_callback,
61
+ ),
62
+ ] = None,
63
+ ) -> None:
46
64
  """
47
65
  Callback function that runs before any command. Useful for checks on the
48
66
  environment.
@@ -64,15 +82,16 @@ def handle_go_cli() -> None:
64
82
  if exists:
65
83
  delete = Confirm.ask(
66
84
  "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,
85
+ f"at [magenta]{GO_CLI_PATH}[/magenta] now?",
86
+ default=False,
69
87
  )
70
88
  if delete:
71
89
  remove_go_cli()
72
90
  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]."
91
+ info(
92
+ msg="You can delete the [italic red]deprecated[/italic red] Nextmv CLI later by removing "
93
+ f"[magenta]{GO_CLI_PATH}[/magenta]. Make sure you also clean up your [code]PATH[/code].",
94
+ emoji=":bulb:",
76
95
  )
77
96
 
78
97
 
@@ -109,9 +128,9 @@ def go_cli_exists() -> bool:
109
128
  # Check if the Go CLI executable exists
110
129
  exists = GO_CLI_PATH.exists()
111
130
  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."
131
+ warning(
132
+ "A [italic red]deprecated[/italic red] Nextmv CLI is installed at "
133
+ f"[magenta]{GO_CLI_PATH}[/magenta]. You must delete it to avoid conflicts."
115
134
  )
116
135
 
117
136
  check_config_in_path()
@@ -126,7 +145,7 @@ def remove_go_cli() -> None:
126
145
 
127
146
  if GO_CLI_PATH.exists():
128
147
  GO_CLI_PATH.unlink()
129
- rich.print(f":white_check_mark: Deleted deprecated {GO_CLI_PATH}.")
148
+ success(f"Deleted deprecated [magenta]{GO_CLI_PATH}[/magenta].")
130
149
 
131
150
  check_config_in_path()
132
151
 
@@ -140,7 +159,30 @@ def check_config_in_path() -> None:
140
159
  config_dir_str = str(CONFIG_DIR)
141
160
 
142
161
  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]."
162
+ warning(
163
+ f"[magenta]{CONFIG_DIR}[/magenta] was found in your [code]PATH[/code]. "
164
+ f"You should remove any entries related to [magenta]{CONFIG_DIR}[/magenta] from your [code]PATH[/code]."
146
165
  )
166
+
167
+
168
+ def main() -> None:
169
+ """
170
+ Entry point for the CLI with global exception handling.
171
+
172
+ Catches all exceptions except Typer/Click exceptions (which handle their
173
+ own exit codes) and displays a clean error message instead of a traceback.
174
+ """
175
+
176
+ try:
177
+ app()
178
+ except (typer.Exit, typer.Abort, SystemExit):
179
+ raise
180
+ except Exception as e:
181
+ # We do not use the messages.error function here because doing so would
182
+ # raise a Typer exception, which would print a traceback.
183
+ msg = str(e).rstrip("\n")
184
+ if not msg.endswith("."):
185
+ msg += "."
186
+
187
+ rich.print(f"[red]Error:[/red] {msg}", file=sys.stderr)
188
+ sys.exit(1)
nextmv/cli/message.py ADDED
@@ -0,0 +1,153 @@
1
+ """
2
+ The message module is used to print messages to the user with pre-defined
3
+ formatting. Logging, in general, is always printed to stderr.
4
+ """
5
+
6
+ import sys
7
+ from enum import Enum
8
+ from typing import Any
9
+
10
+ import rich
11
+ import typer
12
+
13
+
14
+ def error(msg: str) -> None:
15
+ """
16
+ Pretty-print an error message and exit with code 1. Your message should end
17
+ with a period.
18
+
19
+ Parameters
20
+ ----------
21
+ msg : str
22
+ The error message to display.
23
+
24
+ Raises
25
+ ------
26
+ typer.Exit
27
+ Exits the program with code 1.
28
+ """
29
+
30
+ msg = msg.rstrip("\n")
31
+ if not msg.endswith("."):
32
+ msg += "."
33
+
34
+ rich.print(f"[red]Error:[/red] {msg}", file=sys.stderr)
35
+
36
+ raise typer.Exit(code=1)
37
+
38
+
39
+ def success(msg: str) -> None:
40
+ """
41
+ Pretty-print a success message. Your message should end with a period.
42
+
43
+ Parameters
44
+ ----------
45
+ msg : str
46
+ The success message to display.
47
+ """
48
+
49
+ msg = msg.rstrip("\n")
50
+ if not msg.endswith("."):
51
+ msg += "."
52
+
53
+ rich.print(f":white_check_mark: {msg}", file=sys.stderr)
54
+
55
+
56
+ def warning(msg: str) -> None:
57
+ """
58
+ Pretty-print a warning message. Your message should end with a period.
59
+
60
+ Parameters
61
+ ----------
62
+ msg : str
63
+ The warning message to display.
64
+ """
65
+
66
+ msg = msg.rstrip("\n")
67
+ if not msg.endswith("."):
68
+ msg += "."
69
+
70
+ rich.print(f":construction: {msg}", file=sys.stderr)
71
+
72
+
73
+ def info(msg: str, emoji: str | None = None) -> None:
74
+ """
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.
79
+
80
+ Parameters
81
+ ----------
82
+ 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:`.
89
+ """
90
+
91
+ msg = msg.rstrip("\n")
92
+ if not msg.endswith("."):
93
+ msg += "."
94
+
95
+ if emoji:
96
+ rich.print(f"{emoji} {msg}", file=sys.stderr)
97
+ return
98
+
99
+ rich.print(msg, file=sys.stderr)
100
+
101
+
102
+ def in_progress(msg: str) -> None:
103
+ """
104
+ Pretty-print an in-progress message with an hourglass emoji. Your message
105
+ should end with a period.
106
+
107
+ Parameters
108
+ ----------
109
+ msg : str
110
+ The in-progress message to display.
111
+ """
112
+
113
+ info(msg, emoji=":hourglass_flowing_sand:")
114
+
115
+
116
+ def print_json(data: dict[str, Any] | list[dict[str, Any]]) -> None:
117
+ """
118
+ Pretty-print json-serializable data as JSON to stdout.
119
+
120
+ Parameters
121
+ ----------
122
+ data : dict[str, Any] | list[dict[str, Any]]
123
+ The data to print as JSON.
124
+ """
125
+
126
+ rich.print_json(data=data)
127
+
128
+
129
+ def enum_values(enum_class: Enum) -> str:
130
+ """
131
+ Get a nicely formatted string of the values of an Enum class, using commas
132
+ and an oxford comma.
133
+
134
+ Parameters
135
+ ----------
136
+ enum_class : Enum
137
+ The Enum class to get the values from.
138
+
139
+ Returns
140
+ -------
141
+ str
142
+ A nicely formatted string of the values of the Enum class.
143
+ """
144
+
145
+ values = [f"[magenta]{member.value}[/magenta]" for member in enum_class]
146
+ if len(values) == 0:
147
+ return ""
148
+ if len(values) == 1:
149
+ return values[0]
150
+ if len(values) == 2:
151
+ return " and ".join(values)
152
+
153
+ return ", ".join(values[:-1]) + ", and " + values[-1]