nextmv 1.0.0.dev7__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.
Files changed (37) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/_serialization.py +1 -1
  3. nextmv/cli/cloud/acceptance/create.py +12 -12
  4. nextmv/cli/cloud/app/push.py +15 -15
  5. nextmv/cli/cloud/input_set/__init__.py +0 -2
  6. nextmv/cli/cloud/run/create.py +9 -4
  7. nextmv/cli/cloud/shadow/stop.py +2 -14
  8. nextmv/cli/cloud/switchback/stop.py +2 -14
  9. nextmv/cli/community/clone.py +197 -11
  10. nextmv/cli/community/list.py +116 -46
  11. nextmv/cloud/__init__.py +0 -4
  12. nextmv/cloud/application/__init__.py +200 -1
  13. nextmv/cloud/application/_acceptance.py +8 -13
  14. nextmv/cloud/application/_input_set.py +6 -42
  15. nextmv/cloud/application/_run.py +8 -1
  16. nextmv/cloud/application/_shadow.py +3 -9
  17. nextmv/cloud/application/_switchback.py +2 -11
  18. nextmv/cloud/batch_experiment.py +1 -3
  19. nextmv/cloud/client.py +1 -1
  20. nextmv/cloud/integration.py +4 -7
  21. nextmv/cloud/shadow.py +0 -25
  22. nextmv/cloud/switchback.py +0 -2
  23. nextmv/default_app/main.py +4 -6
  24. nextmv/local/executor.py +83 -3
  25. nextmv/local/geojson_handler.py +1 -1
  26. nextmv/manifest.py +11 -7
  27. nextmv/model.py +2 -2
  28. nextmv/options.py +1 -1
  29. nextmv/output.py +57 -21
  30. nextmv/run.py +12 -3
  31. {nextmv-1.0.0.dev7.dist-info → nextmv-1.0.0.dev8.dist-info}/METADATA +1 -3
  32. {nextmv-1.0.0.dev7.dist-info → nextmv-1.0.0.dev8.dist-info}/RECORD +35 -37
  33. nextmv/cli/cloud/input_set/delete.py +0 -67
  34. nextmv/cloud/community.py +0 -441
  35. {nextmv-1.0.0.dev7.dist-info → nextmv-1.0.0.dev8.dist-info}/WHEEL +0 -0
  36. {nextmv-1.0.0.dev7.dist-info → nextmv-1.0.0.dev8.dist-info}/entry_points.txt +0 -0
  37. {nextmv-1.0.0.dev7.dist-info → nextmv-1.0.0.dev8.dist-info}/licenses/LICENSE +0 -0
