nextmv 1.0.0.dev1__py3-none-any.whl → 1.0.0.dev3__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 (37) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/cli/cloud/__init__.py +4 -0
  3. nextmv/cli/cloud/batch/get.py +1 -1
  4. nextmv/cli/cloud/input_set/create.py +5 -3
  5. nextmv/cli/cloud/input_set/update.py +1 -1
  6. nextmv/cli/cloud/scenario/get.py +1 -1
  7. nextmv/cli/cloud/secrets/update.py +1 -1
  8. nextmv/cli/cloud/shadow/__init__.py +33 -0
  9. nextmv/cli/cloud/shadow/create.py +184 -0
  10. nextmv/cli/cloud/shadow/delete.py +68 -0
  11. nextmv/cli/cloud/shadow/get.py +61 -0
  12. nextmv/cli/cloud/shadow/list.py +63 -0
  13. nextmv/cli/cloud/shadow/metadata.py +66 -0
  14. nextmv/cli/cloud/shadow/start.py +43 -0
  15. nextmv/cli/cloud/shadow/stop.py +43 -0
  16. nextmv/cli/cloud/shadow/update.py +96 -0
  17. nextmv/cli/cloud/switchback/__init__.py +33 -0
  18. nextmv/cli/cloud/switchback/create.py +147 -0
  19. nextmv/cli/cloud/switchback/delete.py +68 -0
  20. nextmv/cli/cloud/switchback/get.py +62 -0
  21. nextmv/cli/cloud/switchback/list.py +63 -0
  22. nextmv/cli/cloud/switchback/metadata.py +68 -0
  23. nextmv/cli/cloud/switchback/start.py +43 -0
  24. nextmv/cli/cloud/switchback/stop.py +43 -0
  25. nextmv/cli/cloud/switchback/update.py +96 -0
  26. nextmv/cli/options.py +28 -0
  27. nextmv/cloud/__init__.py +10 -0
  28. nextmv/cloud/application/__init__.py +4 -0
  29. nextmv/cloud/application/_shadow.py +314 -0
  30. nextmv/cloud/application/_switchback.py +315 -0
  31. nextmv/cloud/shadow.py +190 -0
  32. nextmv/cloud/switchback.py +189 -0
  33. {nextmv-1.0.0.dev1.dist-info → nextmv-1.0.0.dev3.dist-info}/METADATA +1 -1
  34. {nextmv-1.0.0.dev1.dist-info → nextmv-1.0.0.dev3.dist-info}/RECORD +37 -15
  35. {nextmv-1.0.0.dev1.dist-info → nextmv-1.0.0.dev3.dist-info}/WHEEL +0 -0
  36. {nextmv-1.0.0.dev1.dist-info → nextmv-1.0.0.dev3.dist-info}/entry_points.txt +0 -0
  37. {nextmv-1.0.0.dev1.dist-info → nextmv-1.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,96 @@
1
+ """
2
+ This module defines the cloud switchback update command for the Nextmv CLI.
3
+ """
4
+
5
+ import json
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from nextmv.cli.configuration.config import build_app
11
+ from nextmv.cli.message import in_progress, print_json, success
12
+ from nextmv.cli.options import AppIDOption, ProfileOption, SwitchbackTestIDOption
13
+
14
+ # Set up subcommand application.
15
+ app = typer.Typer()
16
+
17
+
18
+ @app.command()
19
+ def update(
20
+ app_id: AppIDOption,
21
+ switchback_test_id: SwitchbackTestIDOption,
22
+ description: Annotated[
23
+ str | None,
24
+ typer.Option(
25
+ "--description",
26
+ "-d",
27
+ help="Updated description of the switchback test.",
28
+ metavar="DESCRIPTION",
29
+ ),
30
+ ] = None,
31
+ name: Annotated[
32
+ str | None,
33
+ typer.Option(
34
+ "--name",
35
+ "-n",
36
+ help="Updated name of the switchback test.",
37
+ metavar="NAME",
38
+ ),
39
+ ] = None,
40
+ output: Annotated[
41
+ str | None,
42
+ typer.Option(
43
+ "--output",
44
+ "-o",
45
+ help="Saves the updated switchback test information to this location.",
46
+ metavar="OUTPUT_PATH",
47
+ ),
48
+ ] = None,
49
+ profile: ProfileOption = None,
50
+ ) -> None:
51
+ """
52
+ Update a Nextmv Cloud switchback test.
53
+
54
+ Update the name and/or description of a switchback test. Any fields not
55
+ specified will remain unchanged.
56
+
57
+ [bold][underline]Examples[/underline][/bold]
58
+
59
+ - Update the name of a switchback test.
60
+ $ [green]nextmv cloud switchback update --app-id hare-app --switchback-test-id carrot-feast \\
61
+ --name "Spring Carrot Harvest"[/green]
62
+
63
+ - Update the description of a switchback test.
64
+ $ [green]nextmv cloud switchback update --app-id hare-app --switchback-test-id bunny-hop-routes \\
65
+ --description "Optimizing hop paths through the meadow"[/green]
66
+
67
+ - Update both name and description and save the result.
68
+ $ [green]nextmv cloud switchback update --app-id hare-app --switchback-test-id lettuce-delivery \\
69
+ --name "Warren Lettuce Express" --description "Fast lettuce delivery to all burrows" \\
70
+ --output updated-switchback-test.json[/green]
71
+ """
72
+
73
+ cloud_app = build_app(app_id=app_id, profile=profile)
74
+
75
+ in_progress(msg="Updating switchback test...")
76
+ switchback_test = cloud_app.update_switchback_test(
77
+ switchback_test_id=switchback_test_id,
78
+ name=name,
79
+ description=description,
80
+ )
81
+
82
+ switchback_test_dict = switchback_test.to_dict()
83
+ success(
84
+ f"Switchback test [magenta]{switchback_test_id}[/magenta] updated successfully "
85
+ f"in application [magenta]{app_id}[/magenta]."
86
+ )
87
+
88
+ if output is not None and output != "":
89
+ with open(output, "w") as f:
90
+ json.dump(switchback_test_dict, f, indent=2)
91
+
92
+ success(msg=f"Updated switchback test information saved to [magenta]{output}[/magenta].")
93
+
94
+ return
95
+
96
+ print_json(switchback_test_dict)
nextmv/cli/options.py CHANGED
@@ -190,3 +190,31 @@ SecretsCollectionIDOption = Annotated[
190
190
  metavar="SECRETS_COLLECTION_ID",
191
191
  ),
192
192
  ]
