nextmv 1.0.0.dev4__py3-none-any.whl → 1.0.0.dev6__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 (38) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +1 -2
  3. nextmv/__init__.py +0 -4
  4. nextmv/cli/cloud/app/push.py +294 -204
  5. nextmv/cli/cloud/input_set/__init__.py +2 -0
  6. nextmv/cli/cloud/input_set/delete.py +67 -0
  7. nextmv/cli/cloud/run/create.py +4 -9
  8. nextmv/cli/cloud/shadow/stop.py +14 -2
  9. nextmv/cli/cloud/switchback/stop.py +14 -2
  10. nextmv/cli/community/clone.py +11 -197
  11. nextmv/cli/community/list.py +46 -116
  12. nextmv/cli/confirm.py +5 -3
  13. nextmv/cloud/__init__.py +4 -38
  14. nextmv/cloud/acceptance_test.py +1 -65
  15. nextmv/cloud/account.py +1 -6
  16. nextmv/cloud/application/__init__.py +1 -198
  17. nextmv/cloud/application/_batch_scenario.py +2 -17
  18. nextmv/cloud/application/_input_set.py +42 -6
  19. nextmv/cloud/application/_instance.py +1 -1
  20. nextmv/cloud/application/_managed_input.py +1 -1
  21. nextmv/cloud/application/_shadow.py +10 -4
  22. nextmv/cloud/application/_switchback.py +11 -2
  23. nextmv/cloud/application/_version.py +1 -1
  24. nextmv/cloud/batch_experiment.py +3 -1
  25. nextmv/cloud/community.py +441 -0
  26. nextmv/cloud/shadow.py +25 -0
  27. nextmv/deprecated.py +5 -3
  28. nextmv/input.py +0 -52
  29. nextmv/local/runner.py +1 -1
  30. nextmv/options.py +11 -256
  31. nextmv/output.py +0 -62
  32. nextmv/run.py +1 -10
  33. nextmv/status.py +1 -51
  34. {nextmv-1.0.0.dev4.dist-info → nextmv-1.0.0.dev6.dist-info}/METADATA +3 -1
  35. {nextmv-1.0.0.dev4.dist-info → nextmv-1.0.0.dev6.dist-info}/RECORD +38 -36
  36. {nextmv-1.0.0.dev4.dist-info → nextmv-1.0.0.dev6.dist-info}/WHEEL +0 -0
  37. {nextmv-1.0.0.dev4.dist-info → nextmv-1.0.0.dev6.dist-info}/entry_points.txt +0 -0
  38. {nextmv-1.0.0.dev4.dist-info → nextmv-1.0.0.dev6.dist-info}/licenses/LICENSE +0 -0
@@ -21,7 +21,7 @@ list_application
21
21
  import json
22
22
  import shutil
23
23
  import sys
24
- from datetime import datetime, timezone
24
+ from datetime import datetime
25
25
  from enum import Enum
26
26
  from typing import Any
27
27
 
@@ -46,9 +46,7 @@ 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
50
49
  from nextmv.cloud.url import UploadURL
51
- from nextmv.cloud.version import Version
52
50
  from nextmv.logger import log
53
51
  from nextmv.manifest import Manifest
54
52
  from nextmv.model import Model, ModelConfiguration
@@ -407,14 +405,6 @@ class Application(
407
405
  model: Model | None = None,
408
406
  model_configuration: ModelConfiguration | None = None,
409
407
  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,
418
408
  ) -> None:
419
409
  """
420
410
  Push an app to Nextmv Cloud.
@@ -432,17 +422,6 @@ class Application(
432
422
  `nextmv.Model`. The model is encoded, some dependencies and
433
423
  accompanying files are packaged, and the app is pushed to Nextmv Cloud.
434
424
 
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
-
446
425
  Parameters
447
426
  ----------
448
427
  manifest : Optional[Manifest], default=None
@@ -461,30 +440,6 @@ class Application(
461
440
  with `model`.
462
441
  rich_print : bool, default=False
463
442
  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.
488
443
 
489
444
  Raises
490
445
  ------
@@ -588,44 +543,6 @@ class Application(
588
543
  except OSError as e:
589
544
  raise Exception(f"error deleting output directory: {e}") from e
590
545
 
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
-
629
546
  def update(
630
547
  self,
631
548
  name: str | None = None,
@@ -941,120 +858,6 @@ class Application(
941
858
  log(f'💥️ Successfully pushed to application: "{self.id}".')
942
859
  log(json.dumps(data, indent=2))
943
860
 
944
- def __version_on_push(
945
- self,
946
- now: datetime,
947
- version_id: str | None = None,
948
- version_name: str | None = None,
949
- version_description: str | None = None,
950
- verbose: bool = False,
951
- rich_print: bool = False,
952
- ) -> Version:
953
- if version_description is None or version_description == "":
954
- version_description = f"Version created automatically from push at {now.strftime('%Y-%m-%dT%H:%M:%SZ')}"
955
-
956
- version = self.new_version(
957
- id=version_id,
958
- name=version_name,
959
- description=version_description,
960
- )
961
- version_dict = version.to_dict()
962
-
963
- if not verbose:
964
- return version
965
-
966
- if rich_print:
967
- rich.print(
968
- f":white_check_mark: Automatically created new version [magenta]{version.id}[/magenta].",
969
- file=sys.stderr,
970
- )
971
- rich.print_json(data=version_dict)
972
-
973
- return version
974
-
975
- log(f'✅ Automatically created new version "{version.id}".')
976
- log(json.dumps(version_dict, indent=2))
977
-
978
- return version
979
-
980
- def __instance_on_push(
981
- self,
982
- now: datetime,
983
- version_id: str,
984
- instance_id: str | None = None,
985
- instance_name: str | None = None,
986
- instance_description: str | None = None,
987
- verbose: bool = False,
988
- rich_print: bool = False,
989
- ) -> Instance:
990
- if instance_description is None or instance_description == "":
991
- instance_description = f"Instance created automatically from push at {now.strftime('%Y-%m-%dT%H:%M:%SZ')}"
992
-
993
- instance = self.new_instance(
994
- version_id=version_id,
995
- id=instance_id,
996
- name=instance_name,
997
- description=instance_description,
998
- )
999
- instance_dict = instance.to_dict()
1000
-
1001
- if not verbose:
1002
- return instance
1003
-
1004
- if rich_print:
1005
- rich.print(
1006
- f":white_check_mark: Automatically created new instance [magenta]{instance.id}[/magenta] "
1007
- f"using version [magenta]{version_id}[/magenta].",
1008
- file=sys.stderr,
1009
- )
1010
- rich.print_json(data=instance_dict)
1011
-
1012
- return instance
1013
-
1014
- log(f'✅ Automatically created new instance "{instance.id}" using version "{version_id}".')
1015
- log(json.dumps(instance_dict, indent=2))
1016
-
1017
- return instance
1018
-
1019
- def __update_on_push(
1020
- self,
1021
- instance: Instance,
1022
- update_default_instance: bool = False,
1023
- verbose: bool = False,
1024
- rich_print: bool = False,
1025
- ) -> None:
1026
- if not update_default_instance:
1027
- return
1028
-
1029
- if verbose:
1030
- if rich_print:
1031
- rich.print(
1032
- f":hourglass_flowing_sand: Updating default instance for app [magenta]{self.id}[/magenta]...",
1033
- file=sys.stderr,
1034
- )
1035
- else:
1036
- log("⌛️ Updating default instance for the Nextmv application...")
1037
-
1038
- updated_app = self.update(default_instance_id=instance.id)
1039
- if not verbose:
1040
- return
1041
-
1042
- if rich_print:
1043
- rich.print(
1044
- f":white_check_mark: Updated default instance to "
1045
- f"[magenta]{updated_app.default_instance_id}[/magenta] for application "
1046
- f"[magenta]{self.id}[/magenta].",
1047
- file=sys.stderr,
1048
- )
1049
- rich.print_json(data=updated_app.to_dict())
1050
-
1051
- return
1052
-
1053
- log(
1054
- f'✅ Updated default instance to "{updated_app.default_instance_id}" for application "{self.id}".',
1055
- )
1056
- log(json.dumps(updated_app.to_dict(), indent=2))
1057
-
1058
861
 
1059
862
  def list_applications(client: Client) -> list[Application]:
1060
863
  """
@@ -10,7 +10,6 @@ from nextmv.cloud.batch_experiment import (
10
10
  BatchExperimentMetadata,
11
11
  BatchExperimentRun,
12
12
  ExperimentStatus,
13
- to_runs,
14
13
  )
15
14
  from nextmv.cloud.input_set import InputSet, ManagedInput
16
15
  from nextmv.cloud.scenario import Scenario, ScenarioInputType, _option_sets, _scenarios_by_id
@@ -258,7 +257,6 @@ class ApplicationBatchMixin:
258
257
  self: "Application",
259
258
  name: str | None = None,
260
259
  input_set_id: str | None = None,
261
- instance_ids: list[str] | None = None,
262
260
  description: str | None = None,
263
261
  id: str | None = None,
264
262
  option_sets: dict[str, dict[str, str]] | None = None,
