nextmv 0.29.3__py3-none-any.whl → 0.29.4.dev1__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.
nextmv/__about__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "v0.29.3"
1
+ __version__ = "v0.29.4-dev.1"
@@ -29,6 +29,7 @@ import shutil
29
29
  import tarfile
30
30
  import tempfile
31
31
  import time
32
+ import uuid
32
33
  from collections.abc import Callable
33
34
  from dataclasses import dataclass
34
35
  from datetime import datetime
@@ -45,6 +46,7 @@ from nextmv.cloud.batch_experiment import (
45
46
  BatchExperimentInformation,
46
47
  BatchExperimentMetadata,
47
48
  BatchExperimentRun,
49
+ to_runs,
48
50
  )
49
51
  from nextmv.cloud.client import Client, get_size
50
52
  from nextmv.cloud.input_set import InputSet, ManagedInput
@@ -273,7 +275,7 @@ class Application:
273
275
  Client to use for interacting with the Nextmv Cloud API.
274
276
  id : str
275
277
  ID of the application.
276
- default_instance_id : str, default="devint"
278
+ default_instance_id : str, default=None
277
279
  Default instance ID to use for submitting runs.
278
280
  endpoint : str, default="v1/applications/{id}"
279
281
  Base endpoint for the application.
@@ -294,13 +296,22 @@ class Application:
294
296
  id: str
295
297
  """ID of the application."""
296
298
 
297
- default_instance_id: str = "devint"
299
+ default_instance_id: str = None
298
300
  """Default instance ID to use for submitting runs."""
299
301
  endpoint: str = "v1/applications/{id}"
300
302
  """Base endpoint for the application."""
301
303
  experiments_endpoint: str = "{base}/experiments"
302
304
  """Base endpoint for the experiments in the application."""
303
305
 
306
+ # Local experience parameters.
307
+ src: Optional[str] = None
308
+ """
309
+ Source of the application, if initialized locally. This is the path
310
+ to the application's source code.
311
+ """
312
+ description: Optional[str] = None
313
+ """Description of the application."""
314
+
304
315
  def __post_init__(self):