193
+
194
+ # shadow_test_id option - can be used in any command that requires a shadow test ID.
195
+ # Define it as follows in commands or callbacks, as necessary:
196
+ # shadow_test_id: ShadowTestIDOption
197
+ ShadowTestIDOption = Annotated[
198
+ str,
199
+ typer.Option(
200
+ "--shadow-test-id",
201
+ "-s",
202
+ help="The Nextmv Cloud shadow test ID to use for this action.",
203
+ envvar="NEXTMV_SHADOW_TEST_ID",
204
+ metavar="SHADOW_TEST_ID",
205
+ ),
206
+ ]
207
+
208
+ # switchback_test_id option - can be used in any command that requires a switchback test ID.
209
+ # Define it as follows in commands or callbacks, as necessary:
210
+ # switchback_test_id: SwitchbackTestIDOption
211
+ SwitchbackTestIDOption = Annotated[
212
+ str,
213
+ typer.Option(
214
+ "--switchback-test-id",
215
+ "-s",
216
+ help="The Nextmv Cloud switchback test ID to use for this action.",
217
+ envvar="NEXTMV_SWITCHBACK_TEST_ID",
218
+ metavar="SWITCHBACK_TEST_ID",
219
+ ),
220
+ ]
nextmv/cloud/__init__.py CHANGED
@@ -90,6 +90,16 @@ from .secrets import Secret as Secret
90
90
  from .secrets import SecretsCollection as SecretsCollection
