nextmv 0.39.0.dev1__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 (161) 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 +86 -0
  110. nextmv/cli/community/list.py +200 -0
  111. nextmv/cli/configuration/__init__.py +23 -0
  112. nextmv/cli/configuration/config.py +228 -0
  113. nextmv/cli/configuration/create.py +94 -0
  114. nextmv/cli/configuration/delete.py +67 -0
  115. nextmv/cli/configuration/list.py +77 -0
  116. nextmv/cli/confirm.py +34 -0
  117. nextmv/cli/main.py +161 -3
  118. nextmv/cli/message.py +170 -0
  119. nextmv/cli/options.py +220 -0
  120. nextmv/cli/version.py +22 -2
  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.39.0.dev1.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/cloud/application.py +0 -4204
  158. nextmv-0.39.0.dev1.dist-info/RECORD +0 -55
  159. nextmv-0.39.0.dev1.dist-info/entry_points.txt +0 -2
  160. {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/WHEEL +0 -0
  161. {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,500 @@
1
+ """
2
+ This module defines the cloud run track command for the Nextmv CLI.
3
+ """
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ from nextmv.cli.cloud.run.create import build_run_config
13
+ from nextmv.cli.configuration.config import build_app
14
+ from nextmv.cli.message import enum_values, error, in_progress, print_json
15
+ from nextmv.cli.options import AppIDOption, ProfileOption
16
+ from nextmv.input import InputFormat
17
+ from nextmv.run import RunType, TrackedRun, TrackedRunStatus
18
+
19
+ # Set up subcommand application.
20
+ app = typer.Typer()
21
+
22
+
23
+ @app.command()
24
+ def track(
25
+ app_id: AppIDOption,
26
+ # Options for controlling the tracked run.
27
+ output: Annotated[
28
+ str,
29
+ typer.Option(
30
+ "--output",
31
+ "-o",
32
+ help="The output of the run being tracked. A file or directory depending on content format.",
33
+ metavar="OUTPUT_PATH",
34
+ rich_help_panel="Tracked run configuration",
35
+ ),
36
+ ],
37
+ status: Annotated[
38
+ TrackedRunStatus,
39
+ typer.Option(
40
+ "--status",
41
+ "-s",
42
+ help=f"Status of the tracked run. Allowed values are: {enum_values(TrackedRunStatus)}.",
43
+ metavar="STATUS",
44
+ rich_help_panel="Tracked run configuration",
45
+ ),
46
+ ],
47
+ assets: Annotated[
48
+ str | None,
49
+ typer.Option(
50
+ help="The assets of the run being tracked. A [magenta]json[/magenta] file to read the assets from.",
51
+ metavar="ASSETS_PATH",
52
+ rich_help_panel="Tracked run configuration",
53
+ ),
54
+ ] = None,
55
+ content_format: Annotated[
56
+ InputFormat | None,
57
+ typer.Option(
58
+ "--content-format",
59
+ "-c",
60
+ help=f"The content format of the run to track. Allowed values are: {enum_values(InputFormat)}.",
61
+ metavar="CONTENT_FORMAT",
62
+ rich_help_panel="Tracked run configuration",
63
+ ),
64
+ ] = InputFormat.JSON,
65
+ description: Annotated[
66
+ str | None,
67
+ typer.Option(
68
+ help="An optional description for the tracked run.",
69
+ metavar="DESCRIPTION",
70
+ rich_help_panel="Tracked run configuration",
71
+ ),
72
+ ] = None,
73
+ duration: Annotated[
74
+ int,
75
+ typer.Option(
76
+ "--duration",
77
+ "-d",
78
+ help="The duration of the run being tracked, in milliseconds.",
79
+ metavar="DURATION_MS",
80
+ rich_help_panel="Tracked run configuration",
81
+ ),
82
+ ] = 0,
83
+ error_msg: Annotated[
84
+ str | None,
85
+ typer.Option(
86
+ "--error-msg",
87
+ "-e",
88
+ help="An error message if the run being tracked failed.",
89
+ metavar="ERROR_MESSAGE",
90
+ rich_help_panel="Tracked run configuration",
91
+ ),
92
+ ] = None,
93
+ input: Annotated[
94
+ str | None,
95
+ typer.Option(
96
+ "--input",
97
+ "-i",
98
+ help="The input of the run being tracked. File or directory depending on content format. "
99
+ "Uses [magenta]stdin[/magenta] if not defined.",
100
+ metavar="INPUT_PATH",
101
+ rich_help_panel="Tracked run configuration",
102
+ ),
103
+ ] = None,
104
+ logs: Annotated[
105
+ str | None,
106
+ typer.Option(
107
+ "--logs",
108
+ "-l",
109
+ help="The logs of the run being tracked. A utf-8 encoded text file to read the logs from.",
110
+ metavar="LOGS_PATH",
111
+ rich_help_panel="Tracked run configuration",
112
+ ),
113
+ ] = None,
114
+ name: Annotated[
115
+ str | None,
116
+ typer.Option(
117
+ "--name",
118
+ "-n",
119
+ help="An optional name for the tracked run.",
120
+ metavar="NAME",
121
+ rich_help_panel="Tracked run configuration",
122
+ ),
123
+ ] = None,
124
+ statistics: Annotated[
125
+ str | None,
126
+ typer.Option(
127
+ help="The statistics of the run being tracked. A [magenta]json[/magenta] file to read the statistics from.",
128
+ metavar="STATISTICS_PATH",
129
+ rich_help_panel="Tracked run configuration",
130
+ ),
131
+ ] = None,
132
+ # Options for run configuration.
133
+ instance_id: Annotated[
134
+ str | None,
135
+ typer.Option(
136
+ help="The instance ID to use for the run.",
137
+ metavar="INSTANCE_ID",
138
+ rich_help_panel="Run configuration",
139
+ ),
140
+ ] = "latest",
141
+ profile: ProfileOption = None,
142
+ ) -> None:
143
+ """
144
+ Track an external run as a Nextmv Cloud application run.
145
+
146
+ Please see the help of the --content-type option for details on valid
147
+ content types.
148
+
149
+ If the content type is [magenta]json[/magenta] or [magenta]text[/magenta],
150
+ then input for the run can be given through [magenta]stdin[/magenta]. The
151
+ --input option allows you to specify a file or directory path for the
152
+ input, instead of using [magenta]stdin[/magenta]. In the case of
153
+ [magenta]multi-file[/magenta] content type, the input must be given through
154
+ a directory specified via the --input option.
155
+
156
+ The --output option allows you to specify a file or directory path for the
157
+ output of the run. The behavior depends on the content type. If the content
158
+ type is [magenta]json[/magenta] or [magenta]text[/magenta], then a file
159
+ path must be provided. If the content type is
160
+ [magenta]multi-file[/magenta], then a directory path must be provided.
161
+
162
+ Run logs, assets, and statistics can be provided via files using the
163
+ --logs, --assets, and --statistics options, respectively. Assets and
164
+ statistics must be provided as [magenta]json[/magenta] files, while logs
165
+ must be provided as a utf-8 encoded text file.
166
+
167
+ [bold][underline]Examples[/underline][/bold]
168
+
169
+ - Track a [magenta]successful[/magenta] [magenta]json[/magenta] run via [magenta]stdin[/magenta]
170
+ input, for an app with ID [magenta]hare-app[/magenta].
171
+ $ [dim]cat input.json | nextmv cloud run track --app-id hare-app --status succeeded[/dim]
172
+
173
+ - Track a [magenta]successful[/magenta] [magenta]json[/magenta] run with input from an
174
+ [magenta]input.json[/magenta] file and output from an
175
+ [magenta]output.json[/magenta] file, for an app with ID
176
+ [magenta]hare-app[/magenta].
177
+ $ [dim]nextmv cloud run track --app-id hare-app --status succeeded --input input.json \\
178
+ --output output.json[/dim]
179
+
180
+ - Track a [magenta]successful[/magenta] [magenta]json[/magenta] run including logs from a
181
+ [magenta]logs.log[/magenta] file, for an app with ID
182
+ [magenta]hare-app[/magenta].
183
+ $ [dim]nextmv cloud run track --app-id hare-app --status succeeded --input input.json \\
184
+ --output output.json --logs logs.log[/dim]
185
+
186
+ - Track a [magenta]successful[/magenta] [magenta]json[/magenta] run with assets and statistics
187
+ from [magenta]json[/magenta] files, for an app with ID
188
+ [magenta]hare-app[/magenta].
189
+ $ [dim]nextmv cloud run track --app-id hare-app --status succeeded --input input.json \\
190
+ --output output.json --assets assets.json --statistics statistics.json[/dim]
191
+
192
+ - Track a [magenta]failed[/magenta] run with an error message, for an app with ID
193
+ [magenta]hare-app[/magenta].
194
+ $ [dim]nextmv cloud run track --app-id hare-app --status failed --input input.json \\
195
+ --error-msg "Solver timed out"[/dim]
196
+
197
+ - Track a [magenta]successful[/magenta] [magenta]text[/magenta] run with text content type,
198
+ for an app with ID [magenta]hare-app[/magenta].
199
+ $ [dim]nextmv cloud run track --app-id hare-app --status succeeded --input input.txt \\
200
+ --output output.txt --content-type text[/dim]
201
+
202
+ - Track a [magenta]successful[/magenta] [magenta]multi-file[/magenta] run from an
203
+ [magenta]inputs[/magenta] directory with output to an
204
+ [magenta]outputs[/magenta] directory, for an app with ID
205
+ [magenta]hare-app[/magenta], using the [magenta]default[/magenta]
206
+ instance.
207
+ $ [dim]nextmv cloud run track --app-id hare-app --status succeeded --input inputs \\
208
+ --output outputs --content-type multi-file --instance-id default[/dim]
209
+
210
+ - Track a [magenta]successful[/magenta] run with a name, description, and duration, for an app
211
+ with ID [magenta]hare-app[/magenta].
212
+ $ [dim]nextmv cloud run track --app-id hare-app --status succeeded --input input.json \\
213
+ --output output.json --name "Production run" --description "Weekly optimization" --duration 5000[/dim]
214
+
215
+ - Track a [magenta]successful[/magenta] [magenta]json[/magenta] run with all available options,
216
+ for an app with ID [magenta]hare-app[/magenta].
217
+ $ [dim]nextmv cloud run track --app-id hare-app --status succeeded --input input.json \\
218
+ --output output.json --logs logs.log --assets assets.json --statistics statistics.json \\
219
+ --name "Full run" --description "Complete example" --duration 10000 --instance-id burrow[/dim]
220
+ """
221
+
222
+ # Validate that input is provided.
223
+ stdin = sys.stdin.read().strip() if sys.stdin.isatty() is False else None
224
+ if stdin is None and (input is None or input == ""):
225
+ error("Input data must be provided via the --input flag or [magenta]stdin[/magenta].")
226
+
227
+ # Instantiate the basic requirements to start a new run.
228
+ cloud_app = build_app(app_id=app_id, profile=profile)
229
+ config = build_run_config(
230
+ run_type=RunType.EXTERNAL,
231
+ priority=6,
232
+ no_queuing=False,
233
+ content_format=content_format,
234
+ )
235
+
236
+ # Handles the default instance.
237
+ if instance_id == "default":
238
+ instance_id = ""
239
+
240
+ # Build the tracked run input.
241
+ tracked_run = build_tracked_run_input(
242
+ status=status,
243
+ duration=duration,
244
+ error_msg=error_msg,
245
+ name=name,
246
+ description=description,
247
+ stdin=stdin,
248
+ input=input,
249
+ content_format=content_format,
250
+ output=output,
251
+ assets=assets,
252
+ logs=logs,
253
+ statistics=statistics,
254
+ )
255
+
256
+ # Actually track the run.
257
+ in_progress(msg="Tracking run...")
258
+ run_id = cloud_app.track_run(
259
+ tracked_run=tracked_run,
260
+ instance_id=instance_id,
261
+ configuration=config,
262
+ )
263
+
264
+ print_json({"run_id": run_id})
265
+
266
+
267
+ def build_tracked_run_input(
268
+ status: TrackedRunStatus,
269
+ duration: int,
270
+ error_msg: str | None,
271
+ name: str | None,
272
+ description: str | None,
273
+ stdin: str | None,
274
+ input: str | None,
275
+ content_format: InputFormat,
276
+ output: str,
277
+ assets: str | None = None,
278
+ logs: str | None = None,
279
+ statistics: str | None = None,
280
+ ) -> TrackedRun:
281
+ """
282
+ Builds the tracked run input for tracking a run. Starts by creating a
283
+ TrackedRun object with the provided status, duration, error message, name,
284
+ and description. Then resolves the input and output using helper functions.
285
+ Finally, it reads the assets, logs, and statistics from the provided file
286
+ paths, if any, and assigns them to the TrackedRun object.
287
+
288
+ Parameters
289
+ ----------
290
+ status : TrackedRunStatus
291
+ The status of the tracked run.
292
+ duration : int
293
+ The duration of the tracked run in milliseconds.
294
+ error_msg : str | None
295
+ An error message if the run failed.
296
+ name : str | None
297
+ An optional name for the tracked run.
298
+ description : str | None
299
+ An optional description for the tracked run.
300
+ stdin : str | None
301
+ The input provided via stdin, if any.
302
+ input : str | None
303
+ The input file or directory path, if any.
304
+ content_format : InputFormat
305
+ The content format of the input (json or text).
306
+ output : str
307
+ The output file or directory path.
308
+ assets : str | None
309
+ The assets file path, if any.
310
+ logs : str | None
311
+ The logs file path, if any.
312
+ statistics : str | None
313
+ The statistics file path, if any.
314
+ """
315
+ tracked_run = TrackedRun(
316
+ status=TrackedRunStatus(status),
317
+ duration=duration,
318
+ error=error_msg,
319
+ name=name,
320
+ description=description,
321
+ )
322
+ tracked_run = resolve_input(
323
+ tracked_run=tracked_run,
324
+ stdin=stdin,
325
+ input=input,
326
+ content_format=content_format,
327
+ )
328
+ tracked_run = resolve_output(
329
+ tracked_run=tracked_run,
330
+ output=output,
331
+ content_format=content_format,
332
+ )
333
+
334
+ # Handle the assets, which should be a JSON file.
335
+ if assets is not None and assets != "":
336
+ try:
337
+ with open(assets) as f:
338
+ tracked_run.assets = json.load(f)
339
+ except json.JSONDecodeError as e:
340
+ error(f"Failed to parse assets file [magenta]{assets}[/magenta] as [magenta]json[/magenta]: {e}.")
341
+
342
+ # Handle the logs, which should be a text file.
343
+ if logs is not None and logs != "":
344
+ try:
345
+ log_content = Path(logs).read_text()
346
+ tracked_run.logs = log_content
347
+ except Exception as e:
348
+ error(f"Failed to read logs file [magenta]{logs}[/magenta]: {e}.")
349
+
350
+ # Handle the statistics, which should be a JSON file.
351
+ if statistics is not None and statistics != "":
352
+ try:
353
+ with open(statistics) as f:
354
+ tracked_run.statistics = json.load(f)
355
+ except json.JSONDecodeError as e:
356
+ error(f"Failed to parse statistics file [magenta]{statistics}[/magenta] as [magenta]json[/magenta]: {e}.")
357
+
358
+ return tracked_run
359
+
360
+
361
+ def resolve_input(
362
+ tracked_run: TrackedRun,
363
+ stdin: str | None,
364
+ input: str | None,
365
+ content_format: InputFormat,
366
+ ) -> TrackedRun:
367
+ """
368
+ Resolves the input for the tracked run, either from stdin or from a
369
+ file/directory.
370
+
371
+ Parameters
372
+ ----------
373
+ tracked_run : TrackedRun
374
+ The tracked run to set the input for.
375
+ stdin : str | None
376
+ The input provided via stdin, if any.
377
+ input : str | None
378
+ The input file or directory path, if any.
379
+ content_format : InputFormat
380
+ The content format of the input (json or text).
381
+
382
+ Returns
383
+ -------
384
+ TrackedRun
385
+ The tracked run with the resolved input.
386
+ """
387
+ if stdin is not None:
388
+ # Handle the case where stdin is provided as JSON for a JSON app.
389
+ try:
390
+ input_data = json.loads(stdin)
391
+ if content_format != InputFormat.JSON:
392
+ error(
393
+ "Input provided via [magenta]stdin[/magenta] is [magenta]json[/magenta], "
394
+ f"but the specified content format is {content_format.value}. "
395
+ "--content-format should be set to [magenta]json[/magenta]."
396
+ )
397
+
398
+ except json.JSONDecodeError:
399
+ input_data = stdin
400
+ if content_format != InputFormat.TEXT:
401
+ error(
402
+ "Input provided via [magenta]stdin[/magenta] is [magenta]text[/magenta], "
403
+ f"but the specified content format is {content_format.value}. "
404
+ "--content-format should be set to [magenta]text[/magenta]."
405
+ )
406
+
407
+ tracked_run.input = input_data
408
+
409
+ return tracked_run
410
+
411
+ # We know that input was defined because otherwise we would have failed
412
+ # early if both stdin and input were undefined.
413
+ input_path = Path(input)
414
+
415
+ if input_path.is_file():
416
+ if content_format == InputFormat.JSON:
417
+ try:
418
+ with input_path.open("r") as f:
419
+ input_data = json.load(f)
420
+
421
+ tracked_run.input = input_data
422
+
423
+ return tracked_run
424
+
425
+ except json.JSONDecodeError as e:
426
+ error(f"Failed to parse input file [magenta]{input}[/magenta] as [magenta]json[/magenta]: {e}.")
427
+
428
+ elif content_format == InputFormat.TEXT:
429
+ input_data = input_path.read_text()
430
+ tracked_run.input = input_data
431
+
432
+ return tracked_run
433
+
434
+ else:
435
+ error(f"Unsupported content format [magenta]{content_format.value}[/magenta] for file input.")
436
+
437
+ # If the input is a directory, we give the path directly to the run method.
438
+ # Internally, the files will be tarred and uploaded.
439
+ if input_path.is_dir():
440
+ tracked_run.input_dir_path = input
441
+
442
+ return tracked_run
443
+
444
+ error(f"Input path [magenta]{input}[/magenta] does not exist.")
445
+
446
+
447
+ def resolve_output(
448
+ tracked_run: TrackedRun,
449
+ output: str,
450
+ content_format: InputFormat,
451
+ ) -> TrackedRun:
452
+ """
453
+ Resolves the output for the tracked run.
454
+
455
+ Parameters
456
+ ----------
457
+ tracked_run : TrackedRun
458
+ The tracked run to set the output for.
459
+ output : str
460
+ The output file or directory path.
461
+ content_format : InputFormat
462
+ The content format of the output (json or text).
463
+
464
+ Returns
465
+ -------
466
+ TrackedRun
467
+ The tracked run with the resolved output.
468
+ """
469
+
470
+ output_path = Path(output)
471
+ if output_path.is_file():
472
+ if content_format == InputFormat.JSON:
473
+ try:
474
+ with output_path.open("r") as f:
475
+ output_data = json.load(f)
476
+
477
+ tracked_run.output = output_data
478
+
479
+ return tracked_run
480
+
481
+ except json.JSONDecodeError as e:
482
+ error(f"Failed to parse output file [magenta]{output}[/magenta] as [magenta]json[/magenta]: {e}.")
483
+
484
+ elif content_format == InputFormat.TEXT:
485
+ output_data = output_path.read_text()
486
+ tracked_run.output = output_data
487
+
488
+ return tracked_run
489
+
490
+ else:
491
+ error(f"Unsupported content type [magenta]{content_format.value}[/magenta] for file output.")
492
+
493
+ # If the output is a directory, we give the path directly to the run method.
494
+ # Internally, the files will be downloaded and extracted.
495
+ if output_path.is_dir():
496
+ tracked_run.output_dir_path = output
497
+
498
+ return tracked_run
499
+
500
+ error(f"Output path [magenta]{output}[/magenta] does not exist.")
@@ -0,0 +1,29 @@
1
+ """
2
+ This module defines the cloud scenario command tree for the Nextmv CLI.
3
+ """
4
+
5
+ import typer
6
+
7
+ from nextmv.cli.cloud.scenario.create import app as create_app
8
+ from nextmv.cli.cloud.scenario.delete import app as delete_app
9
+ from nextmv.cli.cloud.scenario.get import app as get_app
10
+ from nextmv.cli.cloud.scenario.list import app as list_app
11
+ from nextmv.cli.cloud.scenario.metadata import app as metadata_app
12
+ from nextmv.cli.cloud.scenario.update import app as update_app
13
+
14
+ # Set up subcommand application.
15
+ app = typer.Typer()
16
+ app.add_typer(create_app)
17
+ app.add_typer(delete_app)
18
+ app.add_typer(get_app)
19
+ app.add_typer(list_app)
20
+ app.add_typer(metadata_app)
21
+ app.add_typer(update_app)
22
+
23
+
24
+ @app.callback()
25
+ def callback() -> None:
26
+ """
27
+ Create and manage Nextmv Cloud scenario tests.
28
+ """
29
+ pass