305
316
  """Initialize the endpoint and experiments_endpoint attributes.
306
317
 
@@ -310,6 +321,158 @@ class Application:
310
321
  self.endpoint = self.endpoint.format(id=self.id)
311
322
  self.experiments_endpoint = self.experiments_endpoint.format(base=self.endpoint)
312
323
 
324
+ @classmethod
325
+ def initialize(
326
+ cls,
327
+ name: str,
328
+ id: Optional[str] = None,
329
+ description: Optional[str] = None,
330
+ destination: Optional[str] = None,
331
+ client: Optional[Client] = None,
332
+ ) -> "Application":
333
+ """
334
+ Initialize a Nextmv application, locally.
335
+
336
+ This method will create a new application in the local file system. The
337
+ application is a folder with the name given by `name`, under the
338
+ location given by `destination`. If the `destination` parameter is not
339
+ specified, the current working directory is used as default. This
340
+ method will scaffold the application with the necessary files and
341
+ directories to have an opinionated structure for your decision model.
342
+ Once the application is initialized, you are encouraged to complete it
343
+ with the decision model itself, so that the application can be run,
344
+ locally or remotely.
345
+
346
+ This method differs from the `Application.new` method in that it
347
+ creates the application locally rather than in the Cloud.
348
+
349
+ Although not required, you are encouraged to specify the `client`
350
+ parameter, so that the application can be pushed and synced remotely,
351
+ with the Nextmv Cloud. If you don't specify the `client`, and intend to
352
+ interact with the Nextmv Cloud, you will encounter an error. Make sure
353
+ you set the `client` parameter on the `Application` instance after
354
+ initialization, if you don't provide it here.
355
+
356
+ Use the `destination` parameter to specify where you want the app to be
357
+ initialized, using the current working directory by default.
358
+
359
+ Parameters
360
+ ----------
361
+ name : str
362
+ Name of the application.
363
+ id : str, optional
364
+ ID of the application. Will be generated if not provided.
365
+ description : str, optional
366
+ Description of the application.
367
+ destination : str, optional
368
+ Destination directory where the application will be initialized. If
369
+ not provided, the current working directory will be used.
370
+ client : Client, optional
371
+ Client to use for interacting with the Nextmv Cloud API.
372
+
373
+ Returns
374
+ -------
375
+ Application
376
+ The initialized application instance.
377
+ """
378
+
379
+ destination_dir = os.getcwd() if destination is None else destination
380
+ app_id = id if id is not None else str(uuid.uuid4())
381
+
382
+ # Create the new directory with the given name.
383
+ src = os.path.join(destination_dir, name)
384
+ os.makedirs(src, exist_ok=True)
385
+
386
+ # Get the path to the initial app structure template.
387
+ current_file_dir = os.path.dirname(os.path.abspath(__file__))
388
+ initial_app_structure_path = os.path.join(current_file_dir, "..", "default_app")
389
+ initial_app_structure_path = os.path.normpath(initial_app_structure_path)
390
+
391
+ # Copy everything from initial_app_structure to the new directory.
392
+ if os.path.exists(initial_app_structure_path):
393
+ for item in os.listdir(initial_app_structure_path):
394
+ source_path = os.path.join(initial_app_structure_path, item)
395
+ dest_path = os.path.join(src, item)
396
+
397
+ if os.path.isdir(source_path):
398
+ shutil.copytree(source_path, dest_path, dirs_exist_ok=True)
399
+ continue
400
+
401
+ shutil.copy2(source_path, dest_path)
402
+
403
+ return cls(
404
+ id=app_id,
405
+ client=client,
406
+ src=src,
407
+ description=description,
408
+ )
409
+
410
+ @classmethod
411
+ def new(
412
+ cls,
413
+ client: Client,
414
+ name: str,
415
+ id: Optional[str] = None,
416
+ description: Optional[str] = None,
417
+ is_workflow: Optional[bool] = None,
418
+ exist_ok: bool = False,
419
+ ) -> "Application":
420
+ """
421
+ Create a new application directly in Nextmv Cloud.
422
+
423
+ The application is created as an empty shell, and executable code must
424
+ be pushed to the app before running it remotely.
425
+
426
+ Parameters
427
+ ----------
428
+ client : Client
429
+ Client to use for interacting with the Nextmv Cloud API.
430
+ name : str
431
+ Name of the application.
432
+ id : str, optional
433
+ ID of the application. Will be generated if not provided.
434
+ description : str, optional
435
+ Description of the application.
436
+ is_workflow : bool, optional
437
+ Whether the application is a Decision Workflow.
438
+ exist_ok : bool, default=False
439
+ If True and an application with the same ID already exists,
440
+ return the existing application instead of creating a new one.
441
+
442
+ Returns
443
+ -------
444
+ Application
445
+ The newly created (or existing) application.
446
+
447
+ Examples
448
+ --------
449
+ >>> from nextmv.cloud import Client
450
+ >>> client = Client(api_key="your-api-key")
451
+ >>> app = Application.new(client=client, name="My New App", id="my-app")
452
+ """
453
+
454
+ if exist_ok and cls.exists(client=client, id=id):
455
+ return Application(client=client, id=id)
456
+
457
+ payload = {
458
+ "name": name,
459
+ }
460
+
461
+ if description is not None:
462
+ payload["description"] = description
463
+ if id is not None:
464
+ payload["id"] = id
465
+ if is_workflow is not None:
466
+ payload["is_pipeline"] = is_workflow
467
+
468
+ response = client.request(
469
+ method="POST",
470
+ endpoint="v1/applications",
471
+ payload=payload,
472
+ )
473
+
474
+ return cls(client=client, id=response.json()["id"])
475
+
313
476
  def acceptance_test(self, acceptance_test_id: str) -> AcceptanceTest:
314
477
  """
315
478
  Retrieve details of an acceptance test.
@@ -901,69 +1064,6 @@ class Application:
901
1064
 
902
1065
  return ManagedInput.from_dict(response.json())