91
91
  from .secrets import SecretsCollectionSummary as SecretsCollectionSummary
92
92
  from .secrets import SecretType as SecretType
93
+ from .shadow import ShadowTest as ShadowTest
94
+ from .shadow import ShadowTestMetadata as ShadowTestMetadata
95
+ from .shadow import StartEvents as StartEvents
96
+ from .shadow import TerminationEvents as TerminationEvents
97
+ from .shadow import TestComparison as TestComparison
98
+ from .switchback import SwitchbackPlan as SwitchbackPlan
99
+ from .switchback import SwitchbackPlanUnit as SwitchbackPlanUnit
100
+ from .switchback import SwitchbackTest as SwitchbackTest
101
+ from .switchback import SwitchbackTestMetadata as SwitchbackTestMetadata
102
+ from .switchback import TestComparisonSingle as TestComparisonSingle
93
103
  from .url import DownloadURL as DownloadURL
94
104
  from .url import UploadURL as UploadURL
95
105
  from .version import Version as Version
@@ -41,6 +41,8 @@ from nextmv.cloud.application._instance import ApplicationInstanceMixin
41
41
  from nextmv.cloud.application._managed_input import ApplicationManagedInputMixin
42
42
  from nextmv.cloud.application._run import ApplicationRunMixin
43
43
  from nextmv.cloud.application._secrets import ApplicationSecretsMixin
44
+ from nextmv.cloud.application._shadow import ApplicationShadowMixin
45
+ from nextmv.cloud.application._switchback import ApplicationSwitchbackMixin
44
46
  from nextmv.cloud.application._utils import _is_not_exist_error
45
47
  from nextmv.cloud.application._version import ApplicationVersionMixin
46
48
  from nextmv.cloud.client import Client
@@ -100,6 +102,8 @@ class Application(
100
102
  ApplicationVersionMixin,
101
103
  ApplicationInputSetMixin,
102
104
  ApplicationManagedInputMixin,
105
+ ApplicationShadowMixin,
106
+ ApplicationSwitchbackMixin,
103
107
  ):
