nextmv 0.28.5__tar.gz → 0.29.0.dev0__tar.gz
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-0.28.5 → nextmv-0.29.0.dev0}/PKG-INFO +1 -1
- nextmv-0.29.0.dev0/nextmv/__about__.py +1 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/__init__.py +8 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/application.py +125 -13
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/client.py +28 -9
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/manifest.py +142 -14
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/package.py +1 -1
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/input.py +419 -6
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/model.py +12 -3
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/options.py +88 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/output.py +535 -51
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/requirements.txt +2 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/cloud/app.yaml +16 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/cloud/test_manifest.py +132 -3
- nextmv-0.29.0.dev0/tests/test_input.py +551 -0
- nextmv-0.29.0.dev0/tests/test_inputs/test_data.csv +4 -0
- nextmv-0.29.0.dev0/tests/test_inputs/test_data.json +1 -0
- nextmv-0.29.0.dev0/tests/test_inputs/test_data.txt +3 -0
- nextmv-0.29.0.dev0/tests/test_output.py +1425 -0
- nextmv-0.28.5/nextmv/__about__.py +0 -1
- nextmv-0.28.5/tests/test_input.py +0 -202
- nextmv-0.28.5/tests/test_output.py +0 -683
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/.gitignore +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/LICENSE +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/README.md +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/__entrypoint__.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/_serialization.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/base_model.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/__init__.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/acceptance_test.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/account.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/batch_experiment.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/input_set.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/instance.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/run.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/safe.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/scenario.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/secrets.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/status.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/cloud/version.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/deprecated.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/nextmv/logger.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/pyproject.toml +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/__init__.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/cloud/__init__.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/cloud/test_application.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/cloud/test_client.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/cloud/test_package.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/cloud/test_run.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/cloud/test_safe_name_id.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/cloud/test_scenario.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/scripts/__init__.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/scripts/options1.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/scripts/options2.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/scripts/options3.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/scripts/options4.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/scripts/options5.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/scripts/options6.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/scripts/options7.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/scripts/options_deprecated.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/test_base_model.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/test_entrypoint/__init__.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/test_entrypoint/test_entrypoint.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/test_logger.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/test_model.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/test_options.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/test_serialization.py +0 -0
- {nextmv-0.28.5 → nextmv-0.29.0.dev0}/tests/test_version.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "v0.29.0.dev0"
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
from .__about__ import __version__
|
|
4
4
|
from .base_model import BaseModel as BaseModel
|
|
5
5
|
from .base_model import from_dict as from_dict
|
|
6
|
+
from .input import DataFile as DataFile
|
|
6
7
|
from .input import Input as Input
|
|
7
8
|
from .input import InputFormat as InputFormat
|
|
8
9
|
from .input import InputLoader as InputLoader
|
|
9
10
|
from .input import LocalInputLoader as LocalInputLoader
|
|
11
|
+
from .input import csv_data_file as csv_data_file
|
|
12
|
+
from .input import json_data_file as json_data_file
|
|
10
13
|
from .input import load as load
|
|
11
14
|
from .input import load_local as load_local
|
|
15
|
+
from .input import text_data_file as text_data_file
|
|
12
16
|
from .logger import log as log
|
|
13
17
|
from .logger import redirect_stdout as redirect_stdout
|
|
14
18
|
from .logger import reset_stdout as reset_stdout
|
|
@@ -27,9 +31,13 @@ from .output import ResultStatistics as ResultStatistics
|
|
|
27
31
|
from .output import RunStatistics as RunStatistics
|
|
28
32
|
from .output import Series as Series
|
|
29
33
|
from .output import SeriesData as SeriesData
|
|
34
|
+
from .output import SolutionFile as SolutionFile
|
|
30
35
|
from .output import Statistics as Statistics
|
|
31
36
|
from .output import Visual as Visual
|
|
32
37
|
from .output import VisualSchema as VisualSchema
|
|
38
|
+
from .output import csv_solution_file as csv_solution_file
|
|
39
|
+
from .output import json_solution_file as json_solution_file
|
|
40
|
+
from .output import text_solution_file as text_solution_file
|
|
33
41
|
from .output import write as write
|
|
34
42
|
from .output import write_local as write_local
|
|
35
43
|
|
|
@@ -23,8 +23,11 @@ poll
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
import json
|
|
26
|
+
import os
|
|
26
27
|
import random
|
|
27
28
|
import shutil
|
|
29
|
+
import tarfile
|
|
30
|
+
import tempfile
|
|
28
31
|
import time
|
|
29
32
|
from collections.abc import Callable
|
|
30
33
|
from dataclasses import dataclass
|
|
@@ -1494,6 +1497,7 @@ class Application:
|
|
|
1494
1497
|
batch_experiment_id: Optional[str] = None,
|
|
1495
1498
|
external_result: Optional[Union[ExternalRunResult, dict[str, Any]]] = None,
|
|
1496
1499
|
json_configurations: Optional[dict[str, Any]] = None,
|
|
1500
|
+
dir_path: Optional[str] = None,
|
|
1497
1501
|
) -> str:
|
|
1498
1502
|
"""
|
|
1499
1503
|
Submit an input to start a new run of the application. Returns the
|
|
@@ -1503,11 +1507,35 @@ class Application:
|
|
|
1503
1507
|
----------
|
|
1504
1508
|
input: Union[Input, dict[str, Any], BaseModel, str]
|
|
1505
1509
|
Input to use for the run. This can be a `nextmv.Input` object,
|
|
1506
|
-
`dict`, `BaseModel` or `str`.
|
|
1507
|
-
|
|
1508
|
-
`
|
|
1509
|
-
`
|
|
1510
|
-
|
|
1510
|
+
`dict`, `BaseModel` or `str`.
|
|
1511
|
+
|
|
1512
|
+
If `nextmv.Input` is used, and the `input_format` is either
|
|
1513
|
+
`nextmv.InputFormat.JSON` or `nextmv.InputFormat.TEXT`, then the
|
|
1514
|
+
input data is extracted from the `.data` property.
|
|
1515
|
+
|
|
1516
|
+
If you want to work with `nextmv.InputFormat.CSV_ARCHIVE` or
|
|
1517
|
+
`nextmv.InputFormat.MULTI_FILE`, you should use the `dir_path`
|
|
1518
|
+
argument instead. This argument takes precedence over the `input`.
|
|
1519
|
+
If `dir_path` is specified, this function looks for files in that
|
|
1520
|
+
directory and tars them, to later be uploaded using the
|
|
1521
|
+
`upload_large_input` method. If both the `dir_path` and `input`
|
|
1522
|
+
arguments are provided, the `input` is ignored.
|
|
1523
|
+
|
|
1524
|
+
When `dir_path` is specified, the `configuration` argument must
|
|
1525
|
+
also be provided. More specifically, the
|
|
1526
|
+
`RunConfiguration.format.format_input.input_type` parameter
|
|
1527
|
+
dictates what kind of input is being submitted to the Nextmv Cloud.
|
|
1528
|
+
Make sure that this parameter is specified when working with the
|
|
1529
|
+
following input formats:
|
|
1530
|
+
|
|
1531
|
+
- `nextmv.InputFormat.CSV_ARCHIVE`
|
|
1532
|
+
- `nextmv.InputFormat.MULTI_FILE`
|
|
1533
|
+
|
|
1534
|
+
When working with JSON or text data, use the `input` argument
|
|
1535
|
+
directly.
|
|
1536
|
+
|
|
1537
|
+
In general, if an input is too large, it will be uploaded with the
|
|
1538
|
+
`upload_large_input` method.
|
|
1511
1539
|
instance_id: Optional[str]
|
|
1512
1540
|
ID of the instance to use for the run. If not provided, the default
|
|
1513
1541
|
instance ID associated to the Class (`default_instance_id`) is
|
|
@@ -1560,26 +1588,36 @@ class Application:
|
|
|
1560
1588
|
not `JSON`. If the final `options` are not of type `dict[str,str]`.
|
|
1561
1589
|
"""
|
|
1562
1590
|
|
|
1591
|
+
self.__validate_dir_path_and_configuration(dir_path, configuration)
|
|
1592
|
+
|
|
1593
|
+
tar_file = ""
|
|
1594
|
+
if dir_path is not None and dir_path != "":
|
|
1595
|
+
if not os.path.exists(dir_path):
|
|
1596
|
+
raise ValueError(f"Directory {dir_path} does not exist.")
|
|
1597
|
+
|
|
1598
|
+
if not os.path.isdir(dir_path):
|
|
1599
|
+
raise ValueError(f"Path {dir_path} is not a directory.")
|
|
1600
|
+
|
|
1601
|
+
tar_file = self.__package_inputs(dir_path)
|
|
1602
|
+
|
|
1563
1603
|
input_data = None
|
|
1564
1604
|
if isinstance(input, BaseModel):
|
|
1565
1605
|
input_data = input.to_dict()
|
|
1566
1606
|
elif isinstance(input, dict) or isinstance(input, str):
|
|
1567
1607
|
input_data = input
|
|
1568
1608
|
elif isinstance(input, Input):
|
|
1569
|
-
if input.input_format == InputFormat.CSV_ARCHIVE:
|
|
1570
|
-
raise ValueError("csv-archive is not supported")
|
|
1571
1609
|
input_data = input.data
|
|
1572
1610
|
|
|
1573
1611
|
input_size = 0
|
|
1574
1612
|
if input_data is not None:
|
|
1575
1613
|
input_size = get_size(input_data)
|
|
1576
1614
|
|
|
1577
|
-
upload_url_required = input_size > _MAX_RUN_SIZE
|
|
1615
|
+
upload_url_required = input_size > _MAX_RUN_SIZE or tar_file != ""
|
|
1578
1616
|
upload_id_used = upload_id is not None
|
|
1579
1617
|
|
|
1580
1618
|
if not upload_id_used and upload_url_required:
|
|
1581
1619
|
upload_url = self.upload_url()
|
|
1582
|
-
self.upload_large_input(input=input_data, upload_url=upload_url)
|
|
1620
|
+
self.upload_large_input(input=input_data, upload_url=upload_url, tar_file=tar_file)
|
|
1583
1621
|
upload_id = upload_url.upload_id
|
|
1584
1622
|
upload_id_used = True
|
|
1585
1623
|
|
|
@@ -2797,9 +2835,10 @@ class Application:
|
|
|
2797
2835
|
|
|
2798
2836
|
def upload_large_input(
|
|
2799
2837
|
self,
|
|
2800
|
-
input: Union[dict[str, Any], str],
|
|
2838
|
+
input: Optional[Union[dict[str, Any], str]],
|
|
2801
2839
|
upload_url: UploadURL,
|
|
2802
2840
|
json_configurations: Optional[dict[str, Any]] = None,
|
|
2841
|
+
tar_file: Optional[str] = None,
|
|
2803
2842
|
) -> None:
|
|
2804
2843
|
"""
|
|
2805
2844
|
Upload large input data to the provided upload URL.
|
|
@@ -2810,14 +2849,19 @@ class Application:
|
|
|
2810
2849
|
|
|
2811
2850
|
Parameters
|
|
2812
2851
|
----------
|
|
2813
|
-
input : Union[dict[str, Any], str]
|
|
2852
|
+
input : Optional[Union[dict[str, Any], str]]
|
|
2814
2853
|
Input data to upload. Can be either a dictionary that will be
|
|
2815
2854
|
converted to JSON, or a pre-formatted JSON string.
|
|
2816
2855
|
upload_url : UploadURL
|
|
2817
2856
|
Upload URL object containing the pre-signed URL to use for uploading.
|
|
2818
2857
|
json_configurations : Optional[dict[str, Any]], default=None
|
|
2819
2858
|
Optional configurations for JSON serialization. If provided, these
|
|
2820
|
-
configurations will be used when serializing the data via
|
|
2859
|
+
configurations will be used when serializing the data via
|
|
2860
|
+
`json.dumps`.
|
|
2861
|
+
tar_file : Optional[str], default=None
|
|
2862
|
+
If provided, this will be used to upload a tar file instead of
|
|
2863
|
+
a JSON string or dictionary. This is useful for uploading large
|
|
2864
|
+
files that are already packaged as a tarball.
|
|
2821
2865
|
|
|
2822
2866
|
Returns
|
|
2823
2867
|
-------
|
|
@@ -2841,12 +2885,13 @@ class Application:
|
|
|
2841
2885
|
>>> app.upload_large_input(input=json_str, upload_url=url)
|
|
2842
2886
|
"""
|
|
2843
2887
|
|
|
2844
|
-
if isinstance(input, dict):
|
|
2888
|
+
if input is not None and isinstance(input, dict):
|
|
2845
2889
|
input = deflated_serialize_json(input, json_configurations=json_configurations)
|
|
2846
2890
|
|
|
2847
2891
|
self.client.upload_to_presigned_url(
|
|
2848
2892
|
url=upload_url.upload_url,
|
|
2849
2893
|
data=input,
|
|
2894
|
+
tar_file=tar_file,
|
|
2850
2895
|
)
|
|
2851
2896
|
|
|
2852
2897
|
def upload_url(self) -> UploadURL:
|
|
@@ -3185,6 +3230,73 @@ class Application:
|
|
|
3185
3230
|
|
|
3186
3231
|
raise ValueError(f"Unknown scenario input type: {scenario.scenario_input.scenario_input_type}")
|
|
3187
3232
|
|
|
3233
|
+
def __validate_dir_path_and_configuration(
|
|
3234
|
+
self,
|
|
3235
|
+
dir_path: Optional[str],
|
|
3236
|
+
configuration: Optional[RunConfiguration],
|
|
3237
|
+
) -> None:
|
|
3238
|
+
"""
|
|
3239
|
+
Auxiliary function to validate the directory path and configuration.
|
|
3240
|
+
"""
|
|
3241
|
+
if dir_path is None or dir_path == "":
|
|
3242
|
+
return
|
|
3243
|
+
|
|
3244
|
+
if configuration is None:
|
|
3245
|
+
raise ValueError(
|
|
3246
|
+
"If dir_path is provided, a RunConfiguration must also be provided.",
|
|
3247
|
+
)
|
|
3248
|
+
|
|
3249
|
+
if configuration.format is None:
|
|
3250
|
+
raise ValueError(
|
|
3251
|
+
"If dir_path is provided, RunConfiguration.format must also be provided.",
|
|
3252
|
+
)
|
|
3253
|
+
|
|
3254
|
+
if configuration.format.format_input is None:
|
|
3255
|
+
raise ValueError(
|
|
3256
|
+
"If dir_path is provided, RunConfiguration.format.format_input must also be provided.",
|
|
3257
|
+
)
|
|
3258
|
+
|
|
3259
|
+
input_type = configuration.format.format_input.input_type
|
|
3260
|
+
if input_type is None or input_type in (InputFormat.JSON, InputFormat.TEXT):
|
|
3261
|
+
raise ValueError(
|
|
3262
|
+
"If dir_path is provided, RunConfiguration.format.format_input.input_type must be set to a valid type."
|
|
3263
|
+
f"Valid types are: {[InputFormat.CSV_ARCHIVE, InputFormat.MULTI_FILE]}",
|
|
3264
|
+
)
|
|
3265
|
+
|
|
3266
|
+
def __package_inputs(self, dir_path: str) -> str:
|
|
3267
|
+
"""
|
|
3268
|
+
This is an auxiliary function for packaging the inputs found in the
|
|
3269
|
+
provided `dir_path`. All the files found in the directory are tarred and
|
|
3270
|
+
g-zipped. This function returns the tar file path that contains the
|
|
3271
|
+
packaged inputs.
|
|
3272
|
+
"""
|
|
3273
|
+
|
|
3274
|
+
# Create a temporary directory for the output
|
|
3275
|
+
output_dir = tempfile.mkdtemp(prefix="nextmv-inputs-out-")
|
|
3276
|
+
|
|
3277
|
+
# Define the output tar file name and path
|
|
3278
|
+
tar_filename = "inputs.tar.gz"
|
|
3279
|
+
tar_file_path = os.path.join(output_dir, tar_filename)
|
|
3280
|
+
|
|
3281
|
+
# Create the tar.gz file
|
|
3282
|
+
with tarfile.open(tar_file_path, "w:gz") as tar:
|
|
3283
|
+
for root, _, files in os.walk(dir_path):
|
|
3284
|
+
for file in files:
|
|
3285
|
+
if file == tar_filename:
|
|
3286
|
+
continue
|
|
3287
|
+
|
|
3288
|
+
file_path = os.path.join(root, file)
|
|
3289
|
+
|
|
3290
|
+
# Skip directories, only process files
|
|
3291
|
+
if os.path.isdir(file_path):
|
|
3292
|
+
continue
|
|
3293
|
+
|
|
3294
|
+
# Create relative path for the archive
|
|
3295
|
+
arcname = os.path.relpath(file_path, start=dir_path)
|
|
3296
|
+
tar.add(file_path, arcname=arcname)
|
|
3297
|
+
|
|
3298
|
+
return tar_file_path
|
|
3299
|
+
|
|
3188
3300
|
|
|
3189
3301
|
def poll( # noqa: C901
|
|
3190
3302
|
polling_options: PollingOptions,
|
|
@@ -323,7 +323,11 @@ class Client:
|
|
|
323
323
|
return response
|
|
324
324
|
|
|
325
325
|
def upload_to_presigned_url(
|
|
326
|
-
self,
|
|
326
|
+
self,
|
|
327
|
+
data: Optional[Union[dict[str, Any], str]],
|
|
328
|
+
url: str,
|
|
329
|
+
json_configurations: Optional[dict[str, Any]] = None,
|
|
330
|
+
tar_file: Optional[str] = None,
|
|
327
331
|
) -> None:
|
|
328
332
|
"""
|
|
329
333
|
Uploads data to a presigned URL.
|
|
@@ -333,7 +337,7 @@ class Client:
|
|
|
333
337
|
|
|
334
338
|
Parameters
|
|
335
339
|
----------
|
|
336
|
-
data : dict[str, Any]
|
|
340
|
+
data : Union[dict[str, Any], str], optional
|
|
337
341
|
The data to upload. If a dictionary is provided, it will be
|
|
338
342
|
JSON-serialized. If a string is provided, it will be uploaded
|
|
339
343
|
as is.
|
|
@@ -344,6 +348,11 @@ class Client:
|
|
|
344
348
|
customization of the Python `json.dumps` function, such as
|
|
345
349
|
specifying `indent` for pretty printing or `default` for custom
|
|
346
350
|
serialization functions.
|
|
351
|
+
tar_file : str, optional
|
|
352
|
+
If provided, this will be used to upload a tar file instead of
|
|
353
|
+
a JSON string or dictionary. This is useful for uploading large
|
|
354
|
+
files that are already packaged as a tarball. If this is provided,
|
|
355
|
+
`data` is expected to be `None`.
|
|
347
356
|
|
|
348
357
|
Raises
|
|
349
358
|
------
|
|
@@ -361,12 +370,13 @@ class Client:
|
|
|
361
370
|
"""
|
|
362
371
|
|
|
363
372
|
upload_data: Optional[str] = None
|
|
364
|
-
if
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
373
|
+
if data is not None:
|
|
374
|
+
if isinstance(data, dict):
|
|
375
|
+
upload_data = deflated_serialize_json(data, json_configurations=json_configurations)
|
|
376
|
+
elif isinstance(data, str):
|
|
377
|
+
upload_data = data
|
|
378
|
+
else:
|
|
379
|
+
raise ValueError("data must be a dictionary or a string")
|
|
370
380
|
|
|
371
381
|
session = requests.Session()
|
|
372
382
|
retries = Retry(
|
|
@@ -379,12 +389,21 @@ class Client:
|
|
|
379
389
|
)
|
|
380
390
|
adapter = HTTPAdapter(max_retries=retries)
|
|
381
391
|
session.mount("https://", adapter)
|
|
392
|
+
|
|
382
393
|
kwargs: dict[str, Any] = {
|
|
383
394
|
"url": url,
|
|
384
395
|
"timeout": self.timeout,
|
|
385
|
-
"data": upload_data,
|
|
386
396
|
}
|
|
387
397
|
|
|
398
|
+
if upload_data is not None:
|
|
399
|
+
kwargs["data"] = upload_data
|
|
400
|
+
elif tar_file is not None:
|
|
401
|
+
if not os.path.exists(tar_file):
|
|
402
|
+
raise ValueError(f"tar_file {tar_file} does not exist")
|
|
403
|
+
kwargs["data"] = open(tar_file, "rb")
|
|
404
|
+
else:
|
|
405
|
+
raise ValueError("either data or tar_file must be provided")
|
|
406
|
+
|
|
388
407
|
response = session.put(**kwargs)
|
|
389
408
|
|
|
390
409
|
try:
|
|
@@ -16,10 +16,14 @@ ManifestPythonModel
|
|
|
16
16
|
Class for model-specific instructions for Python apps.
|
|
17
17
|
ManifestPython
|
|
18
18
|
Class for Python-specific instructions in the manifest.
|
|
19
|
+
ManifestOptionUI
|
|
20
|
+
Class for UI attributes of options in the manifest.
|
|
19
21
|
ManifestOption
|
|
20
22
|
Class representing an option for the decision model in the manifest.
|
|
21
23
|
ManifestOptions
|
|
22
24
|
Class containing a list of options for the decision model.
|
|
25
|
+
ManifestValidation
|
|
26
|
+
Class for validation rules for options in the manifest.
|
|
23
27
|
ManifestConfiguration
|
|
24
28
|
Class for configuration settings for the decision model.
|
|
25
29
|
Manifest
|
|
@@ -40,7 +44,7 @@ from pydantic import AliasChoices, Field
|
|
|
40
44
|
|
|
41
45
|
from nextmv.base_model import BaseModel
|
|
42
46
|
from nextmv.model import _REQUIREMENTS_FILE, ModelConfiguration
|
|
43
|
-
from nextmv.options import Option, Options
|
|
47
|
+
from nextmv.options import Option, Options, OptionsEnforcement
|
|
44
48
|
|
|
45
49
|
MANIFEST_FILE_NAME = "app.yaml"
|
|
46
50
|
"""Name of the app manifest file.
|
|
@@ -318,6 +322,43 @@ class ManifestPython(BaseModel):
|
|
|
318
322
|
from the app bundle.
|
|
319
323
|
"""
|
|
320
324
|
|
|
325
|
+
class ManifestOptionUI(BaseModel):
|
|
326
|
+
"""
|
|
327
|
+
UI attributes for an option in the manifest.
|
|
328
|
+
|
|
329
|
+
You can import the `ManifestOptionUI` class directly from `cloud`:
|
|
330
|
+
|
|
331
|
+
```python
|
|
332
|
+
from nextmv.cloud import ManifestOptionUI
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
control_type : str, optional
|
|
338
|
+
The type of control to use for the option in the Nextmv Cloud UI. This is
|
|
339
|
+
useful for defining how the option should be presented in the Nextmv
|
|
340
|
+
Cloud UI. Current control types include "input", "select", "slider", and
|
|
341
|
+
"toggle". This attribute is not used in the local `Options` class, but
|
|
342
|
+
it is used in the Nextmv Cloud UI to define the type of control to use for
|
|
343
|
+
the option. This will be validated by the Nextmv Cloud, and availability
|
|
344
|
+
is based on options_type.
|
|
345
|
+
hidden_from : list[str], optional
|
|
346
|
+
A list of team roles to which this option will be hidden in the UI. For
|
|
347
|
+
example, if you want to hide an option from the "operator" role, you can
|
|
348
|
+
pass `hidden_from=["operator"]`.
|
|
349
|
+
|
|
350
|
+
Examples
|
|
351
|
+
--------
|
|
352
|
+
>>> from nextmv.cloud import ManifestOptionUI
|
|
353
|
+
>>> ui_config = ManifestOptionUI(control_type="input")
|
|
354
|
+
>>> ui_config.control_type
|
|
355
|
+
'input'
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
control_type: Optional[str] = None
|
|
359
|
+
"""The type of control to use for the option in the Nextmv Cloud UI."""
|
|
360
|
+
hidden_from: Optional[list[str]] = None
|
|
361
|
+
"""A list of team roles for which this option will be hidden in the UI."""
|
|
321
362
|
|
|
322
363
|
class ManifestOption(BaseModel):
|
|
323
364
|
"""
|
|
@@ -349,6 +390,12 @@ class ManifestOption(BaseModel):
|
|
|
349
390
|
length of a string or the maximum value of an integer. These
|
|
350
391
|
additional attributes will be shown in the help message of the
|
|
351
392
|
`Options`.
|
|
393
|
+
ui : Optional[ManifestOptionUI], default=None
|
|
394
|
+
Optional UI attributes for the option. This is a dictionary that can
|
|
395
|
+
contain additional information about how the option should be displayed
|
|
396
|
+
in the Nextmv Cloud UI. This is not used in the local `Options` class,
|
|
397
|
+
but it is used in the Nextmv Cloud UI to define how the option should be
|
|
398
|
+
presented.
|
|
352
399
|
|
|
353
400
|
Examples
|
|
354
401
|
--------
|
|
@@ -378,12 +425,10 @@ class ManifestOption(BaseModel):
|
|
|
378
425
|
required: bool = False
|
|
379
426
|
"""Whether the option is required or not"""
|
|
380
427
|
additional_attributes: Optional[dict[str, Any]] = None
|
|
381
|
-
"""Optional additional attributes for the option.
|
|
428
|
+
"""Optional additional attributes for the option."""
|
|
429
|
+
ui: Optional[ManifestOptionUI] = None
|
|
430
|
+
"""Optional UI attributes for the option."""
|
|
382
431
|
|
|
383
|
-
The Nextmv Cloud may perform validation on these attributes. For example,
|
|
384
|
-
the maximum length of a string or the maximum value of an integer. These
|
|
385
|
-
additional attributes will be shown in the help message of the `Options`.
|
|
386
|
-
"""
|
|
387
432
|
|
|
388
433
|
@classmethod
|
|
389
434
|
def from_option(cls, option: Option) -> "ManifestOption":
|
|
@@ -435,6 +480,10 @@ class ManifestOption(BaseModel):
|
|
|
435
480
|
description=option.description,
|
|
436
481
|
required=option.required,
|
|
437
482
|
additional_attributes=option.additional_attributes,
|
|
483
|
+
ui=ManifestOptionUI(
|
|
484
|
+
control_type=option.control_type,
|
|
485
|
+
hidden_from=option.hidden_from,
|
|
486
|
+
) if option.control_type or option.hidden_from else None,
|
|
438
487
|
)
|
|
439
488
|
|
|
440
489
|
def to_option(self) -> Option:
|
|
@@ -481,8 +530,44 @@ class ManifestOption(BaseModel):
|
|
|
481
530
|
description=self.description,
|
|
482
531
|
required=self.required,
|
|
483
532
|
additional_attributes=self.additional_attributes,
|
|
533
|
+
control_type=self.ui.control_type if self.ui else None,
|
|
534
|
+
hidden_from=self.ui.hidden_from if self.ui else None,
|
|
484
535
|
)
|
|
485
536
|
|
|
537
|
+
class ManifestValidation(BaseModel):
|
|
538
|
+
"""
|
|
539
|
+
Validation rules for options in the manifest.
|
|
540
|
+
|
|
541
|
+
You can import the `ManifestValidation` class directly from `cloud`:
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
from nextmv.cloud import ManifestValidation
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Parameters
|
|
548
|
+
----------
|
|
549
|
+
enforce : str, default="none"
|
|
550
|
+
The enforcement level for the validation rules. This can be set to
|
|
551
|
+
"none" or "all". If set to "none", no validation will be performed
|
|
552
|
+
on the options prior to creating a run. If set to "all", all validation
|
|
553
|
+
rules will be enforced on the options, and runs will not be created
|
|
554
|
+
if any of the rules of the options are violated.
|
|
555
|
+
|
|
556
|
+
Examples
|
|
557
|
+
--------
|
|
558
|
+
>>> from nextmv.cloud import ManifestValidation
|
|
559
|
+
>>> validation = ManifestValidation(enforce="all")
|
|
560
|
+
>>> validation.enforce
|
|
561
|
+
'all'
|
|
562
|
+
"""
|
|
563
|
+
|
|
564
|
+
enforce: str = "none"
|
|
565
|
+
"""The enforcement level for the validation rules.
|
|
566
|
+
This can be set to "none" or "all". If set to "none", no validation will
|
|
567
|
+
be performed on the options prior to creating a run. If set to "all", all
|
|
568
|
+
validation rules will be enforced on the options, and runs will not be
|
|
569
|
+
created if any of the rules of the options are violated.
|
|
570
|
+
"""
|
|
486
571
|
|
|
487
572
|
class ManifestOptions(BaseModel):
|
|
488
573
|
"""
|
|
@@ -501,12 +586,16 @@ class ManifestOptions(BaseModel):
|
|
|
501
586
|
items : Optional[list[ManifestOption]], default=None
|
|
502
587
|
The actual list of options for the decision model. An option
|
|
503
588
|
is a parameter that configures the decision model.
|
|
589
|
+
validation: Optional[ManifestValidation], default=None
|
|
590
|
+
Optional validation rules for all options.
|
|
591
|
+
|
|
504
592
|
|
|
505
593
|
Examples
|
|
506
594
|
--------
|
|
507
595
|
>>> from nextmv.cloud import ManifestOptions, ManifestOption
|
|
508
596
|
>>> options_config = ManifestOptions(
|
|
509
597
|
... strict=True,
|
|
598
|
+
... validation=ManifestValidation(enforce="all"),
|
|
510
599
|
... items=[
|
|
511
600
|
... ManifestOption(name="timeout", option_type="int", default=60),
|
|
512
601
|
... ManifestOption(name="vehicle_capacity", option_type="float", default=100.0)
|
|
@@ -520,12 +609,48 @@ class ManifestOptions(BaseModel):
|
|
|
520
609
|
|
|
521
610
|
strict: Optional[bool] = False
|
|
522
611
|
"""If strict is set to `True`, only the listed options will be allowed."""
|
|
612
|
+
validation: Optional[ManifestValidation] = None
|
|
613
|
+
"""Optional validation rules for all options."""
|
|
523
614
|
items: Optional[list[ManifestOption]] = None
|
|
524
615
|
"""The actual list of options for the decision model.
|
|
525
616
|
|
|
526
617
|
An option is a parameter that configures the decision model.
|
|
527
618
|
"""
|
|
528
619
|
|
|
620
|
+
@classmethod
|
|
621
|
+
def from_options(cls, options: Options, validation: OptionsEnforcement = None) -> "ManifestOptions":
|
|
622
|
+
"""
|
|
623
|
+
Create a `ManifestOptions` from a `nextmv.Options`.
|
|
624
|
+
|
|
625
|
+
Parameters
|
|
626
|
+
----------
|
|
627
|
+
options : nextmv.options.Options
|
|
628
|
+
The options to convert.
|
|
629
|
+
validation : Optional[OptionsEnforcement], default=None
|
|
630
|
+
Optional validation rules for the options. If provided, it will be
|
|
631
|
+
used to set the `validation` attribute of the `ManifestOptions`.
|
|
632
|
+
|
|
633
|
+
Returns
|
|
634
|
+
-------
|
|
635
|
+
ManifestOptions
|
|
636
|
+
The converted options.
|
|
637
|
+
|
|
638
|
+
Examples
|
|
639
|
+
--------
|
|
640
|
+
>>> from nextmv.options import Options, Option
|
|
641
|
+
>>> from nextmv.cloud import ManifestOptions
|
|
642
|
+
>>> sdk_options = Options(Option("max_vehicles", int, 5))
|
|
643
|
+
>>> manifest_options = ManifestOptions.from_options(sdk_options)
|
|
644
|
+
>>> manifest_options.items[0].name
|
|
645
|
+
'max_vehicles'
|
|
646
|
+
"""
|
|
647
|
+
|
|
648
|
+
items = [ManifestOption.from_option(option) for option in options.options]
|
|
649
|
+
return cls(
|
|
650
|
+
strict=validation.strict if validation else False,
|
|
651
|
+
validation=ManifestValidation(enforce="all" if validation and validation.validation_enforce else "none"),
|
|
652
|
+
items=items
|
|
653
|
+
)
|
|
529
654
|
|
|
530
655
|
class ManifestConfiguration(BaseModel):
|
|
531
656
|
"""
|
|
@@ -852,16 +977,16 @@ class Manifest(BaseModel):
|
|
|
852
977
|
|
|
853
978
|
if model_configuration.options is not None:
|
|
854
979
|
manifest.configuration = ManifestConfiguration(
|
|
855
|
-
options=ManifestOptions(
|
|
856
|
-
|
|
857
|
-
|
|
980
|
+
options=ManifestOptions.from_options(
|
|
981
|
+
options=model_configuration.options,
|
|
982
|
+
validation=model_configuration.options_enforcement,
|
|
858
983
|
),
|
|
859
984
|
)
|
|
860
985
|
|
|
861
986
|
return manifest
|
|
862
987
|
|
|
863
988
|
@classmethod
|
|
864
|
-
def from_options(cls, options: Options) -> "Manifest":
|
|
989
|
+
def from_options(cls, options: Options, validation: OptionsEnforcement = None) -> "Manifest":
|
|
865
990
|
"""
|
|
866
991
|
Create a basic Python manifest from `nextmv.options.Options`.
|
|
867
992
|
|
|
@@ -882,6 +1007,9 @@ class Manifest(BaseModel):
|
|
|
882
1007
|
----------
|
|
883
1008
|
options : nextmv.options.Options
|
|
884
1009
|
The options to include in the manifest.
|
|
1010
|
+
validation : nextmv.options.OptionsEnforcement default=None
|
|
1011
|
+
The validation rules for the options. This is used to set the
|
|
1012
|
+
`validation` attribute of the `ManifestOptions`.
|
|
885
1013
|
|
|
886
1014
|
Returns
|
|
887
1015
|
-------
|
|
@@ -913,11 +1041,11 @@ class Manifest(BaseModel):
|
|
|
913
1041
|
type=ManifestType.PYTHON,
|
|
914
1042
|
python=ManifestPython(pip_requirements="requirements.txt"),
|
|
915
1043
|
configuration=ManifestConfiguration(
|
|
916
|
-
options=ManifestOptions(
|
|
917
|
-
|
|
918
|
-
|
|
1044
|
+
options= ManifestOptions.from_options(
|
|
1045
|
+
options=options,
|
|
1046
|
+
validation=validation
|
|
919
1047
|
),
|
|
920
|
-
)
|
|
1048
|
+
)
|
|
921
1049
|
)
|
|
922
1050
|
|
|
923
1051
|
return manifest
|
|
@@ -28,7 +28,7 @@ def _package(
|
|
|
28
28
|
model_configuration: Optional[ModelConfiguration] = None,
|
|
29
29
|
verbose: bool = False,
|
|
30
30
|
) -> tuple[str, str]:
|
|
31
|
-
"""Package the app into a tarball
|
|
31
|
+
"""Package the app into a tarball."""
|
|
32
32
|
|
|
33
33
|
with tempfile.TemporaryDirectory(prefix="nextmv-temp-") as temp_dir:
|
|
34
34
|
if manifest.type == ManifestType.PYTHON:
|