nextmv 1.0.0.dev6__py3-none-any.whl → 1.0.0.dev8__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.
@@ -2,18 +2,18 @@
2
2
  This module defines the community list command for the Nextmv CLI.
3
3
  """
4
4
 
5
- from typing import Annotated
5
+ from typing import Annotated, Any
6
6
 
7
+ import requests
7
8
  import rich
8
9
  import typer
10
+ import yaml
9
11
  from rich.console import Console
10
12
  from rich.table import Table
11
13
 
12
14
  from nextmv.cli.configuration.config import build_client
13
15
  from nextmv.cli.message import error
14
16
  from nextmv.cli.options import ProfileOption
15
- from nextmv.cloud.client import Client
16
- from nextmv.cloud.community import CommunityApp, list_community_apps
17
17
 
18
18
  # Set up subcommand application.
19
19
  app = typer.Typer()
@@ -62,73 +62,96 @@ def list(
62
62
  if app is not None and app == "":
63
63
  error("The --app flag cannot be an empty string.")
64
64
 
65
- client = build_client(profile)
65
+ manifest = download_manifest(profile=profile)
66
66
  if flat and app is None:
67
- _apps_list(client)
67
+ apps_list(manifest)
68
68
  raise typer.Exit()
69
69
  elif not flat and app is None:
70
- _apps_table(client)
70
+ apps_table(manifest)
71
71
  raise typer.Exit()
72
72
  elif flat and app is not None and app != "":
73
- _versions_list(client, app)
73
+ versions_list(manifest, app)
74
74
  raise typer.Exit()
75
75
  elif not flat and app is not None and app != "":
76
- _versions_table(client, app)
76
+ versions_table(manifest, app)
77
77
  raise typer.Exit()
78
78
 
79
79
 
80
- def _apps_table(client: Client) -> None:
80
+ def download_manifest(profile: str | None = None) -> dict:
81
81
  """
82
- This function prints a table of community apps.
82
+ Downloads and returns the community apps manifest.
83
83
 
84
84
  Parameters
85
85
  ----------
86
- client : Client
87
- The Nextmv Cloud client to use for the request.
86
+ profile : str | None
87
+ The profile name to use. If None, the default profile is used.
88
+
89
+ Returns
90
+ -------
91
+ dict
92
+ The community apps manifest as a dictionary.
93
+
94
+ Raises
95
+ requests.HTTPError
96
+ If the response status code is not 2xx.
97
+ """
98
+
99
+ response = download_file(directory="community-apps", file="manifest.yml", profile=profile)
100
+ manifest = yaml.safe_load(response.text)
101
+
102
+ return manifest
103
+
104
+
105
+ def apps_table(manifest: dict[str, Any]) -> None:
106
+ """
107
+ This function prints a table of community apps from the manifest.
108
+
109
+ Parameters
110
+ ----------
111
+ manifest : dict[str, Any]
112
+ The community apps manifest.
88
113
  """
89
114
 
90
- apps = list_community_apps(client)
91
115
  table = Table("Name", "Type", "Latest", "Description", border_style="cyan", header_style="cyan")
92
- for app in apps:
116
+ for app in manifest.get("apps", []):
93
117
  table.add_row(
94
- app.name,
95
- app.app_type,
96
- app.latest_app_version if app.latest_app_version is not None else "",
97
- app.description,
118
+ app.get("name", ""),
119
+ app.get("type", ""),
120
+ app.get("latest_app_version", ""),
121
+ app.get("description", ""),
98
122
  )
99
123
 
100
124
  console.print(table)
101
125
 
102
126
 
103
- def _apps_list(client: Client) -> None:
127
+ def apps_list(manifest: dict[str, Any]) -> None:
104
128
  """
105
- This function prints a flat list of community app names.
129
+ This function prints a flat list of community app names from the manifest.
106
130
 
107
131
  Parameters
108
132
  ----------
109
- client : Client
110
- The Nextmv Cloud client to use for the request.
133
+ manifest : dict[str, Any]
134
+ The community apps manifest.
111
135
  """
112
136
 
113
- apps = list_community_apps(client)
114
- names = [app.name for app in apps]
137
+ names = [app.get("name", "") for app in manifest.get("apps", [])]
115
138
  print("\n".join(names))
116
139
 
117
140
 
118
- def _versions_table(client: Client, app: str) -> None:
141
+ def versions_table(manifest: dict[str, Any], app: str) -> None:
119
142
  """