@@ -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(
@@ -5,7 +5,6 @@ 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
9
8
  from nextmv.cloud.switchback import SwitchbackTest, SwitchbackTestMetadata, TestComparisonSingle
10
9
  from nextmv.run import Run
11
10
  from nextmv.safe import safe_id
@@ -217,7 +216,7 @@ class ApplicationSwitchbackMixin:
217
216
  payload = {
218
217
  "id": switchback_test_id,
219
218
  "name": name,
220
- "comparison": comparison.to_dict(),
219
+ "comparison": comparison,
221
220
  "generate_random_plan": {
222
221
  "unit_duration_minutes": unit_duration_minutes,
223
222
  "units": units,
@@ -258,7 +257,7 @@ class ApplicationSwitchbackMixin:
258
257
  endpoint=f"{self.experiments_endpoint}/switchback/{switchback_test_id}/start",
259
258
  )
260
259
 
261
- def stop_switchback_test(self: "Application", switchback_test_id: str, intent: StopIntent) -> None:
260
+ def stop_switchback_test(self: "Application", switchback_test_id: str) -> None:
262
261
  """
263
262
  Stop a switchback test. The test should already have started before using
264
263
  this method.
@@ -268,23 +267,15 @@ class ApplicationSwitchbackMixin:
268
267
  switchback_test_id : str
269
268
  ID of the switchback test to stop.
270
269
 
271
- intent : StopIntent
272
- Intent for stopping the switchback test.
273
-
274
270
  Raises
275
271
  ------
276
272
  requests.HTTPError
277
273
  If the response status code is not 2xx.
278
274
  """
279
275
 
280
- payload = {
281
- "intent": intent.value,
282
- }
283
-
284
276
  _ = self.client.request(
285
277
  method="PUT",
286
278
  endpoint=f"{self.experiments_endpoint}/switchback/{switchback_test_id}/stop",
287
- payload=payload,
288
279
  )
289
280
 
290
281
  def update_switchback_test(
@@ -30,9 +30,7 @@ class ExperimentStatus(str, Enum):
30
30
 
31
31
  You can import the `ExperimentStatus` class directly from `cloud`:
32
32
 
33
- ```python
34
- from nextmv.cloud import ExperimentStatus
35
- ```
33
+ ```python from nextmv.cloud import ExperimentStatus ```
36
34
 
37
35
  This enum represents the comprehensive set of possible states for an
38
36
  experiment in Nextmv Cloud.
nextmv/cloud/client.py CHANGED
@@ -303,7 +303,7 @@ class Client:
303
303
  if data is not None:
304
304
  kwargs["data"] = data
305
305
  if payload is not None:
306
- if isinstance(payload, (dict, list)):
306
+ if isinstance(payload, dict | list):
307
307
  data = deflated_serialize_json(payload, json_configurations=json_configurations)
308
308
  kwargs["data"] = data
309
309
  else:
@@ -225,12 +225,12 @@ class Integration(BaseModel):
225
225
  def new( # noqa: C901
226
226
  cls,
227
227
  client: Client,
228
+ name: str,
228
229
  integration_type: IntegrationType | str,
229
230
  exec_types: list[ManifestType | str],
230
231
  provider: IntegrationProvider | str,
231
232
  provider_config: dict[str, Any],
232
233
  integration_id: str | None = None,
233
- name: str | None = None,
234
234
  description: str | None = None,
235
235
  is_global: bool = False,
236
236
  application_ids: list[str] | None = None,
@@ -243,6 +243,8 @@ class Integration(BaseModel):
243
243
  ----------
244
244
  client : Client
245
245
  Client to use for interacting with the Nextmv Cloud API.
246
+ name : str
247
+ The name of the integration.
246
248
  integration_type : IntegrationType | str
247
249
  The type of the integration. Please refer to the `IntegrationType`
248
250
  enum for possible values.
@@ -257,9 +259,6 @@ class Integration(BaseModel):
257
259
  integration_id : str, optional
258
260
  The unique identifier of the integration. If not provided,
259
261
  it will be generated automatically.
260
- name : str | None, optional
261
- The name of the integration. If not provided, the integration ID
262
- will be used as the name.
263
262
  description : str, optional
264
263
  An optional description of the integration.
265
264
  is_global : bool, optional, default=False
@@ -303,10 +302,8 @@ class Integration(BaseModel):
303
302
  elif not is_global and application_ids is None:
304
303
  raise ValueError("A non-global integration must have specific application IDs.")
305
304
 
306
- if integration_id is None or integration_id == "":
305
+ if integration_id is None:
307
306
  integration_id = safe_id("integration")
308
- if name is None or name == "":
309
- name = integration_id
310
307
 
311
308
  if exist_ok:
312
309
  try:
nextmv/cloud/shadow.py CHANGED
@@ -19,7 +19,6 @@ ShadowTest
19
19
  """
20
20
 
21
21
  from datetime import datetime
22
- from enum import Enum
23
22
  from typing import Any
24
23
 
25
24
  from pydantic import AliasChoices, Field
@@ -228,27 +227,3 @@ class ShadowTest(BaseModel):
228
227
  """Grouped distributional summaries of the shadow test."""
229
228
  runs: list[Run] | None = None
230
229
  """List of runs in the shadow test."""
231
-
232
-
233
- class StopIntent(str, Enum):
234
- """
235
- Intent for stopping a shadow test.
236
-
237
- You can import the `StopIntent` class directly from `cloud`:
238
-
239
- ```python
240
- from nextmv.cloud import StopIntent
241
- ```
242
-
243
- Attributes
244
- ----------
245
- complete : str
246
- The test is marked as complete.
247
- cancel : str
248
- The test is canceled.
249
- """
250
-
251
- complete = "complete"
252
- """The test is marked as complete."""
253
- cancel = "cancel"
254
- """The test is canceled."""
@@ -45,8 +45,6 @@ class TestComparisonSingle(BaseModel):
45
45
  ID of the candidate instance for comparison.
46
46
  """
47
47
 
48
- __test__ = False # Prevents pytest from collecting this class as a test case
49
-
50
48
  baseline_instance_id: str
51
49
  """ID of the baseline instance for comparison."""
52
50
  candidate_instance_id: str
@@ -26,12 +26,10 @@ assets = create_visuals(name, input.data["radius"], input.data["distance"])
26
26
  output = nextmv.Output(
27
27
  options=options,
28
28
  solution={"message": message},
29
- statistics=nextmv.Statistics(
30
- result=nextmv.ResultStatistics(
31
- value=1.23,
32
- custom={"message": message},
33
- ),
34
- ),
29
+ metrics={
30
+ "value": 1.23,
31
+ "custom": {"message": message},
32
+ },
35
33
  assets=assets,
36
34
  )
37
35
  nextmv.write(output)
nextmv/local/executor.py CHANGED
@@ -22,6 +22,8 @@ process_run_information
22
22
  Function to update run metadata including duration and status.
23
23
  process_run_logs
24
24
  Function to process and save run logs.
25
+ process_run_metrics
26
+ Function to process and save run metrics.
25
27
  process_run_statistics
26
28
  Function to process and save run statistics.
27
29
  process_run_assets
@@ -57,7 +59,16 @@ from nextmv.local.local import (
57
59
  )
58
60
  from nextmv.local.plotly_handler import handle_plotly_visual
59
61
  from nextmv.manifest import Manifest, ManifestType
60
- from nextmv.output import ASSETS_KEY, OUTPUTS_KEY, SOLUTIONS_KEY, STATISTICS_KEY, Asset, OutputFormat, VisualSchema
62
+ from nextmv.output import (
63
+ ASSETS_KEY,
64
+ METRICS_KEY,
65
+ OUTPUTS_KEY,
66
+ SOLUTIONS_KEY,
67
+ STATISTICS_KEY,
68
+ Asset,
69
+ OutputFormat,
70
+ VisualSchema,
71
+ )
61
72
  from nextmv.status import StatusV2
62
73
 
63
74
 
@@ -305,7 +316,7 @@ def process_run_output(
305
316
  ) -> None:
306
317
  """
307
318
  Processes the result of the subprocess run. This function is in charge of
308
- handling the run results, including solutions, statistics, logs, assets,
319
+ handling the run results, including solutions, statistics, metrics, logs, assets,
309
320
  and visuals.
310
321
 
311
322
  Parameters
@@ -347,6 +358,13 @@ def process_run_output(
347
358
  result=result,
348
359
  stdout_output=stdout_output,
349
360
  )
361
+ process_run_metrics(
362
+ temp_run_outputs_dir=temp_run_outputs_dir,
363
+ outputs_dir=outputs_dir,
364
+ stdout_output=stdout_output,
365
+ temp_src=temp_src,
366
+ manifest=manifest,
367
+ )
350
368
  process_run_statistics(
351
369
  temp_run_outputs_dir=temp_run_outputs_dir,
352
370
  outputs_dir=outputs_dir,
@@ -499,6 +517,65 @@ def process_run_logs(
499
517
 
500
518
  f.write(std_err)
501
519
 
520
+ def process_run_metrics(
521
+ temp_run_outputs_dir: str,
522
+ outputs_dir: str,
523
+ stdout_output: str | dict[str, Any],
524
+ temp_src: str,
525
+ manifest: Manifest,
526
+ ) -> None:
527
+ """
528
+ Processes the metrics of the run. Checks for an outputs/metrics folder
529
+ or custom metrics file location from manifest. If found, copies to run
530
+ directory. Otherwise, attempts to extract metrics from stdout.
531
+
532
+ Parameters
533
+ ----------
534
+ temp_run_outputs_dir : str
535
+ The path to the temporary outputs directory.
536
+ outputs_dir : str
537
+ The path to the outputs directory in the run directory.
538
+ stdout_output : Union[str, dict[str, Any]]
539
+ The stdout output of the run, either as raw string or parsed dictionary.
540
+ temp_src : str
541
+ The path to the temporary source directory.
542
+ manifest : Manifest
543
+ The application manifest containing configuration and custom paths.
544
+ """
545
+
546
+ metrics_dst = os.path.join(outputs_dir, METRICS_KEY)
547
+ os.makedirs(metrics_dst, exist_ok=True)
548
+ metrics_file = f"{METRICS_KEY}.json"
549
+
550
+ # Check for custom location in manifest and override metrics_src if needed.
551
+ if (
552
+ manifest.configuration is not None
553
+ and manifest.configuration.content is not None
554
+ and manifest.configuration.content.format == OutputFormat.MULTI_FILE
555
+ and manifest.configuration.content.multi_file is not None
556
+ ):
557
+ metrics_src_file = os.path.join(temp_src, manifest.configuration.content.multi_file.output.metrics)
558
+
559
+ # If the custom metrics file exists, copy it to the metrics destination
560
+ if os.path.exists(metrics_src_file) and os.path.isfile(metrics_src_file):
561
+ metrics_dst_file = os.path.join(metrics_dst, metrics_file)
562
+ shutil.copy2(metrics_src_file, metrics_dst_file)
563
+ return
564
+
565
+ metrics_src = os.path.join(temp_run_outputs_dir, METRICS_KEY)
566
+ if os.path.exists(metrics_src) and os.path.isdir(metrics_src):
567
+ shutil.copytree(metrics_src, metrics_dst, dirs_exist_ok=True)
568
+ return
569
+
570
+ if not isinstance(stdout_output, dict):
571
+ return
572
+
573
+ if METRICS_KEY not in stdout_output:
574
+ return
575
+
576
+ with open(os.path.join(metrics_dst, metrics_file), "w") as f:
577
+ metrics = {METRICS_KEY: stdout_output[METRICS_KEY]}
578
+ json.dump(metrics, f, indent=2)
502
579
 
503
580
  def process_run_statistics(
504
581
  temp_run_outputs_dir: str,
@@ -508,6 +585,9 @@ def process_run_statistics(
508
585
  manifest: Manifest,
509
586
  ) -> None:
510
587
  """
588
+ !!! warning
589
+ `process_run_statistics` is deprecated, use `process_run_metrics` instead.
590
+
511
591
  Processes the statistics of the run. Checks for an outputs/statistics folder
512
592
  or custom statistics file location from manifest. If found, copies to run
513
593
  directory. Otherwise, attempts to extract statistics from stdout.
@@ -848,7 +928,7 @@ def _copy_new_or_modified_files( # noqa: C901
848
928
  This function identifies files that are either new (not present in the original
849
929
  source) or have been modified (different content, checksum, or modification time)
850
930
  compared to the original source. It excludes files that exist in specified
851
- exclusion directories to avoid copying input data, statistics, or assets as
931
+ exclusion directories to avoid copying input data, statistics, metrics, or assets as
852
932
  solution outputs.
853
933
 
854
934
  Parameters
@@ -111,7 +111,7 @@ def extract_coordinates(coords, all_coords) -> None:
111
111
  like Polygons and MultiPolygons
112
112
  """
113
113
  if isinstance(coords, list):
114
- if len(coords) == 2 and isinstance(coords[0], (int, float)) and isinstance(coords[1], (int, float)):
114
+ if len(coords) == 2 and isinstance(coords[0], int | float) and isinstance(coords[1], int | float):
115
115
  # This is a coordinate pair [lon, lat]
116
116
  all_coords.append(coords)
117
117
  else:
nextmv/manifest.py CHANGED
@@ -829,7 +829,9 @@ class ManifestContentMultiFileOutput(BaseModel):
829
829
  Parameters
830
830
  ----------
831
831
  statistics : Optional[str], default=""
832
- The path to the statistics file.
832
+ Deprecated: Use `metrics` instead. The path to the statistics file.
833
+ metrics : Optional[str], default=""
834
+ The path to the metrics file.
833
835
  assets : Optional[str], default=""
834
836
  The path to the assets file.
835
837
  solutions : Optional[str], default=""
@@ -839,16 +841,18 @@ class ManifestContentMultiFileOutput(BaseModel):
839
841
  --------
840
842
  >>> from nextmv import ManifestContentMultiFileOutput
841
843
  >>> output_config = ManifestContentMultiFileOutput(
842
- ... statistics="my-outputs/statistics.json",
844
+ ... metrics="my-outputs/metrics.json",
843
845
  ... assets="my-outputs/assets.json",
844
846
  ... solutions="my-outputs/solutions/"
845
847
  ... )
846
- >>> output_config.statistics
847
- 'my-outputs/statistics.json'
848
+ >>> output_config.metrics
849
+ 'my-outputs/metrics.json'
848
850
  """
849
851
 
850
852
  statistics: str | None = ""
851
- """The path to the statistics file."""
853
+ """Deprecated: Use `metrics` instead. The path to the statistics file."""
854
+ metrics: str | None = ""
855
+ """The path to the metrics file."""
852
856
  assets: str | None = ""
853
857
  """The path to the assets file."""
854
858
  solutions: str | None = ""
@@ -878,7 +882,7 @@ class ManifestContentMultiFile(BaseModel):
878
882
  >>> multi_file_config = ManifestContentMultiFile(
879
883
  ... input=ManifestContentMultiFileInput(path="data/input/"),
880
884
  ... output=ManifestContentMultiFileOutput(
881
- ... statistics="my-outputs/statistics.json",
885
+ ... metrics="my-outputs/metrics.json",
882
886
  ... assets="my-outputs/assets.json",
883
887
  ... solutions="my-outputs/solutions/"
884
888
  ... )
@@ -919,7 +923,7 @@ class ManifestContent(BaseModel):
919
923
  ... multi_file=ManifestContentMultiFile(
920
924
  ... input=ManifestContentMultiFileInput(path="data/input/"),
921
925
  ... output=ManifestContentMultiFileOutput(
922
- ... statistics="my-outputs/statistics.json",
926
+ ... metrics="my-outputs/metrics.json",
923
927
  ... assets="my-outputs/assets.json",
924
928
  ... solutions="my-outputs/solutions/"
925
929
  ... )
nextmv/model.py CHANGED
@@ -195,7 +195,7 @@ class Model:
195
195
  ... return nextmv.Output(
196
196
  ... options=input.options,
197
197
  ... solution=nextroute_output.solutions[0].to_dict(),
198
- ... statistics=nextroute_output.statistics.to_dict(),
198
+ ... metrics=nextroute_output.metrics.to_dict(),
199
199
  ... )
200
200
  """
201
201
 
@@ -234,7 +234,7 @@ class Model:
234
234
  ... return Output(
235
235
  ... options=input.options,
236
236
  ... solution=result,
237
- ... statistics={"processing_time": 0.5}
237
+ ... metrics={"processing_time": 0.5}
238
238
  ... )
239
239
  """
240
240
 
nextmv/options.py CHANGED
@@ -695,7 +695,7 @@ class Options:
695
695
  help=argparse.SUPPRESS,
696
696
  default="1",
697
697
  )
698
- args, _ = parser.parse_known_args()
698
+ args = parser.parse_args()
699
699
 
700
700
  for arg in vars(args):
701
701
  if arg == "fff" or arg == "f":