nextmv 1.0.0.dev2__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.
@@ -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
@@ -204,3 +204,17 @@ ShadowTestIDOption = Annotated[
204
204
  metavar="SHADOW_TEST_ID",
205
205
  ),
206
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
@@ -95,6 +95,11 @@ from .shadow import ShadowTestMetadata as ShadowTestMetadata
95
95
  from .shadow import StartEvents as StartEvents
96
96
  from .shadow import TerminationEvents as TerminationEvents
97
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
98
103
  from .url import DownloadURL as DownloadURL
99
104
  from .url import UploadURL as UploadURL
100
105
  from .version import Version as Version
@@ -42,6 +42,7 @@ 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
44
  from nextmv.cloud.application._shadow import ApplicationShadowMixin
45
+ from nextmv.cloud.application._switchback import ApplicationSwitchbackMixin
45
46
  from nextmv.cloud.application._utils import _is_not_exist_error
46
47
  from nextmv.cloud.application._version import ApplicationVersionMixin
47
48
  from nextmv.cloud.client import Client
@@ -102,6 +103,7 @@ class Application(
102
103
  ApplicationInputSetMixin,
103
104
  ApplicationManagedInputMixin,
104
105
  ApplicationShadowMixin,
106
+ ApplicationSwitchbackMixin,
105
107
  ):