120
143
  This function prints a table of versions for a specific community app.
121
144
 
122
145
  Parameters
123
146
  ----------
124
- client : Client
125
- The Nextmv Cloud client to use for the request.
147
+ manifest : dict[str, Any]
148
+ The community apps manifest.
126
149
  app : str
127
150
  The name of the community app.
128
151
  """
129
152
 
130
- comm_app = _find_app(client, app)
131
- latest_version = comm_app.latest_app_version if comm_app.latest_app_version is not None else ""
153
+ app_obj = find_app(manifest, app)
154
+ latest_version = app_obj.get("latest_app_version", "")
132
155
 
133
156
  # Add the latest version with indicator
134
157
  table = Table("Version", "Latest?", border_style="cyan", header_style="cyan")
@@ -136,7 +159,7 @@ def _versions_table(client: Client, app: str) -> None:
136
159
  table.add_row("", "") # Empty row to separate latest from others.
137
160
 
138
161
  # Add all other versions (excluding the latest)
139
- versions = comm_app.app_versions if comm_app.app_versions is not None else []
162
+ versions = app_obj.get("app_versions", [])
140
163
  for version in versions:
141
164
  if version != latest_version:
142
165
  table.add_row(version, "")
@@ -144,52 +167,99 @@ def _versions_table(client: Client, app: str) -> None:
144
167
  console.print(table)
145
168
 
146
169
 
147
- def _versions_list(client: Client, app: str) -> None:
170
+ def versions_list(manifest: dict[str, Any], app: str) -> None:
148
171
  """
149
172
  This function prints a flat list of versions for a specific community app.
150
173
 
151
174
  Parameters
152
175
  ----------
153
- client : Client
154
- The Nextmv Cloud client to use for the request.
176
+ manifest : dict[str, Any]
177
+ The community apps manifest.
155
178
  app : str
156
179
  The name of the community app.
157
180
  """
158
181
 
159
- comm_app = _find_app(client, app)
160
- versions = comm_app.app_versions if comm_app.app_versions is not None else []
182
+ app_obj = find_app(manifest, app)
183
+ versions = app_obj.get("app_versions", [])
161
184
 
162
185
  versions_output = ""
163
186
  for version in versions:
164
187
  versions_output += f"{version}\n"
165
188
 
166
- print("\n".join(versions_output))
189
+ print("\n".join(app_obj.get("app_versions", [])))
190
+
191
+
192
+ def download_file(
193
+ directory: str,
194
+ file: str,
195
+ profile: str | None = None,
196
+ ) -> requests.Response:
197
+ """
198
+ Gets a file from an internal bucket and return it.
199
+
200
+ Parameters
201
+ ----------
202
+ directory : str
203
+ The directory in the bucket where the file is located.
204
+ file : str
205
+ The name of the file to download.
206
+ profile : str | None
207
+ The profile name to use. If None, the default profile is used.
208
+
209
+ Returns
210
+ -------
211
+ requests.Response
212
+ The response object containing the file data.
213
+
214
+ Raises
215
+ requests.HTTPError
216
+ If the response status code is not 2xx.
217
+ """
218
+
219
+ client = build_client(profile)
220
+
221
+ # Request the download URL for the file.
222
+ response = client.request(
223
+ method="GET",
224
+ endpoint="v0/internal/tools",
225
+ headers=client.headers | {"request-source": "cli"}, # Pass `client.headers` to preserve auth.
226
+ query_params={"file": f"{directory}/{file}"},
227
+ )
228
+
229
+ # Use the URL obtained to download the file.
230
+ body = response.json()
231
+ download_response = client.request(
232
+ method="GET",
233
+ endpoint=body.get("url"),
234
+ headers={"Content-Type": "application/json"},
235
+ )
236
+
237
+ return download_response
167
238
 
168
239
 
169
- def _find_app(client: Client, app: str) -> CommunityApp | None:
240
+ def find_app(manifest: dict[str, Any], app: str) -> dict[str, Any] | None:
170
241
  """
171
242
  Finds and returns a community app from the manifest by its name.
172
243
 
173
244
  Parameters
174
245
  ----------
175
- client : Client
176
- The Nextmv Cloud client to use for the request.
246
+ manifest : dict[str, Any]
247
+ The community apps manifest.
177
248
  app : str