903
1066
 
904
- @classmethod
905
- def new(
906
- cls,
907
- client: Client,
908
- name: str,
909
- id: Optional[str] = None,
910
- description: Optional[str] = None,
911
- is_workflow: Optional[bool] = None,
912
- exist_ok: bool = False,
913
- ) -> "Application":
914
- """
915
- Create a new application.
916
-
917
- Parameters
918
- ----------
919
- client : Client
920
- Client to use for interacting with the Nextmv Cloud API.
921
- name : str
922
- Name of the application.
923
- id : str, optional
924
- ID of the application. Will be generated if not provided.
925
- description : str, optional
926
- Description of the application.
927
- is_workflow : bool, optional
928
- Whether the application is a Decision Workflow.
929
- exist_ok : bool, default=False
930
- If True and an application with the same ID already exists,
931
- return the existing application instead of creating a new one.
932
-
933
- Returns
934
- -------
935
- Application
936
- The newly created (or existing) application.
937
-
938
- Examples
939
- --------
940
- >>> from nextmv.cloud import Client
941
- >>> client = Client(api_key="your-api-key")
942
- >>> app = Application.new(client=client, name="My New App", id="my-app")
943
- """
944
-
945
- if exist_ok and cls.exists(client=client, id=id):
946
- return Application(client=client, id=id)
947
-
948
- payload = {
949
- "name": name,
950
- }
951
-
952
- if description is not None:
953
- payload["description"] = description
954
- if id is not None:
955
- payload["id"] = id
956
- if is_workflow is not None:
957
- payload["is_pipeline"] = is_workflow
958
-
959
- response = client.request(
960
- method="POST",
961
- endpoint="v1/applications",
962
- payload=payload,
963
- )
964
-
965
- return cls(client=client, id=response.json()["id"])
966
-
967
1067
  def new_acceptance_test(
968
1068
  self,
969
1069
  candidate_instance_id: str,
@@ -1027,12 +1127,21 @@ class Application:
1027
1127
  f"batch experiment {id} does not exist, input_set_id must be defined to create a new one"
1028
1128
  ) from e
1029
1129
  else:
1130
+ runs = [
1131
+ BatchExperimentRun(
1132
+ instance_id=candidate_instance_id,
1133
+ input_set_id=input_set_id,
1134
+ ),
1135
+ BatchExperimentRun(
1136
+ instance_id=baseline_instance_id,
1137
+ input_set_id=input_set_id,
1138
+ ),
1139
+ ]
1030
1140
  batch_experiment_id = self.new_batch_experiment(
1031
1141
  name=name,
1032
- input_set_id=input_set_id,
1033
- instance_ids=[candidate_instance_id, baseline_instance_id],
1034
1142
  description=description,
1035
1143
  id=id,
1144
+ runs=runs,
1036
1145
  )
1037
1146
 
1038
1147
  if batch_experiment_id != id:
@@ -1175,6 +1284,7 @@ class Application:
1175
1284
  ID of the input set to use for the batch experiment.
1176
1285
  instance_ids: list[str]
1177
1286
  List of instance IDs to use for the batch experiment.
1287
+ This argument is deprecated, use `runs` instead.
1178
1288
  description: Optional[str]
1179
1289
  Optional description of the batch experiment.
1180
1290
  id: Optional[str]
@@ -1207,7 +1317,10 @@ class Application:
1207
1317
  if input_set_id is not None:
1208
1318
  payload["input_set_id"] = input_set_id
1209
1319
  if instance_ids is not None:
1210
- payload["instance_ids"] = instance_ids
1320
+ input_set = self.input_set(input_set_id)
1321
+ runs = to_runs(instance_ids, input_set)
1322
+ payload_runs = [run.to_dict() for run in runs]
1323
+ payload["runs"] = payload_runs
1211
1324
  if description is not None:
1212
1325
  payload["description"] = description
1213
1326
  if id is not None:
@@ -1675,9 +1788,9 @@ class Application:
1675
1788
  )
1676
1789
  payload["result"] = external_dict