@@ -274,9 +272,6 @@ class ApplicationBatchMixin:
274
272
  Name of the batch experiment. If not provided, the ID will be used as the name.
275
273
  input_set_id: str | None
276
274
  ID of the input set to use for the batch experiment.
277
- instance_ids: list[str]
278
- This argument is deprecated, use `runs` instead.
279
- List of instance IDs to use for the batch experiment.
280
275
  description: Optional[str]
281
276
  Optional description of the batch experiment.
282
277
  id: Optional[str]
@@ -308,7 +303,7 @@ class ApplicationBatchMixin:
308
303
  id = safe_id("batch")
309
304
 
310
305
  # Use ID as name if name not provided
311
- if name is None:
306
+ if name is None or name == "":
312
307
  name = id
313
308
 
314
309
  payload = {
@@ -317,11 +312,6 @@ class ApplicationBatchMixin:
317
312
  }
318
313
  if input_set_id is not None:
319
314
  payload["input_set_id"] = input_set_id
320
- if instance_ids is not None:
321
- input_set = self.input_set(input_set_id)
322
- runs = to_runs(instance_ids, input_set)
323
- payload_runs = [run.to_dict() for run in runs]
324
- payload["runs"] = payload_runs
325
315
  if description is not None:
326
316
  payload["description"] = description
327
317
  if option_sets is not None:
@@ -346,7 +336,6 @@ class ApplicationBatchMixin:
346
336
  self: "Application",
347
337
  name: str | None = None,
348
338
  input_set_id: str | None = None,
349
- instance_ids: list[str] | None = None,
350
339
  description: str | None = None,
351
340
  id: str | None = None,
352
341
  option_sets: dict[str, dict[str, str]] | None = None,
@@ -368,9 +357,6 @@ class ApplicationBatchMixin:
368
357
  Name of the batch experiment. If not provided, the ID will be used as the name.
369
358
  input_set_id: str
370
359
  ID of the input set to use for the batch experiment.
371
- instance_ids: list[str]
372
- List of instance IDs to use for the batch experiment. This argument
373
- is deprecated, use `runs` instead.
374
360
  description: Optional[str]
375
361
  Optional description of the batch experiment.
376
362
  id: Optional[str]