178
249
  The name of the community app to find.
179
250
 
180
251
  Returns
181
252
  -------
182
- CommunityApp | None
183
- The community app if found, otherwise None.
253
+ dict[str, Any] | None
254
+ The community app dictionary if found, otherwise None.
184
255
  """
185
256
 
186
- comm_apps = list_community_apps(client)
187
- for comm_app in comm_apps:
188
- if comm_app.name == app:
189
- return comm_app
257
+ for manifest_app in manifest.get("apps", []):
258
+ if manifest_app.get("name", "") == app:
259
+ return manifest_app
190
260
 
191
261
  # We don't use error() here to allow printing something before exiting.
192
262
  rich.print(f"[red]Error:[/red] Community app [magenta]{app}[/magenta] was not found. Here are the available apps:")
193
- _apps_table(client)
263
+ apps_table(manifest)
194
264
 
195
265
  raise typer.Exit(code=1)
nextmv/cloud/__init__.py CHANGED
@@ -30,9 +30,6 @@ from .batch_experiment import BatchExperimentRun as BatchExperimentRun
30
30
  from .batch_experiment import ExperimentStatus as ExperimentStatus
31
31
  from .client import Client as Client
32
32
  from .client import get_size as get_size
33
- from .community import CommunityApp as CommunityApp
34
- from .community import clone_community_app as clone_community_app
35
- from .community import list_community_apps as list_community_apps
36
33
  from .ensemble import EnsembleDefinition as EnsembleDefinition
37
34
  from .ensemble import EvaluationRule as EvaluationRule
38
35
  from .ensemble import RuleObjective as RuleObjective
@@ -58,7 +55,6 @@ from .secrets import SecretType as SecretType
58
55
  from .shadow import ShadowTest as ShadowTest
59
56
  from .shadow import ShadowTestMetadata as ShadowTestMetadata
60
57
  from .shadow import StartEvents as StartEvents
61
- from .shadow import StopIntent as StopIntent
62
58
  from .shadow import TerminationEvents as TerminationEvents
63
59
  from .shadow import TestComparison as TestComparison
64
60
  from .switchback import SwitchbackPlan as SwitchbackPlan
@@ -21,7 +21,7 @@ list_application
21
21
  import json
22
22
  import shutil
23
23
  import sys
24
- from datetime import datetime
24
+ from datetime import datetime, timezone
25
25
  from enum import Enum
26
26
  from typing import Any
27
27
 
@@ -46,7 +46,9 @@ from nextmv.cloud.application._switchback import ApplicationSwitchbackMixin
46
46
  from nextmv.cloud.application._utils import _is_not_exist_error
47
47
  from nextmv.cloud.application._version import ApplicationVersionMixin
48
48
  from nextmv.cloud.client import Client
49
+ from nextmv.cloud.instance import Instance
49
50
  from nextmv.cloud.url import UploadURL
51
+ from nextmv.cloud.version import Version
50
52
  from nextmv.logger import log
51
53
  from nextmv.manifest import Manifest
52
54
  from nextmv.model import Model, ModelConfiguration
@@ -405,6 +407,14 @@ class Application(
405
407
  model: Model | None = None,
406
408
  model_configuration: ModelConfiguration | None = None,
407
409
  rich_print: bool = False,
410
+ auto_create: bool = False,
411
+ version_id: str | None = None,
412
+ version_name: str | None = None,
413
+ version_description: str | None = None,
414
+ instance_id: str | None = None,
415
+ instance_name: str | None = None,
416
+ instance_description: str | None = None,
417
+ update_default_instance: bool = False,
408
418
  ) -> None:
409
419
  """
410
420
  Push an app to Nextmv Cloud.