1677
1790
 
1678
- query_params = {
1679
- "instance_id": instance_id if instance_id is not None else self.default_instance_id,
1680
- }
1791
+ query_params = {}
1792
+ if instance_id is not None or self.default_instance_id is not None:
1793
+ query_params["instance_id"] = instance_id if instance_id is not None else self.default_instance_id
1681
1794
  response = self.client.request(
1682
1795
  method="POST",
1683
1796
  endpoint=f"{self.endpoint}/runs",
@@ -1996,8 +2109,16 @@ class Application:
1996
2109
  >>> # Create a new secrets collection with API keys
1997
2110
  >>> from nextmv.cloud import Secret
1998
2111
  >>> secrets = [
1999
- ... Secret(key="API_KEY", value="your-api-key"),
2000
- ... Secret(key="DATABASE_URL", value="your-database-url")
2112
+ ... Secret(
2113
+ ... location="API_KEY",
2114
+ ... value="your-api-key",
2115
+ ... secret_type=SecretType.ENV,
2116
+ ... ),
2117
+ ... Secret(
2118
+ ... location="DATABASE_URL",
2119
+ ... value="your-database-url",
2120
+ ... secret_type=SecretType.ENV,
2121
+ ... ),
2001
2122
  ... ]
2002
2123
  >>> collection = app.new_secrets_collection(
2003
2124
  ... secrets=secrets,
@@ -3219,7 +3340,7 @@ class Application:
3219
3340
  {
3220
3341
  "app_id": self.id,
3221
3342
  "endpoint": self.client.url,
3222
- "instance_url": f"{self.endpoint}/runs?instance_id=devint",
3343
+ "instance_url": f"{self.endpoint}/runs?instance_id=latest",
3223
3344
  },
3224
3345
  indent=2,
3225
3346
  )
@@ -17,6 +17,7 @@ from datetime import datetime
17
17
  from typing import Any, Optional
18
18
 
19
19
  from nextmv.base_model import BaseModel
20
+ from nextmv.cloud.input_set import InputSet
20
21
 
21
22
 
22
23
  class BatchExperimentInformation(BaseModel):
@@ -143,10 +144,10 @@ class BatchExperimentRun(BaseModel):
143
144
 
144
145
  Parameters
145
146
  ----------
146
- option_set : str
147
- Option set used for the experiment.
148
147
  input_id : str
149
148
  ID of the input used for the experiment.
149
+ option_set : str
150
+ Option set used for the experiment. Defaults to None.
150
151
  instance_id : str, optional
151
152
  ID of the instance used for the experiment. Defaults to None.
152
153
  version_id : str, optional
@@ -162,11 +163,11 @@ class BatchExperimentRun(BaseModel):
162
163
  Run number of the experiment. Defaults to None.
163
164
  """
164
165
 
165
- option_set: str
166
- """Option set used for the experiment."""
167
166
  input_id: str
168
167
  """ID of the input used for the experiment."""
169
168
 
169
+ option_set: Optional[str] = None
170
+ """Option set used for the experiment."""
170
171
  instance_id: Optional[str] = None
171
172
  """ID of the instance used for the experiment."""
172
173
  version_id: Optional[str] = None
@@ -177,8 +178,6 @@ class BatchExperimentRun(BaseModel):
177
178
  """If the batch experiment is a scenario test, this is the ID of that test."""
178
179
  repetition: Optional[int] = None
179
180
  """Repetition number of the experiment."""
180
- run_number: Optional[str] = None
181
- """Run number of the experiment."""
182
181
 
183
182
  def __post_init_post_parse__(self):
184
183
  """
@@ -215,3 +214,37 @@ class BatchExperimentMetadata(BatchExperimentInformation):
215
214
 
216
215
  app_id: Optional[str] = None
217
216
  """ID of the application used for the batch experiment."""
217
+
218
+
219
+ def to_runs(instance_ids: list[str], input_set: InputSet) -> list[BatchExperimentRun]:
220
+ """
221
+ Translate a legacy batch experiment list of instance ids to runs.
222
+
223
+ Parameters
224
+ ----------
225
+ instance_ids : list[str]
226
+ List of instance IDs to be converted into runs.
227
+ input_set : InputSet
228
+ Input set associated with the runs.
229
+
230
+ Returns
231
+ -------
232
+ list[BatchExperimentRun]
233
+ A list of `BatchExperimentRun` objects created from the instance IDs.
234
+ """
235
+
236
+ input_ids = input_set.input_ids
237
+ if len(input_set.input_ids) == 0:
238
+ input_ids = [i.id for i in input_set.inputs]
239
+
240
+ runs = []
241
+ for instance_id in instance_ids:
242
+ for input_id in input_ids:
243
+ run = BatchExperimentRun(
244
+ input_id=input_id,
245
+ instance_id=instance_id,
246
+ input_set_id=input_set.id,
247
+ )
248
+ runs.append(run)
249
+
250
+ return runs
nextmv/cloud/manifest.py CHANGED
@@ -322,6 +322,7 @@ class ManifestPython(BaseModel):
322
322
  from the app bundle.
323
323
  """
324
324
 
325
+
325
326
  class ManifestOptionUI(BaseModel):
326
327
  """
327
328
  UI attributes for an option in the manifest.
@@ -360,6 +361,7 @@ class ManifestOptionUI(BaseModel):
360
361
  hidden_from: Optional[list[str]] = None
361
362
  """A list of team roles for which this option will be hidden in the UI."""
362
363
 
364
+
363
365
  class ManifestOption(BaseModel):
364
366
  """
365
367
  An option for the decision model that is recorded in the manifest.
@@ -429,7 +431,6 @@ class ManifestOption(BaseModel):
429
431
  ui: Optional[ManifestOptionUI] = None
430
432
  """Optional UI attributes for the option."""
431
433
 
432
-
433
434
  @classmethod
434
435
  def from_option(cls, option: Option) -> "ManifestOption":
435
436
  """
@@ -483,7 +484,9 @@ class ManifestOption(BaseModel):
483
484
  ui=ManifestOptionUI(
484
485
  control_type=option.control_type,
485
486
  hidden_from=option.hidden_from,
486
- ) if option.control_type or option.hidden_from else None,
487
+ )
488
+ if option.control_type or option.hidden_from
489
+ else None,
487
490
  )