104
108
  """
105
109
  A published decision model that can be executed.
@@ -0,0 +1,314 @@
1
+ """
2
+ Application mixin for managing shadow tests.
3
+ """
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from nextmv.cloud.shadow import ShadowTest, ShadowTestMetadata, StartEvents, TerminationEvents
8
+ from nextmv.run import Run
9
+ from nextmv.safe import safe_id
10
+
11
+ if TYPE_CHECKING:
12
+ from . import Application
13
+
14
+
15
+ class ApplicationShadowMixin:
16
+ """
17
+ Mixin class for managing shadow tests within an application.
18
+ """
19
+
20
+ def shadow_test(self: "Application", shadow_test_id: str) -> ShadowTest:
21
+ """
22
+ Get a shadow test. This method also returns the runs of the shadow
23
+ test under the `.runs` attribute.
24
+
25
+ Parameters
26
+ ----------
27
+ shadow_test_id : str
28
+ ID of the shadow test.
29
+
30
+ Returns
31
+ -------
32
+ ShadowTest
33
+ The requested shadow test details.
34
+
35
+ Raises
36
+ ------
37
+ requests.HTTPError
38
+ If the response status code is not 2xx.
39
+
40
+ Examples
41
+ --------
42
+ >>> shadow_test = app.shadow_test("shadow-123")
43
+ >>> print(shadow_test.name)
44
+ 'My Shadow Test'
45
+ """
46
+
47
+ response = self.client.request(
48
+ method="GET",
49
+ endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}",
50
+ )
51
+
52
+ exp = ShadowTest.from_dict(response.json())
53
+
54
+ runs_response = self.client.request(
55
+ method="GET",
56
+ endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/runs",
57
+ )
58
+
59
+ runs = [Run.from_dict(run) for run in runs_response.json().get("runs", [])]
60
+ exp.runs = runs
61
+
62
+ return exp
63
+
64
+ def shadow_test_metadata(self: "Application", shadow_test_id: str) -> ShadowTestMetadata:
65
+ """
66
+ Get metadata for a shadow test.
67
+
68
+ Parameters
69
+ ----------
70
+ shadow_test_id : str
71
+ ID of the shadow test.
72
+
73
+ Returns
74
+ -------
75
+ ShadowTestMetadata
76
+ The requested shadow test metadata.
77
+
78
+ Raises
79
+ ------
80
+ requests.HTTPError
81
+ If the response status code is not 2xx.
82
+
83
+ Examples
84
+ --------
85
+ >>> metadata = app.shadow_test_metadata("shadow-123")
86
+ >>> print(metadata.name)
87
+ 'My Shadow Test'
88
+ """
89
+
90
+ response = self.client.request(
91
+ method="GET",
92
+ endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/metadata",
93
+ )
94
+
95
+ return ShadowTestMetadata.from_dict(response.json())
96
+
97
+ def delete_shadow_test(self: "Application", shadow_test_id: str) -> None:
98
+ """
99
+ Delete a shadow test.
100
+
101
+ Deletes a shadow test along with all the associated information,
102
+ such as its runs.
103
+
104
+ Parameters
105
+ ----------
106
+ shadow_test_id : str
107
+ ID of the shadow test to delete.
108
+
109
+ Raises
110
+ ------
111
+ requests.HTTPError
112
+ If the response status code is not 2xx.
113
+
114
+ Examples
115
+ --------
116
+ >>> app.delete_shadow_test("shadow-123")
117
+ """
118
+
119
+ _ = self.client.request(
120
+ method="DELETE",
121
+ endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}",
122
+ )
123
+
124
+ def list_shadow_tests(self: "Application") -> list[ShadowTest]:
125
+ """
126
+ List all shadow tests.
127
+
128
+ Returns
129
+ -------
130
+ list[ShadowTest]
131
+ List of shadow tests.
132
+
133
+ Raises
134
+ ------
135
+ requests.HTTPError
136
+ If the response status code is not 2xx.
137
+ """
138
+
139
+ response = self.client.request(
140
+ method="GET",
141
+ endpoint=f"{self.experiments_endpoint}/shadow",
142
+ )
143
+
144
+ return [ShadowTest.from_dict(shadow_test) for shadow_test in response.json()]
145
+
146
+ def new_shadow_test(
147
+ self: "Application",
148
+ comparisons: dict[str, list[str]],
149
+ termination_events: TerminationEvents,
150
+ shadow_test_id: str | None = None,
151
+ name: str | None = None,
152
+ description: str | None = None,
153
+ start_events: StartEvents | None = None,
154
+ ) -> ShadowTest:
155
+ """
156
+ Create a new shadow test in draft mode. Shadow tests are experiments
157
+ that run instances in parallel to compare their results.
158
+
159
+ Use the `comparisons` parameter to define how to set up instance
160
+ comparisons. The keys of the `comparisons` dictionary are the baseline
161
+ instance IDs, and the values are the candidate lists of instance IDs to
162
+ compare against the respective baseline.
163
+
164
+ You may specify `start_events` to make the shadow test start at a
165
+ specific time. Alternatively, you may use the `start_shadow_test`
166
+ method to start the test.
167
+
168
+ The `termination_events` parameter is required and provides control
169
+ over when the shadow test should terminate. Alternatively, you may use
170
+ the `stop_shadow_test` method to stop the test.
171
+
172
+ Parameters
173
+ ----------
174
+ comparisons : dict[str, list[str]]
175
+ Dictionary defining the baseline and candidate instance IDs for
176
+ comparison. The keys are baseline instance IDs, and the values are
177
+ lists of candidate instance IDs to compare against the respective
178
+ baseline.
179
+ termination_events : TerminationEvents
180
+ Termination events for the shadow test.
181
+ shadow_test_id : Optional[str]
182
+ ID of the shadow test. Will be generated if not provided.
183
+ name : Optional[str]
184
+ Name of the shadow test. If not provided, the ID will be used as
185
+ the name.
186
+ description : Optional[str]
187
+ Optional description of the shadow test.
188
+ start_events : Optional[StartEvents]
189
+ Start events for the shadow test.
190
+
191
+ Returns
192
+ -------
193
+ ShadowTest
194
+ The created shadow test.
195
+
196
+ Raises
197
+ ------
198
+ requests.HTTPError
199
+ If the response status code is not 2xx.
200
+ """
201
+
202
+ # Generate ID if not provided
203
+ if shadow_test_id is None:
204
+ shadow_test_id = safe_id("shadow")
205
+
206
+ # Use ID as name if name not provided
207
+ if name is None:
208
+ name = shadow_test_id
209
+
210
+ payload = {
211
+ "id": id,
212
+ "name": name,
213
+ "comparisons": comparisons,
214
+ "termination_events": termination_events.to_dict(),
215
+ }
216
+
217
+ if description is not None:
218
+ payload["description"] = description
219
+ if start_events is not None:
220
+ payload["start_events"] = start_events.to_dict()
221
+
222
+ response = self.client.request(
223
+ method="POST",
224
+ endpoint=f"{self.experiments_endpoint}/shadow",
225
+ payload=payload,
226
+ )
227
+
228
+ return ShadowTest.from_dict(response.json())
229
+
230
+ def start_shadow_test(self: "Application", shadow_test_id: str) -> None:
231
+ """
232
+ Start a shadow test. Create a shadow test in draft mode using the
233
+ `new_shadow_test` method, then use this method to start the test.
234
+
235
+ Parameters
236
+ ----------
237
+ shadow_test_id : str
238
+ ID of the shadow test to start.
239
+
240
+ Raises
241
+ ------
242
+ requests.HTTPError
243
+ If the response status code is not 2xx.
244
+ """
245
+
246
+ _ = self.client.request(
247
+ method="PUT",
248
+ endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/start",
249
+ )
250
+
251
+ def stop_shadow_test(self: "Application", shadow_test_id: str) -> None:
252
+ """
253
+ Stop a shadow test. The test should already have started before using
254
+ this method.
255
+
256
+ Parameters
257
+ ----------
258
+ shadow_test_id : str
259
+ ID of the shadow test to stop.
260
+
261
+ Raises
262
+ ------
263
+ requests.HTTPError
264
+ If the response status code is not 2xx.
265
+ """
266
+
267
+ _ = self.client.request(
268
+ method="PUT",
269
+ endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/stop",
270
+ )
271
+
272
+ def update_shadow_test(
273
+ self: "Application",
274
+ shadow_test_id: str,
275
+ name: str | None = None,
276
+ description: str | None = None,
277
+ ) -> ShadowTest:
278
+ """
279
+ Update a shadow test.
280
+
281
+ Parameters
282
+ ----------
283
+ shadow_test_id : str
284
+ ID of the shadow test to update.
285
+ name : Optional[str], default=None
286
+ Optional name of the shadow test.
287
+ description : Optional[str], default=None
288
+ Optional description of the shadow test.
289
+
290
+ Returns
291
+ -------
292
+ ShadowTest
293
+ The information with the updated shadow test.
294
+
295
+ Raises
296
+ ------
297
+ requests.HTTPError
298
+ If the response status code is not 2xx.
299
+ """
300
+
301
+ payload = {}
302
+
303
+ if name is not None:
304
+ payload["name"] = name
305
+ if description is not None:
306
+ payload["description"] = description
307
+
308
+ response = self.client.request(
309
+ method="PATCH",
310
+ endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}",
311
+ payload=payload,
312
+ )
313
+
314
+ return ShadowTest.from_dict(response.json())