@@ -422,6 +432,17 @@ class Application(
422
432
  `nextmv.Model`. The model is encoded, some dependencies and
423
433
  accompanying files are packaged, and the app is pushed to Nextmv Cloud.
424
434
 
435
+ By default, this function only pushes the app. If you want to
436
+ automatically create a new version and instance after pushing, set
437
+ `auto_create=True`. The `version_id`, `version_name`, and
438
+ `version_description` arguments allow you to customize the new version
439
+ that is created. If not specified, defaults will be generated (random
440
+ ID, timestamped name/description). Similarly, `instance_id`,
441
+ `instance_name`, and `instance_description` can be used to customize
442
+ the new instance. If `update_default_instance` is True, the
443
+ application's default instance will be updated to the newly created
444
+ instance.
445
+
425
446
  Parameters
426
447
  ----------
427
448
  manifest : Optional[Manifest], default=None
@@ -440,6 +461,30 @@ class Application(
440
461
  with `model`.
441
462
  rich_print : bool, default=False
442
463
  Whether to use rich printing when verbose output is enabled.
464
+ auto_create : bool, default=False
465
+ If True, automatically create a new version and instance after
466
+ pushing the app.
467
+ version_id : Optional[str], default=None
468
+ ID of the version to create after pushing the app. If None, a unique
469
+ ID will be generated.
470
+ version_name : Optional[str], default=None
471
+ Name of the version to create after pushing the app. If None, a
472
+ name will be generated.
473
+ version_description : Optional[str], default=None
474
+ Description of the version to create after pushing the app. If None, a
475
+ generic description with a timestamp will be generated.
476
+ instance_id : Optional[str], default=None
477
+ ID of the instance to create after pushing the app. If None, a unique
478
+ ID will be generated.
479
+ instance_name : Optional[str], default=None
480
+ Name of the instance to create after pushing the app. If None, a
481
+ name will be generated.
482
+ instance_description : Optional[str], default=None
483
+ Description of the instance to create after pushing the app. If None,
484
+ a generic description with a timestamp will be generated.
485
+ update_default_instance : bool, default=False
486
+ If True, update the application's default instance to the newly
487
+ created instance.
443
488
 
444
489
  Raises
445
490
  ------
@@ -543,6 +588,44 @@ class Application(
543
588
  except OSError as e:
544
589
  raise Exception(f"error deleting output directory: {e}") from e
545
590
 
591
+ if not auto_create:
592
+ return
593
+
594
+ if verbose:
595
+ if rich_print:
596
+ rich.print(
597
+ f":hourglass_flowing_sand: Push completed for Nextmv application [magenta]{self.id}[/magenta], "
598
+ "creating a new version and instance...",
599
+ file=sys.stderr,
600
+ )
601
+ else:
602
+ log("⌛️ Push completed for the Nextmv application, creating a new version and instance...")
603
+
604
+ now = datetime.now(timezone.utc)
605
+ version = self.__version_on_push(
606
+ now=now,
607
+ version_id=version_id,
608
+ version_name=version_name,
609
+ version_description=version_description,
610
+ verbose=verbose,
611
+ rich_print=rich_print,
612
+ )
613
+ instance = self.__instance_on_push(
614
+ now=now,
615
+ version_id=version.id,
616
+ instance_id=instance_id,
617
+ instance_name=instance_name,
618
+ instance_description=instance_description,
619
+ verbose=verbose,
620
+ rich_print=rich_print,
621
+ )
622
+ self.__update_on_push(
623
+ instance=instance,
624
+ update_default_instance=update_default_instance,
625
+ verbose=verbose,
626
+ rich_print=rich_print,
627
+ )
628
+
546
629
  def update(
547
630
  self,
548
631
  name: str | None = None,
@@ -784,6 +867,8 @@ class Application(
784
867
  output_config = multi_config["output_configuration"] = {}
785
868
  if content.multi_file.output.statistics:
786
869
  output_config["statistics_path"] = content.multi_file.output.statistics
870
+ if content.multi_file.output.metrics:
871
+ output_config["metrics_path"] = content.multi_file.output.metrics
787
872
  if content.multi_file.output.assets:
788
873
  output_config["assets_path"] = content.multi_file.output.assets
789
874
  if content.multi_file.output.solutions:
@@ -858,6 +943,120 @@ class Application(
858
943
  log(f'💥️ Successfully pushed to application: "{self.id}".')
859
944
  log(json.dumps(data, indent=2))
860
945
 
946
+ def __version_on_push(
947
+ self,
948
+ now: datetime,
949
+ version_id: str | None = None,
950
+ version_name: str | None = None,
951
+ version_description: str | None = None,
952
+ verbose: bool = False,
953
+ rich_print: bool = False,
954
+ ) -> Version:
955
+ if version_description is None or version_description == "":
956
+ version_description = f"Version created automatically from push at {now.strftime('%Y-%m-%dT%H:%M:%SZ')}"
957
+
958
+ version = self.new_version(
959
+ id=version_id,
960
+ name=version_name,
961
+ description=version_description,
962
+ )
963
+ version_dict = version.to_dict()
964
+
965
+ if not verbose:
966
+ return version
967
+
968
+ if rich_print:
969
+ rich.print(
970
+ f":white_check_mark: Automatically created new version [magenta]{version.id}[/magenta].",
971
+ file=sys.stderr,
972
+ )
973
+ rich.print_json(data=version_dict)
974
+
975
+ return version
976
+
977
+ log(f'✅ Automatically created new version "{version.id}".')
978
+ log(json.dumps(version_dict, indent=2))
979
+
980
+ return version
981
+
982
+ def __instance_on_push(
983
+ self,
984
+ now: datetime,
985
+ version_id: str,
986
+ instance_id: str | None = None,
987
+ instance_name: str | None = None,
988
+ instance_description: str | None = None,
989
+ verbose: bool = False,
990
+ rich_print: bool = False,
991
+ ) -> Instance:
992
+ if instance_description is None or instance_description == "":
993
+ instance_description = f"Instance created automatically from push at {now.strftime('%Y-%m-%dT%H:%M:%SZ')}"
994
+
995
+ instance = self.new_instance(
996
+ version_id=version_id,
997
+ id=instance_id,
998
+ name=instance_name,
999
+ description=instance_description,
1000
+ )
1001
+ instance_dict = instance.to_dict()
1002
+
1003
+ if not verbose:
1004
+ return instance
1005
+
1006
+ if rich_print:
1007
+ rich.print(
1008
+ f":white_check_mark: Automatically created new instance [magenta]{instance.id}[/magenta] "
1009
+ f"using version [magenta]{version_id}[/magenta].",
1010
+ file=sys.stderr,
1011
+ )
1012
+ rich.print_json(data=instance_dict)
1013
+
1014
+ return instance
1015
+
1016
+ log(f'✅ Automatically created new instance "{instance.id}" using version "{version_id}".')
1017
+ log(json.dumps(instance_dict, indent=2))
1018
+
1019
+ return instance
1020
+
1021
+ def __update_on_push(
1022
+ self,
1023
+ instance: Instance,
1024
+ update_default_instance: bool = False,
1025
+ verbose: bool = False,
1026
+ rich_print: bool = False,
1027
+ ) -> None:
1028
+ if not update_default_instance:
1029
+ return
1030
+
1031
+ if verbose:
1032
+ if rich_print:
1033
+ rich.print(
1034
+ f":hourglass_flowing_sand: Updating default instance for app [magenta]{self.id}[/magenta]...",
1035
+ file=sys.stderr,
1036
+ )
1037
+ else:
1038
+ log("⌛️ Updating default instance for the Nextmv application...")
1039
+
1040
+ updated_app = self.update(default_instance_id=instance.id)
1041
+ if not verbose:
1042
+ return
1043
+
1044
+ if rich_print:
1045
+ rich.print(
1046
+ f":white_check_mark: Updated default instance to "
1047
+ f"[magenta]{updated_app.default_instance_id}[/magenta] for application "
1048
+ f"[magenta]{self.id}[/magenta].",
1049
+ file=sys.stderr,
1050
+ )
1051
+ rich.print_json(data=updated_app.to_dict())
1052
+
1053
+ return
1054
+
1055
+ log(
1056
+ f'✅ Updated default instance to "{updated_app.default_instance_id}" for application "{self.id}".',
1057
+ )
1058
+ log(json.dumps(updated_app.to_dict(), indent=2))
1059
+
861
1060
 
862
1061
  def list_applications(client: Client) -> list[Application]:
863
1062
  """
@@ -6,7 +6,6 @@ from datetime import datetime
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  from nextmv.cloud.input_set import InputSet, ManagedInput
9
- from nextmv.safe import safe_id
10
9
 
11
10
  if TYPE_CHECKING:
12
11
  from . import Application
@@ -17,32 +16,6 @@ class ApplicationInputSetMixin:
17
16
  Mixin class for managing app input sets within an application.
18
17
  """
19
18
 
20
- def delete_input_set(self: "Application", input_set_id: str) -> None:
21
- """
22
- Delete an input set.
23
-
24
- Deletes an input set along with all the associated information.
25
-
26
- Parameters
27
- ----------
28
- input_set_id : str
29
- ID of the input set to delete.
30
-
31
- Raises
32
- ------
33
- requests.HTTPError
34
- If the response status code is not 2xx.
35
-
36
- Examples
37
- --------
38
- >>> app.delete_input_set("input-set-123")
39
- """
40
-
41
- _ = self.client.request(
42
- method="DELETE",
43
- endpoint=f"{self.experiments_endpoint}/inputsets/{input_set_id}",
44
- )
45
-
46
19
  def input_set(self: "Application", input_set_id: str) -> InputSet:
47
20
  """
48
21
  Get an input set.
@@ -108,8 +81,8 @@ class ApplicationInputSetMixin:
108
81
 
109
82
  def new_input_set(
110
83
  self: "Application",
111
- id: str | None = None,
112
- name: str | None = None,
84
+ id: str,
85
+ name: str,
113
86
  description: str | None = None,
114
87
  end_time: datetime | None = None,
115
88
  instance_id: str | None = None,
@@ -134,11 +107,10 @@ class ApplicationInputSetMixin:
134
107
 
135
108
  Parameters
136
109
  ----------
137
- id: str | None = None
138
- ID of the input set, will be generated if not provided.
139
- name: str | None = None
140
- Name of the input set. If not provided, the ID will be used as
141
- the name.
110
+ id: str
111
+ ID of the input set
112
+ name: str
113
+ Name of the input set.
142
114
  description: Optional[str]
143
115
  Optional description of the input set.
144
116
  end_time: Optional[datetime]
@@ -173,14 +145,6 @@ class ApplicationInputSetMixin:
173
145
  If the response status code is not 2xx.
174
146
  """
175
147
 
176
- # Generate ID if not provided
177
- if id is None or id == "":
178
- id = safe_id("input-set")
179
-
180
- # Use ID as name if name not provided
181
- if name is None or name == "":
182
- name = id
183
-
184
148
  payload = {
185
149
  "id": id,
186
150
  "name": name,
@@ -21,7 +21,14 @@ from nextmv.cloud.url import DownloadURL
21
21
  from nextmv.input import Input, InputFormat
22
22
  from nextmv.logger import log
23
23
  from nextmv.options import Options
24
- from nextmv.output import ASSETS_KEY, STATISTICS_KEY, Asset, Output, OutputFormat, Statistics
24
+ from nextmv.output import (
25
+ ASSETS_KEY,
26
+ STATISTICS_KEY,
27
+ Asset,
28
+ Output,
29
+ OutputFormat,
30
+ Statistics,
31
+ )
25
32
  from nextmv.polling import DEFAULT_POLLING_OPTIONS, PollingOptions, poll
26
33
  from nextmv.run import (
27
34
  ExternalRunResult,
@@ -4,7 +4,7 @@ Application mixin for managing shadow tests.
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from nextmv.cloud.shadow import ShadowTest, ShadowTestMetadata, StartEvents, StopIntent, TerminationEvents
7
+ from nextmv.cloud.shadow import ShadowTest, ShadowTestMetadata, StartEvents, TerminationEvents
8
8
  from nextmv.run import Run
9
9
  from nextmv.safe import safe_id
10
10
 
@@ -248,7 +248,7 @@ class ApplicationShadowMixin:
248
248
  endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/start",
249
249
  )
250
250
 
251
- def stop_shadow_test(self: "Application", shadow_test_id: str, intent: StopIntent) -> None:
251
+ def stop_shadow_test(self: "Application", shadow_test_id: str) -> None:
252
252
  """
253
253
  Stop a shadow test. The test should already have started before using
254
254
  this method.
@@ -257,22 +257,16 @@ class ApplicationShadowMixin:
257
257
  ----------
258
258
  shadow_test_id : str
259
259
  ID of the shadow test to stop.
260
- intent : StopIntent
261
- Intent for stopping the shadow test.
260
+
262
261
  Raises
263
262
  ------
264
263
  requests.HTTPError
265
264
  If the response status code is not 2xx.
266
265
  """
267
266
 
268
- payload = {
269
- "intent": intent.value,
270
- }
271
-
272
267
  _ = self.client.request(
273
268
  method="PUT",
274
269
  endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/stop",
275
- payload=payload,
276
270
  )
277
271
 
278
272
  def update_shadow_test(