488
491
 
489
492
  def to_option(self) -> Option:
@@ -534,6 +537,7 @@ class ManifestOption(BaseModel):
534
537
  hidden_from=self.ui.hidden_from if self.ui else None,
535
538
  )
536
539
 
540
+
537
541
  class ManifestValidation(BaseModel):
538
542
  """
539
543
  Validation rules for options in the manifest.
@@ -569,6 +573,7 @@ class ManifestValidation(BaseModel):
569
573
  created if any of the rules of the options are violated.
570
574
  """
571
575
 
576
+
572
577
  class ManifestOptions(BaseModel):
573
578
  """
574
579
  Options for the decision model.
@@ -649,9 +654,10 @@ class ManifestOptions(BaseModel):
649
654
  return cls(
650
655
  strict=validation.strict if validation else False,
651
656
  validation=ManifestValidation(enforce="all" if validation and validation.validation_enforce else "none"),
652
- items=items
657
+ items=items,
653
658
  )
654
659
 
660
+
655
661
  class ManifestConfiguration(BaseModel):
656
662
  """
657
663
  Configuration for the decision model.
@@ -749,14 +755,17 @@ class Manifest(BaseModel):
749
755
  """The files to include (or exclude) in the app. This is mandatory."""
750
756
 
751
757
  runtime: ManifestRuntime = ManifestRuntime.PYTHON