106
108
  """
107
109
  A published decision model that can be executed.
@@ -0,0 +1,315 @@
1
+ """
2
+ Application mixin for managing switchback tests.
3
+ """
4
+
5
+ from datetime import datetime
6
+ from typing import TYPE_CHECKING
7
+
8
+ from nextmv.cloud.switchback import SwitchbackTest, SwitchbackTestMetadata, TestComparisonSingle
9
+ from nextmv.run import Run
10
+ from nextmv.safe import safe_id
11
+
12
+ if TYPE_CHECKING:
13
+ from . import Application
14
+
15
+
16
+ class ApplicationSwitchbackMixin:
17
+ """
18
+ Mixin class for managing switchback tests within an application.
19
+ """
20
+
21
+ def switchback_test(self: "Application", switchback_test_id: str) -> SwitchbackTest:
22
+ """
23
+ Get a switchback test. This method also returns the runs of the switchback
24
+ test under the `.runs` attribute.
25
+
26
+ Parameters
27
+ ----------
28
+ switchback_test_id : str
29
+ ID of the switchback test.
30
+
31
+ Returns
32
+ -------
33
+ SwitchbackTest
34
+ The requested switchback test details.
35
+
36
+ Raises
37
+ ------
38
+ requests.HTTPError
39
+ If the response status code is not 2xx.
40
+
41
+ Examples
42
+ --------
43
+ >>> switchback_test = app.switchback_test("switchback-123")
44
+ >>> print(switchback_test.name)
45
+ 'My Switchback Test'
46
+ """
47
+
48
+ response = self.client.request(
49
+ method="GET",
50
+ endpoint=f"{self.experiments_endpoint}/switchback/{switchback_test_id}",
51
+ )
52
+
53
+ exp = SwitchbackTest.from_dict(response.json())
54
+
55
+ runs_response = self.client.request(
56
+ method="GET",
57
+ endpoint=f"{self.experiments_endpoint}/switchback/{switchback_test_id}/runs",
58
+ )
59
+
60
+ runs = [Run.from_dict(run) for run in runs_response.json().get("runs", [])]
61
+ exp.runs = runs
62
+
63
+ return exp
64
+
65
+ def switchback_test_metadata(self: "Application", switchback_test_id: str) -> SwitchbackTestMetadata:
66
+ """
67
+ Get metadata for a switchback test.
68
+
69
+ Parameters
70
+ ----------
71
+ switchback_test_id : str
72
+ ID of the switchback test.
73
+
74
+ Returns
75
+ -------
76
+ SwitchbackTestMetadata
77
+ The requested switchback test metadata.
78
+
79
+ Raises
80
+ ------
81
+ requests.HTTPError
82
+ If the response status code is not 2xx.
83
+
84
+ Examples
85
+ --------
86
+ >>> metadata = app.switchback_test_metadata("switchback-123")
87
+ >>> print(metadata.name)
88
+ 'My Switchback Test'
89
+ """
90
+
91
+ response = self.client.request(
92
+ method="GET",
93
+ endpoint=f"{self.experiments_endpoint}/switchback/{switchback_test_id}/metadata",
94
+ )
95
+
96
+ return SwitchbackTestMetadata.from_dict(response.json())
97
+
98
+ def delete_switchback_test(self: "Application", switchback_test_id: str) -> None:
99
+ """
100
+ Delete a switchback test.
101
+
102
+ Deletes a switchback test along with all the associated information,
103
+ such as its runs.
104
+
105
+ Parameters
106
+ ----------
107
+ switchback_test_id : str
108
+ ID of the switchback test to delete.
109
+
110
+ Raises
111
+ ------
112
+ requests.HTTPError
113
+ If the response status code is not 2xx.
114
+
115
+ Examples
116
+ --------
117
+ >>> app.delete_switchback_test("switchback-123")
118
+ """
119
+
120
+ _ = self.client.request(
121
+ method="DELETE",
122
+ endpoint=f"{self.experiments_endpoint}/switchback/{switchback_test_id}",
123
+ )
124
+
125
+ def list_switchback_tests(self: "Application") -> list[SwitchbackTest]:
126
+ """
127
+ List all switchback tests.
128
+
129
+ Returns
130
+ -------
131
+ list[SwitchbackTest]
132
+ List of switchback tests.
133
+
134
+ Raises
135
+ ------
136
+ requests.HTTPError
137
+ If the response status code is not 2xx.
138
+ """
139
+
140
+ response = self.client.request(
141
+ method="GET",
142
+ endpoint=f"{self.experiments_endpoint}/switchback",
143
+ )
144
+
145
+ return [SwitchbackTest.from_dict(switchback_test) for switchback_test in response.json().get("items", [])]
146
+
147
+ def new_switchback_test(
148
+ self: "Application",
149
+ comparison: TestComparisonSingle,
150
+ unit_duration_minutes: float,
151
+ units: int,
152
+ switchback_test_id: str | None = None,
153
+ name: str | None = None,
154
+ description: str | None = None,
155
+ start: datetime | None = None,
156
+ ) -> SwitchbackTest:
157
+ """
158
+ Create a new switchback test in draft mode. Switchback tests are
159
+ experiments that alternate between different instances over specified
160
+ time intervals.
161
+
162
+ Use the `comparison` parameter to define how to set up the instance
163
+ comparison. The test will alternate between the baseline and candidate
164
+ instances defined in the comparison.
165
+
166
+ You may specify `start` to make the switchback test start at a
167
+ specific time. Alternatively, you may use the `start_switchback_test`
168
+ method to start the test.
169
+
170
+ Parameters
171
+ ----------
172
+ comparison : TestComparisonSingle
173
+ Comparison defining the baseline and candidate instances.
174
+ unit_duration_minutes : float
175
+ Duration of each interval in minutes.
176
+ units : int
177
+ Total number of intervals in the switchback test.
178
+ switchback_test_id : Optional[str], default=None
179
+ Optional ID for the switchback test. Will be generated if not
180
+ provided.
181
+ name : Optional[str], default=None
182
+ Optional name of the switchback test. If not provided, the ID will
183
+ be used as the name.
184
+ description : Optional[str], default=None
185
+ Optional description of the switchback test.
186
+ start : Optional[datetime], default=None
187
+ Optional scheduled start time for the switchback test.
188
+
189
+ Returns
190
+ -------
191
+ SwitchbackTest
192
+ The created switchback test.
193
+
194
+ Raises
195
+ ------
196
+ requests.HTTPError
197
+ If the response status code is not 2xx.
198
+ """
199
+
200
+ # Generate ID if not provided
201
+ if switchback_test_id is None:
202
+ switchback_test_id = safe_id("switchback")
203
+
204
+ # Use ID as name if name not provided
205
+ if name is None:
206
+ name = switchback_test_id
207
+
208
+ payload = {
209
+ "id": id,
210
+ "name": name,
211
+ "comparison": comparison,
212
+ "generate_random_plan": {
213
+ "unit_duration_minutes": unit_duration_minutes,
214
+ "units": units,
215
+ },
216
+ }
217
+
218
+ if description is not None:
219
+ payload["description"] = description
220
+ if start is not None:
221
+ payload["generate_random_plan"]["start"] = start.isoformat()
222
+
223
+ response = self.client.request(
224
+ method="POST",
225
+ endpoint=f"{self.experiments_endpoint}/switchback",
226
+ payload=payload,
227
+ )
228
+
229
+ return SwitchbackTest.from_dict(response.json())
230
+
231
+ def start_switchback_test(self: "Application", switchback_test_id: str) -> None:
232
+ """
233
+ Start a switchback test. Create a switchback test in draft mode using the
234
+ `new_switchback_test` method, then use this method to start the test.
235
+
236
+ Parameters
237
+ ----------
238
+ switchback_test_id : str
239
+ ID of the switchback test to start.
240
+
241
+ Raises
242
+ ------
243
+ requests.HTTPError
244
+ If the response status code is not 2xx.
245
+ """
246
+
247
+ _ = self.client.request(
248
+ method="PUT",
249
+ endpoint=f"{self.experiments_endpoint}/switchback/{switchback_test_id}/start",
250
+ )
251
+
252
+ def stop_switchback_test(self: "Application", switchback_test_id: str) -> None:
253
+ """
254
+ Stop a switchback test. The test should already have started before using
255
+ this method.
256
+
257
+ Parameters
258
+ ----------
259
+ switchback_test_id : str
260
+ ID of the switchback test to stop.
261
+
262
+ Raises
263
+ ------
264
+ requests.HTTPError
265
+ If the response status code is not 2xx.
266
+ """
267
+
268
+ _ = self.client.request(
269
+ method="PUT",
270
+ endpoint=f"{self.experiments_endpoint}/switchback/{switchback_test_id}/stop",
271
+ )
272
+
273
+ def update_switchback_test(
274
+ self: "Application",
275
+ switchback_test_id: str,
276
+ name: str | None = None,
277
+ description: str | None = None,
278
+ ) -> SwitchbackTest:
279
+ """
280
+ Update a switchback test.
281
+
282
+ Parameters
283
+ ----------
284
+ switchback_test_id : str
285
+ ID of the switchback test to update.
286
+ name : Optional[str], default=None
287
+ Optional name of the switchback test.
288
+ description : Optional[str], default=None
289
+ Optional description of the switchback test.
290
+
291
+ Returns
292
+ -------
293
+ SwitchbackTest
294
+ The information with the updated switchback test.
295
+
296
+ Raises
297
+ ------
298
+ requests.HTTPError
299
+ If the response status code is not 2xx.
300
+ """
301
+
302
+ payload = {}
303
+
304
+ if name is not None:
305
+ payload["name"] = name
306
+ if description is not None:
307
+ payload["description"] = description
308
+
309
+ response = self.client.request(
310
+ method="PATCH",
311
+ endpoint=f"{self.experiments_endpoint}/switchback/{switchback_test_id}",
312
+ payload=payload,
313
+ )
314
+
315
+ return SwitchbackTest.from_dict(response.json())
@@ -0,0 +1,189 @@
1
+ """
2
+ Classes for working with Nextmv Cloud switchback tests.
3
+
4
+ This module provides classes for interacting with switchback tests in Nextmv Cloud.
5
+ It details the core data structures for these types of experiments.
6
+
7
+ Classes
8
+ -------
9
+ TestComparisonSingle
10
+ A structure to define a single comparison for tests.
11
+ SwitchbackPlanUnit
12
+ A structure to define a single unit in the switchback plan.
13
+ SwitchbackPlan
14
+ A structure to define the switchback plan for tests.
15
+ SwitchbackTestMetadata
16
+ Metadata for a Nextmv Cloud switchback test.
17
+ SwitchbackTest
18
+ A Nextmv Cloud switchback test definition.
19
+ """
20
+
21
+ from datetime import datetime
22
+
23
+ from pydantic import AliasChoices, Field
24
+
25
+ from nextmv.base_model import BaseModel
26
+ from nextmv.cloud.batch_experiment import ExperimentStatus
27
+ from nextmv.run import Run
28
+
29
+
30
+ class TestComparisonSingle(BaseModel):
31
+ """
32
+ A structure to define a single comparison for tests.
33
+
34
+ You can import the `TestComparisonSingle` class directly from `cloud`:
35
+
36
+ ```python
37
+ from nextmv.cloud import TestComparisonSingle
38
+ ```
39
+
40
+ Parameters
41
+ ----------
42
+ baseline_instance_id : str
43
+ ID of the baseline instance for comparison.
44
+ candidate_instance_id : str
45
+ ID of the candidate instance for comparison.
46
+ """
47
+
48
+ baseline_instance_id: str
49
+ """ID of the baseline instance for comparison."""
50
+ candidate_instance_id: str
51
+ """ID of the candidate instance for comparison."""
52
+
53
+
54
+ class SwitchbackPlanUnit(BaseModel):
55
+ """
56
+ A structure to define a single unit in the switchback plan.
57
+
58
+ You can import the `SwitchbackPlanUnit` class directly from `cloud`:
59
+
60
+ ```python
61
+ from nextmv.cloud import SwitchbackPlanUnit
62
+ ```
63
+
64
+ Parameters
65
+ ----------
66
+ duration_minutes : float
67
+ Duration of this interval in minutes.
68
+ instance_id : str
69
+ ID of the instance to run during this unit.
70
+ index : int
71
+ Index of this unit in the switchback plan.
72
+ """
73
+
74
+ duration_minutes: float
75
+ """Duration of this interval in minutes."""
76
+ instance_id: str
77
+ """ID of the instance to run during this unit."""
78
+ index: int
79
+ """Index of this unit in the switchback plan."""
80
+
81
+
82
+ class SwitchbackPlan(BaseModel):
83
+ """
84
+ A structure to define the switchback plan for tests.
85
+
86
+ You can import the `SwitchbackPlan` class directly from `cloud`:
87
+
88
+ ```python
89
+ from nextmv.cloud import SwitchbackPlan
90
+ ```
91
+
92
+ Parameters
93
+ ----------
94
+ interval_duration_seconds : int
95
+ Duration of each interval in seconds.
96
+ total_intervals : int
97
+ Total number of intervals in the switchback test.
98
+ """
99
+
100
+ start: datetime | None = None
101
+ """Start time of the switchback test."""
102
+ units: list[SwitchbackPlanUnit] | None = None
103
+ """List of switchback plan units."""
104
+
105
+
106
+ class SwitchbackTestMetadata(BaseModel):
107
+ """
108
+ Metadata for a Nextmv Cloud switchback test.
109
+
110
+ You can import the `SwitchbackTestMetadata` class directly from `cloud`:
111
+
112
+ ```python
113
+ from nextmv.cloud import SwitchbackTestMetadata
114
+ ```
115
+
116
+ Parameters
117
+ ----------
118
+ switchback_test_id : str, optional
119
+ The unique identifier of the switchback test.
120
+ name : str, optional
121
+ Name of the switchback test.
122
+ description : str, optional
123
+ Description of the switchback test.
124
+ app_id : str, optional
125
+ ID of the application to which the switchback test belongs.
126
+ created_at : datetime, optional
127
+ Creation date of the switchback test.
128
+ updated_at : datetime, optional
129
+ Last update date of the switchback test.
130
+ status : ExperimentStatus, optional
131
+ The current status of the switchback test.
132
+ """
133
+
134
+ switchback_test_id: str | None = Field(
135
+ serialization_alias="id",
136
+ validation_alias=AliasChoices("id", "switchback_test_id"),
137
+ default=None,
138
+ )
139
+ """The unique identifier of the switchback test."""
140
+ name: str | None = None
141
+ """Name of the switchback test."""
142
+ description: str | None = None
143
+ """Description of the switchback test."""
144
+ app_id: str | None = None
145
+ """ID of the application to which the switchback test belongs."""
146
+ created_at: datetime | None = None
147
+ """Creation date of the switchback test."""
148
+ updated_at: datetime | None = None
149
+ """Last update date of the switchback test."""
150
+ status: ExperimentStatus | None = None
151
+ """The current status of the switchback test."""
152
+
153
+
154
+ class SwitchbackTest(SwitchbackTestMetadata):
155
+ """
156
+ A Nextmv Cloud switchback test definition.
157
+
158
+ A switchback test is a type of experiment where runs are executed in
159
+ sequential intervals, alternating between different instances to compare
160
+ their performance.
161
+
162
+ You can import the `SwitchbackTest` class directly from `cloud`:
163
+
164
+ ```python
165
+ from nextmv.cloud import SwitchbackTest
166
+ ```
167
+
168
+ Parameters
169
+ ----------
170
+ completed_at : datetime, optional
171
+ Completion date of the switchback test, if applicable.
172
+ comparison : TestComparisonSingle, optional
173
+ Test comparison defined in the switchback test.
174
+ start_events : StartEvents, optional
175
+ Start events for the switchback test.
176
+ termination_events : TerminationEvents, optional
177
+ Termination events for the switchback test.
178
+ """
179
+
180
+ started_at: datetime | None = None
181
+ """Start date of the switchback test, if applicable."""
182
+ completed_at: datetime | None = None
183
+ """Completion date of the switchback test, if applicable."""
184
+ comparison: TestComparisonSingle | None = None
185
+ """Test comparison defined in the switchback test."""
186
+ plan: SwitchbackPlan | None = None
187
+ """Switchback plan defining the intervals and instance switching."""
188
+ runs: list[Run] | None = None
189
+ """List of runs in the switchback test."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextmv
3
- Version: 1.0.0.dev2
3
+ Version: 1.0.0.dev3
4
4
  Summary: The all-purpose Python SDK for Nextmv
5
5
  Project-URL: Homepage, https://www.nextmv.io
6
6
  Project-URL: Documentation, https://nextmv-py.docs.nextmv.io/en/latest/nextmv/