@@ -402,7 +388,6 @@ class ApplicationBatchMixin:
402
388
  batch_id = self.new_batch_experiment(
403
389
  name=name,
404
390
  input_set_id=input_set_id,
405
- instance_ids=instance_ids,
406
391
  description=description,
407
392
  id=id,
408
393
  option_sets=option_sets,
@@ -485,7 +470,7 @@ class ApplicationBatchMixin:
485
470
  id = safe_id("scenario")
486
471
 
487
472
  # Use ID as name if name not provided
488
- if name is None:
473
+ if name is None or name == "":
489
474
  name = id
490
475
 
491
476
  scenarios_by_id = _scenarios_by_id(scenarios)
@@ -6,6 +6,7 @@ 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
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from . import Application
@@ -16,6 +17,32 @@ class ApplicationInputSetMixin:
16
17
  Mixin class for managing app input sets within an application.
17
18
  """
18
19
 
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
+
19
46
  def input_set(self: "Application", input_set_id: str) -> InputSet:
20
47
  """
21
48
  Get an input set.
@@ -81,8 +108,8 @@ class ApplicationInputSetMixin:
81
108
 
82
109
  def new_input_set(
83
110
  self: "Application",
84
- id: str,
85
- name: str,
111
+ id: str | None = None,
112
+ name: str | None = None,
86
113
  description: str | None = None,
87
114
  end_time: datetime | None = None,
88
115
  instance_id: str | None = None,
@@ -107,10 +134,11 @@ class ApplicationInputSetMixin:
107
134
 
108
135
  Parameters
109
136
  ----------
110
- id: str
111
- ID of the input set
112
- name: str
113
- Name of the input set.
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.
114
142
  description: Optional[str]
115
143
  Optional description of the input set.
116
144
  end_time: Optional[datetime]
@@ -145,6 +173,14 @@ class ApplicationInputSetMixin:
145
173
  If the response status code is not 2xx.
146
174
  """
147
175
 
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
+
148
184
  payload = {
149
185
  "id": id,
150
186
  "name": name,
@@ -202,7 +202,7 @@ class ApplicationInstanceMixin:
202
202
 
203
203
  if id is None or id == "":
204
204
  id = safe_id(prefix="instance")
205
- if name is None:
205
+ if name is None or name == "":
206
206
  name = id
207
207
 
208
208
  payload = {
@@ -149,7 +149,7 @@ class ApplicationManagedInputMixin:
149
149
 
150
150
  if id is None or id == "":
151
151
  id = safe_id(prefix="managed-input")
152
- if name is None:
152
+ if name is None or name == "":
153
153
  name = id
154
154
 
155
155
  payload = {
@@ -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, TerminationEvents
7
+ from nextmv.cloud.shadow import ShadowTest, ShadowTestMetadata, StartEvents, StopIntent, TerminationEvents
8
8
  from nextmv.run import Run
9
9
  from nextmv.safe import safe_id
10
10
 
@@ -204,7 +204,7 @@ class ApplicationShadowMixin:
204
204
  shadow_test_id = safe_id("shadow")
205
205
 
206
206
  # Use ID as name if name not provided
207
- if name is None:
207
+ if name is None or name == "":
208
208
  name = shadow_test_id
209
209
 
210
210
  payload = {
@@ -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) -> None:
251
+ def stop_shadow_test(self: "Application", shadow_test_id: str, intent: StopIntent) -> None:
252
252
  """
253
253
  Stop a shadow test. The test should already have started before using
254
254
  this method.
@@ -257,16 +257,22 @@ class ApplicationShadowMixin:
257
257
  ----------
258
258
  shadow_test_id : str
259
259
  ID of the shadow test to stop.
260
-
260
+ intent : StopIntent
261
+ Intent for stopping the shadow test.
261
262
  Raises
262
263
  ------
263
264
  requests.HTTPError
264
265
  If the response status code is not 2xx.
265
266
  """
266
267
 
268
+ payload = {
269
+ "intent": intent.value,
270
+ }
271
+
267
272
  _ = self.client.request(
268
273
  method="PUT",
269
274
  endpoint=f"{self.experiments_endpoint}/shadow/{shadow_test_id}/stop",
275
+ payload=payload,
270
276
  )
271
277
 
272
278
  def update_shadow_test(
@@ -5,6 +5,7 @@ Application mixin for managing switchback tests.
5
5
  from datetime import datetime
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from nextmv.cloud.shadow import StopIntent
8
9
  from nextmv.cloud.switchback import SwitchbackTest, SwitchbackTestMetadata, TestComparisonSingle
9
10
  from nextmv.run import Run
10
11
  from nextmv.safe import safe_id
@@ -210,7 +211,7 @@ class ApplicationSwitchbackMixin:
210
211
  switchback_test_id = safe_id("switchback")
211
212
 
212
213
  # Use ID as name if name not provided
213
- if name is None:
214
+ if name is None or name == "":
214
215
  name = switchback_test_id
215
216
 
216
217
  payload = {
@@ -257,7 +258,7 @@ class ApplicationSwitchbackMixin:
257
258
  endpoint=f"{self.experiments_endpoint}/switchback/{switchback_test_id}/start",
258
259
  )
259
260
 
260
- def stop_switchback_test(self: "Application", switchback_test_id: str) -> None:
261
+ def stop_switchback_test(self: "Application", switchback_test_id: str, intent: StopIntent) -> None:
261
262
  """
262
263
  Stop a switchback test. The test should already have started before using
263
264
  this method.
@@ -267,15 +268,23 @@ class ApplicationSwitchbackMixin:
267
268
  switchback_test_id : str
268
269
  ID of the switchback test to stop.
269
270
 
271
+ intent : StopIntent
272
+ Intent for stopping the switchback test.
273
+
270
274
  Raises
271
275
  ------
272
276
  requests.HTTPError
273
277
  If the response status code is not 2xx.
274
278
  """
275
279
 
280
+ payload = {
281
+ "intent": intent.value,
282
+ }
283
+
276
284
  _ = self.client.request(
277
285
  method="PUT",
278
286
  endpoint=f"{self.experiments_endpoint}/switchback/{switchback_test_id}/stop",
287
+ payload=payload,
279
288
  )
280
289
 
281
290
  def update_switchback_test(
@@ -141,7 +141,7 @@ class ApplicationVersionMixin:
141
141
  if id is None or id == "":
142
142
  id = safe_id(prefix="version")
143
143
 
144
- if name is None:
144
+ if name is None or name == "":
145
145
  name = id
146
146
 
147
147
  payload = {
@@ -30,7 +30,9 @@ class ExperimentStatus(str, Enum):
30
30
 
31
31
  You can import the `ExperimentStatus` class directly from `cloud`:
32
32
 
33
- ```python from nextmv.cloud import ExperimentStatus ```
33
+ ```python
34
+ from nextmv.cloud import ExperimentStatus
35
+ ```
34
36
 
35
37
  This enum represents the comprehensive set of possible states for an
36
38
  experiment in Nextmv Cloud.