752
- """The runtime to use for the app.
753
-
754
- It provides the environment in which the app runs. This is mandatory.
758
+ """
759
+ The runtime to use for the app. It provides the environment in which the
760
+ app runs. This is mandatory.
755
761
  """
756
762
  type: ManifestType = ManifestType.PYTHON
757
- """Type of application, based on the programming language. This is mandatory."""
763
+ """
764
+ Type of application, based on the programming language. This is mandatory.
765
+ """
758
766
  build: Optional[ManifestBuild] = None
759
- """Build-specific attributes.
767
+ """
768
+ Build-specific attributes.
760
769
 
761
770
  The `build.command` to run to build the app. This command will be executed
762
771
  without a shell, i.e., directly. The command must exit with a status of 0
@@ -770,7 +779,8 @@ class Manifest(BaseModel):
770
779
  validation_alias=AliasChoices("pre-push", "pre_push"),
771
780
  default=None,
772
781
  )
773
- """A command to run before the app is pushed to the Nextmv Cloud.
782
+ """
783
+ A command to run before the app is pushed to the Nextmv Cloud.
774
784
 
775
785
  This command can be used to compile a binary, run tests or similar tasks.
776
786
  One difference with what is specified under build, is that the command will
@@ -780,15 +790,14 @@ class Manifest(BaseModel):
780
790
  pushed (after the build command).
781
791
  """
782
792
  python: Optional[ManifestPython] = None
783
- """Python-specific attributes.
784
-
785
- Only for Python apps. Contains further Python-specific attributes.
793
+ """
794
+ Python-specific attributes. Only for Python apps. Contains further
795
+ Python-specific attributes.
786
796
  """
787
797
  configuration: Optional[ManifestConfiguration] = None
788
- """Configuration for the decision model.
789
-
790
- A list of options for the decision model. An option is a parameter that
791
- configures the decision model.
798
+ """
799
+ Configuration for the decision model. A list of options for the decision
800
+ model. An option is a parameter that configures the decision model.
792
801
  """
793
802
 
794
803
  @classmethod
@@ -1041,11 +1050,8 @@ class Manifest(BaseModel):
1041
1050
  type=ManifestType.PYTHON,
1042
1051
  python=ManifestPython(pip_requirements="requirements.txt"),
1043
1052
  configuration=ManifestConfiguration(
1044
- options= ManifestOptions.from_options(
1045
- options=options,
1046
- validation=validation
1047
- ),
1048
- )
1053
+ options=ManifestOptions.from_options(options=options, validation=validation),
1054
+ ),
1049
1055
  )
1050
1056
 
1051
1057
  return manifest
@@ -0,0 +1,17 @@
1
+ # Nextmv Application
2
+
3
+ This is the basic structure of a Nextmv application.
4
+
5
+ ```text
6
+ ├── app.yaml
7
+ ├── README.md
8
+ ├── requirements.txt
9
+ └── src
10
+ ```
11
+
12
+ * `app.yaml`: App manifest, containing the configuration to run the app
13
+ remotely on Nextmv Cloud.
14
+ * `README.md`: Description of the app.
15
+ * `requirements.txt`: Python dependencies for the app.
16
+ * `src/`: Source code for the app. The `main.py` file is the entry point for
17
+ the app.
@@ -0,0 +1,11 @@
1
+ # This manifest holds the information the app needs to run on the Nextmv Cloud.
2
+ type: python
3
+ runtime: ghcr.io/nextmv-io/runtime/python:3.11
4
+ python:
5
+ # All listed packages will get bundled with the app.
6
+ pip-requirements: requirements.txt
7
+
8
+ # List all files/directories that should be included in the app. Globbing
9
+ # (e.g.: configs/*.json) is supported.
10
+ files:
11
+ - src/
@@ -0,0 +1,2 @@
1
+ nextmv>=0.29.3
2
+ plotly>=6.2.0
File without changes
@@ -0,0 +1,36 @@
1
+ from visuals import create_visuals
2
+
3
+ import nextmv
4
+
5
+ # Read the input from stdin.
6
+ input = nextmv.load()
7
+ name = input.data["name"]
8
+
9
+ options = nextmv.Options(
10
+ nextmv.Option("details", bool, True, "Print details to logs. Default true.", False),
11
+ )
12
+
13
+ ##### Insert model here
14
+
15
+ # Print logs that render in the run view in Nextmv Console.
16
+ message = f"Hello, {name}"
17
+ nextmv.log(message)
18
+
19
+ if options.details:
20
+ detail = f"You are {input.data['distance']} million km from the sun"
21
+ nextmv.log(detail)
22
+
23
+ assets = create_visuals(name, input.data["radius"], input.data["distance"])
24
+
25
+ # Write output and statistics.
26
+ output = nextmv.Output(
27
+ solution=None,
28
+ statistics=nextmv.Statistics(
29
+ result=nextmv.ResultStatistics(
30
+ value=1.23,
31
+ custom={"message": message},
32
+ ),
33
+ ),
34
+ assets=assets,
35
+ )
36
+ nextmv.write(output)
@@ -0,0 +1,36 @@
1
+ import json
2
+
3
+ import plotly.graph_objects as go
4
+
5
+ import nextmv
6
+
7
+
8
+ def create_visuals(name: str, radius: float, distance: float) -> list[nextmv.Asset]:
9
+ """Create a Plotly bar chart with radius and distance for a planet."""
10
+
11
+ fig = go.Figure()
12
+ fig.add_trace(
13
+ go.Bar(x=[name], y=[radius], name="Radius (km)", marker_color="red", opacity=0.5),
14
+ )
15
+ fig.add_trace(
16
+ go.Bar(x=[name], y=[distance], name="Distance (Millions km)", marker_color="blue", opacity=0.5),
17
+ )
18
+ fig.update_layout(
19
+ title="Radius and Distance by Planet", xaxis_title="Planet", yaxis_title="Values", barmode="group"
20
+ )
21
+ fig = fig.to_json()
22
+
23
+ assets = [
24
+ nextmv.Asset(
25
+ name="Plotly example",
26
+ content_type="json",
27
+ visual=nextmv.Visual(
28
+ visual_schema=nextmv.VisualSchema.PLOTLY,
29
+ visual_type="custom-tab",
30
+ label="Charts",
31
+ ),
32
+ content=[json.loads(fig)],
33
+ )
34
+ ]
35
+
36
+ return assets
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextmv
3
- Version: 0.29.3
3
+ Version: 0.29.4.dev1
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/
@@ -1,4 +1,4 @@
1
- nextmv/__about__.py,sha256=T4tzmUuDnnb0J0dqfaNdw9b3G2SAB0ABIB2ddKlW5sI,24
1
+ nextmv/__about__.py,sha256=M8JE5o9w_sW2kAiXxbuOD5CVtJuSX6YEGncr8aePGdY,30
2
2
  nextmv/__entrypoint__.py,sha256=dA0iwwHtrq6Z9w9FxmxKLoBGLyhe7jWtUAU-Y3PEgHg,1094
3
3
  nextmv/__init__.py,sha256=FsF0pEkOSBuPY5EKu7NsBxro7jswGmOmaw61kZEudXY,1930
4
4
  nextmv/_serialization.py,sha256=JlSl6BL0M2Esf7F89GsGIZ__Pp8RnFRNM0UxYhuuYU4,2853
@@ -12,12 +12,12 @@ nextmv/output.py,sha256=vcBqtP1hJLYyUvfk_vl1Ejbk3q5KxOmlTz4Lnqqnyd0,54141
12
12
  nextmv/cloud/__init__.py,sha256=7BCh3z-XkbIcMvFHmbj2wA8OquIovjrAZL7O9kA9VZc,3868
13
13
  nextmv/cloud/acceptance_test.py,sha256=Bcfdmh2fkPeBx8FDCngeUo2fjV_LhsUdygnzDQCDbYY,26898
14
14
  nextmv/cloud/account.py,sha256=eukiYQha4U2fkIjg4SgdoawKE1kU5G7GPyDJVrn8hHA,6064
15
- nextmv/cloud/application.py,sha256=qMf7N0DpJnnkJiyMYjqFjR_2fVzOTjqT-bUWShYg7XE,119766
16
- nextmv/cloud/batch_experiment.py,sha256=rD3m-ioE1G8ADYN7afzr7zlq-3H22TNlj9RAh-_ZqIo,7270
15
+ nextmv/cloud/application.py,sha256=kWmmufaNP7t5ijaliiJTJ1Bt5URx3ETJOAk80tOgrEc,124634
16
+ nextmv/cloud/batch_experiment.py,sha256=Rmcwe1uAVz2kRrAMQqLYC5d__L_IqPXbF__RE3uQTAY,8196
17
17
  nextmv/cloud/client.py,sha256=E0DiUb377jvEnpXlRnfT1PGCI0Jm0lTUoX5VqeU91lk,18165
18
18
  nextmv/cloud/input_set.py,sha256=2dqmf5z-rZjTKwtBRvnUdfPfKv28It5uTCX0C70uP4Y,4242
19
19
  nextmv/cloud/instance.py,sha256=SS4tbp0LQMWDaeYpwcNxJei82oi_Hozv1t5i3QGjASY,4024
20
- nextmv/cloud/manifest.py,sha256=bphIZJzStkAc1gme39SecwI4L36QDjeyhWEZrSaukS4,35782
20
+ nextmv/cloud/manifest.py,sha256=k_i_KlQ33Lsz9hhirWDRbXMdyEh-aM-4aWOhIBhiGAA,35781
21
21
  nextmv/cloud/package.py,sha256=f0OjdlIOsI2LpmgSxdFf6YaA8Ucs9yAm_3bO0Cp8LH4,13027
22
22
  nextmv/cloud/run.py,sha256=YPBVjbnc6Ebgjxm5Rw1eY2-MiYx3KC7fQyqKWXY9auY,20836
23
23
  nextmv/cloud/safe.py,sha256=idifvV8P_79Zo2hIC_qxqZt9LUmD5TLQ9ikKwRUvd34,2522
@@ -25,7 +25,13 @@ nextmv/cloud/scenario.py,sha256=JRFTDiFBcrgud6wE2qDHUu5oO-Ur3zbPYhhB6ONCxTo,1426
25
25
  nextmv/cloud/secrets.py,sha256=fA5cX0jfTsPVZWV7433wzETGlXpWRLHGswuObx9e6FQ,6820
26
26
  nextmv/cloud/status.py,sha256=blvykRCTCTBkaqH88j4dzdQLhU2v1Ig62-_va98zw20,2789
27
27
  nextmv/cloud/version.py,sha256=5_S7_pWUVBFbvAArku20eK7S645GJcHtgE2OpXLdSzQ,5300
28
- nextmv-0.29.3.dist-info/METADATA,sha256=TxzaJMRtys2fgQMqGVSagO7wcU4G0Y5JZpJX8v03eQ8,15826
29
- nextmv-0.29.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
- nextmv-0.29.3.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
31
- nextmv-0.29.3.dist-info/RECORD,,
28
+ nextmv/default_app/README.md,sha256=zY9iXhqQM4Kan9XnTHoQrBMArXfD6d-k1z-MBle8yxs,451
29
+ nextmv/default_app/app.yaml,sha256=USfr6K4YkIC9VEw-ExlNIZKxkZf6beBu1YW6pWIzF2o,370
30
+ nextmv/default_app/requirements.txt,sha256=wRE_HkYYWzCGnYZ2NuatHXul4gCHvU3iUAdsxtzpYiA,29
31
+ nextmv/default_app/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
+ nextmv/default_app/src/main.py,sha256=HlO8UwZbZYiAQyrZDSR_T4NBcwP1gIjdTdwkJP_dn8I,847
33
+ nextmv/default_app/src/visuals.py,sha256=WYK_YBnLmYo3TpVev1CpoNCuW5R7hk9QIkeCmvMn1Fs,1014
34
+ nextmv-0.29.4.dev1.dist-info/METADATA,sha256=TqdIxsG4oSCF52TvDK7Jl2wSJYbQORtIPfKYTzPNGZA,15831
35
+ nextmv-0.29.4.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
+ nextmv-0.29.4.dev1.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
37
+ nextmv-0.29.4.dev1.dist-info/RECORD,,