nextmv 0.33.0__tar.gz → 0.34.0__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.33.0 → nextmv-0.34.0}/PKG-INFO +1 -1
- nextmv-0.34.0/nextmv/__about__.py +1 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/__init__.py +2 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/application.py +50 -49
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/local/application.py +114 -70
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/local/executor.py +419 -66
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/local/local.py +1 -1
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/manifest.py +134 -24
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/run.py +175 -13
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/local/test_executor.py +44 -12
- nextmv-0.33.0/nextmv/__about__.py +0 -1
- {nextmv-0.33.0 → nextmv-0.34.0}/.gitignore +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/LICENSE +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/README.md +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/__entrypoint__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/_serialization.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/base_model.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/acceptance_test.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/account.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/batch_experiment.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/client.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/ensemble.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/input_set.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/instance.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/package.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/scenario.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/secrets.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/url.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/cloud/version.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/default_app/.gitignore +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/default_app/README.md +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/default_app/app.yaml +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/default_app/input.json +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/default_app/main.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/default_app/requirements.txt +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/default_app/src/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/default_app/src/main.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/default_app/src/visuals.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/deprecated.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/input.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/local/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/local/geojson_handler.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/local/plotly_handler.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/local/runner.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/logger.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/model.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/options.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/output.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/polling.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/safe.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/nextmv/status.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/pyproject.toml +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/cloud/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/cloud/app.yaml +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/cloud/test_client.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/cloud/test_package.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/cloud/test_scenario.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/local/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/local/test_application.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/local/test_runner.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/scripts/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/scripts/options1.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/scripts/options2.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/scripts/options3.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/scripts/options4.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/scripts/options5.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/scripts/options6.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/scripts/options7.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/scripts/options_deprecated.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_base_model.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_entrypoint/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_entrypoint/test_entrypoint.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_input.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_inputs/test_data.csv +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_inputs/test_data.json +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_inputs/test_data.txt +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_logger.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_manifest.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_model.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_options.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_output.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_polling.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_run.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_safe.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_serialization.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0}/tests/test_version.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "v0.34.0"
|
|
@@ -25,6 +25,7 @@ from .manifest import ManifestPythonArch as ManifestPythonArch
|
|
|
25
25
|
from .manifest import ManifestPythonModel as ManifestPythonModel
|
|
26
26
|
from .manifest import ManifestRuntime as ManifestRuntime
|
|
27
27
|
from .manifest import ManifestType as ManifestType
|
|
28
|
+
from .manifest import default_python_manifest as default_python_manifest
|
|
28
29
|
from .model import Model as Model
|
|
29
30
|
from .model import ModelConfiguration as ModelConfiguration
|
|
30
31
|
from .options import Option as Option
|
|
@@ -69,6 +70,7 @@ from .run import RunResult as RunResult
|
|
|
69
70
|
from .run import RunType as RunType
|
|
70
71
|
from .run import RunTypeConfiguration as RunTypeConfiguration
|
|
71
72
|
from .run import StatisticsIndicator as StatisticsIndicator
|
|
73
|
+
from .run import SyncedRun as SyncedRun
|
|
72
74
|
from .run import TrackedRun as TrackedRun
|
|
73
75
|
from .run import TrackedRunStatus as TrackedRunStatus
|
|
74
76
|
from .run import run_duration as run_duration
|
|
@@ -58,7 +58,7 @@ from nextmv.logger import log
|
|
|
58
58
|
from nextmv.manifest import Manifest
|
|
59
59
|
from nextmv.model import Model, ModelConfiguration
|
|
60
60
|
from nextmv.options import Options
|
|
61
|
-
from nextmv.output import Output, OutputFormat
|
|
61
|
+
from nextmv.output import ASSETS_KEY, STATISTICS_KEY, Asset, Output, OutputFormat, Statistics
|
|
62
62
|
from nextmv.polling import DEFAULT_POLLING_OPTIONS, PollingOptions, poll
|
|
63
63
|
from nextmv.run import (
|
|
64
64
|
ExternalRunResult,
|
|
@@ -187,17 +187,20 @@ class Application:
|
|
|
187
187
|
>>> app = Application.new(client=client, name="My New App", id="my-app")
|
|
188
188
|
"""
|
|
189
189
|
|
|
190
|
+
if id is None:
|
|
191
|
+
id = safe_id("app")
|
|
192
|
+
|
|
190
193
|
if exist_ok and cls.exists(client=client, id=id):
|
|
191
194
|
return Application(client=client, id=id)
|
|
192
195
|
|
|
193
196
|
payload = {
|
|
194
197
|
"name": name,
|
|
198
|
+
"id": id,
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
if description is not None:
|
|
198
202
|
payload["description"] = description
|
|
199
|
-
|
|
200
|
-
payload["id"] = id
|
|
203
|
+
|
|
201
204
|
if is_workflow is not None:
|
|
202
205
|
payload["is_pipeline"] = is_workflow
|
|
203
206
|
|
|
@@ -1790,7 +1793,7 @@ class Application:
|
|
|
1790
1793
|
when the run is part of a batch experiment.
|
|
1791
1794
|
external_result: Optional[Union[ExternalRunResult, dict[str, Any]]]
|
|
1792
1795
|
External result to use for the run. This can be a
|
|
1793
|
-
`
|
|
1796
|
+
`nextmv.ExternalRunResult` object or a dict. If the object is used,
|
|
1794
1797
|
then the `.to_dict()` method is applied to extract the
|
|
1795
1798
|
configuration. This is used when the run is an external run. We
|
|
1796
1799
|
suggest that instead of specifying this parameter, you use the
|
|
@@ -1820,8 +1823,6 @@ class Application:
|
|
|
1820
1823
|
not `JSON`. If the final `options` are not of type `dict[str,str]`.
|
|
1821
1824
|
"""
|
|
1822
1825
|
|
|
1823
|
-
self.__validate_input_dir_path_and_configuration(input_dir_path, configuration)
|
|
1824
|
-
|
|
1825
1826
|
tar_file = ""
|
|
1826
1827
|
if input_dir_path is not None and input_dir_path != "":
|
|
1827
1828
|
if not os.path.exists(input_dir_path):
|
|
@@ -1879,6 +1880,7 @@ class Application:
|
|
|
1879
1880
|
query_params = {}
|
|
1880
1881
|
if instance_id is not None or self.default_instance_id is not None:
|
|
1881
1882
|
query_params["instance_id"] = instance_id if instance_id is not None else self.default_instance_id
|
|
1883
|
+
|
|
1882
1884
|
response = self.client.request(
|
|
1883
1885
|
method="POST",
|
|
1884
1886
|
endpoint=f"{self.endpoint}/runs",
|
|
@@ -2993,6 +2995,7 @@ class Application:
|
|
|
2993
2995
|
execution_duration=tracked_run.duration,
|
|
2994
2996
|
)
|
|
2995
2997
|
|
|
2998
|
+
# Handle the stderr logs if provided.
|
|
2996
2999
|
if tracked_run.logs is not None:
|
|
2997
3000
|
url_stderr = self.upload_url()
|
|
2998
3001
|
self.upload_large_input(input=tracked_run.logs_text(), upload_url=url_stderr)
|
|
@@ -3001,6 +3004,47 @@ class Application:
|
|
|
3001
3004
|
if tracked_run.error is not None and tracked_run.error != "":
|
|
3002
3005
|
external_result.error_message = tracked_run.error
|
|
3003
3006
|
|
|
3007
|
+
# Handle the statistics upload if provided.
|
|
3008
|
+
stats = tracked_run.statistics
|
|
3009
|
+
if stats is not None:
|
|
3010
|
+
if isinstance(stats, Statistics):
|
|
3011
|
+
stats_dict = stats.to_dict()
|
|
3012
|
+
stats_dict = {STATISTICS_KEY: stats_dict}
|
|
3013
|
+
elif isinstance(stats, dict):
|
|
3014
|
+
stats_dict = stats
|
|
3015
|
+
if STATISTICS_KEY not in stats_dict:
|
|
3016
|
+
stats_dict = {STATISTICS_KEY: stats_dict}
|
|
3017
|
+
else:
|
|
3018
|
+
raise ValueError("tracked_run.statistics must be either a `Statistics` or `dict` object")
|
|
3019
|
+
|
|
3020
|
+
url_stats = self.upload_url()
|
|
3021
|
+
self.upload_large_input(input=stats_dict, upload_url=url_stats)
|
|
3022
|
+
external_result.statistics_upload_id = url_stats.upload_id
|
|
3023
|
+
|
|
3024
|
+
# Handle the assets upload if provided.
|
|
3025
|
+
assets = tracked_run.assets
|
|
3026
|
+
if assets is not None:
|
|
3027
|
+
if isinstance(assets, list):
|
|
3028
|
+
assets_list = []
|
|
3029
|
+
for ix, asset in enumerate(assets):
|
|
3030
|
+
if isinstance(asset, Asset):
|
|
3031
|
+
assets_list.append(asset.to_dict())
|
|
3032
|
+
elif isinstance(asset, dict):
|
|
3033
|
+
assets_list.append(asset)
|
|
3034
|
+
else:
|
|
3035
|
+
raise ValueError(f"tracked_run.assets, index {ix} must be an `Asset` or `dict` object")
|
|
3036
|
+
assets_dict = {ASSETS_KEY: assets_list}
|
|
3037
|
+
elif isinstance(assets, dict):
|
|
3038
|
+
assets_dict = assets
|
|
3039
|
+
if ASSETS_KEY not in assets_dict:
|
|
3040
|
+
assets_dict = {ASSETS_KEY: assets_dict}
|
|
3041
|
+
else:
|
|
3042
|
+
raise ValueError("tracked_run.assets must be either a `list[Asset]`, `list[dict]`, or `dict` object")
|
|
3043
|
+
|
|
3044
|
+
url_assets = self.upload_url()
|
|
3045
|
+
self.upload_large_input(input=assets_dict, upload_url=url_assets)
|
|
3046
|
+
external_result.assets_upload_id = url_assets.upload_id
|
|
3047
|
+
|
|
3004
3048
|
return self.new_run(
|
|
3005
3049
|
upload_id=url_input.upload_id,
|
|
3006
3050
|
external_result=external_result,
|
|
@@ -3860,49 +3904,6 @@ class Application:
|
|
|
3860
3904
|
|
|
3861
3905
|
raise ValueError(f"Unknown scenario input type: {scenario.scenario_input.scenario_input_type}")
|
|
3862
3906
|
|
|
3863
|
-
def __validate_input_dir_path_and_configuration(
|
|
3864
|
-
self,
|
|
3865
|
-
input_dir_path: Optional[str],
|
|
3866
|
-
configuration: Optional[Union[RunConfiguration, dict[str, Any]]],
|
|
3867
|
-
) -> None:
|
|
3868
|
-
"""
|
|
3869
|
-
Auxiliary function to validate the directory path and configuration.
|
|
3870
|
-
"""
|
|
3871
|
-
input_type = self.__get_input_type(configuration)
|
|
3872
|
-
|
|
3873
|
-
# If no explicit input type is defined, there is nothing to validate.
|
|
3874
|
-
if input_type is None:
|
|
3875
|
-
return
|
|
3876
|
-
|
|
3877
|
-
# Validate that the input directory path is provided when explicitly required.
|
|
3878
|
-
dir_types = (InputFormat.MULTI_FILE, InputFormat.CSV_ARCHIVE)
|
|
3879
|
-
if input_type in dir_types and not input_dir_path:
|
|
3880
|
-
raise ValueError(
|
|
3881
|
-
f"If RunConfiguration.format.format_input.input_type is set to {input_type}, "
|
|
3882
|
-
"then input_dir_path must be provided.",
|
|
3883
|
-
)
|
|
3884
|
-
|
|
3885
|
-
def __get_input_type(self, config: Union[RunConfiguration, dict[str, Any]]) -> Optional[InputFormat]:
|
|
3886
|
-
"""
|
|
3887
|
-
Auxiliary function to extract the input type from the run configuration.
|
|
3888
|
-
"""
|
|
3889
|
-
|
|
3890
|
-
if config is None:
|
|
3891
|
-
return None
|
|
3892
|
-
|
|
3893
|
-
if isinstance(config, dict):
|
|
3894
|
-
config = RunConfiguration.from_dict(config)
|
|
3895
|
-
|
|
3896
|
-
if (
|
|
3897
|
-
isinstance(config, RunConfiguration)
|
|
3898
|
-
and config.format is not None
|
|
3899
|
-
and config.format.format_input is not None
|
|
3900
|
-
and config.format.format_input.input_type is not None
|
|
3901
|
-
):
|
|
3902
|
-
return config.format.format_input.input_type
|
|
3903
|
-
|
|
3904
|
-
return None
|
|
3905
|
-
|
|
3906
3907
|
def __package_inputs(self, dir_path: str) -> str:
|
|
3907
3908
|
"""
|
|
3908
3909
|
This is an auxiliary function for packaging the inputs found in the
|
|
@@ -33,11 +33,22 @@ from nextmv.local.local import (
|
|
|
33
33
|
)
|
|
34
34
|
from nextmv.local.runner import run
|
|
35
35
|
from nextmv.logger import log
|
|
36
|
-
from nextmv.manifest import Manifest
|
|
36
|
+
from nextmv.manifest import Manifest, default_python_manifest
|
|
37
37
|
from nextmv.options import Options
|
|
38
|
-
from nextmv.output import OUTPUTS_KEY, SOLUTIONS_KEY, OutputFormat
|
|
38
|
+
from nextmv.output import ASSETS_KEY, OUTPUTS_KEY, SOLUTIONS_KEY, STATISTICS_KEY, OutputFormat
|
|
39
39
|
from nextmv.polling import DEFAULT_POLLING_OPTIONS, PollingOptions, poll
|
|
40
|
-
from nextmv.run import
|
|
40
|
+
from nextmv.run import (
|
|
41
|
+
ErrorLog,
|
|
42
|
+
Format,
|
|
43
|
+
FormatInput,
|
|
44
|
+
Run,
|
|
45
|
+
RunConfiguration,
|
|
46
|
+
RunInformation,
|
|
47
|
+
RunResult,
|
|
48
|
+
SyncedRun,
|
|
49
|
+
TrackedRun,
|
|
50
|
+
TrackedRunStatus,
|
|
51
|
+
)
|
|
41
52
|
from nextmv.safe import safe_id
|
|
42
53
|
from nextmv.status import StatusV2
|
|
43
54
|
|
|
@@ -83,6 +94,39 @@ class Application:
|
|
|
83
94
|
|
|
84
95
|
description: Optional[str] = None
|
|
85
96
|
"""Description of the application."""
|
|
97
|
+
manifest: Optional[Manifest] = None
|
|
98
|
+
"""
|
|
99
|
+
Manifest of the application. A manifest is a file named `app.yaml` that
|
|
100
|
+
must be present at the root of the application's `src` directory. If the
|
|
101
|
+
app is initialized, and a manifest is not present, a default Python
|
|
102
|
+
manifest will be created, using the `nextmv.default_python_manifest`
|
|
103
|
+
function. If you specify this argument, and a manifest file is already
|
|
104
|
+
present in the `src` directory, the provided manifest will override the
|
|
105
|
+
existing one.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __post_init__(self):
|
|
109
|
+
"""
|
|
110
|
+
Validate the presence of the manifest in the application.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
if self.manifest is not None:
|
|
114
|
+
self.manifest.to_yaml(self.src)
|
|
115
|
+
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
manifest = Manifest.from_yaml(self.src)
|
|
120
|
+
self.manifest = manifest
|
|
121
|
+
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
except Exception:
|
|
125
|
+
manifest = default_python_manifest()
|
|
126
|
+
self.manifest = manifest
|
|
127
|
+
manifest.to_yaml(self.src)
|
|
128
|
+
|
|
129
|
+
return
|
|
86
130
|
|
|
87
131
|
@classmethod
|
|
88
132
|
def initialize(
|
|
@@ -283,7 +327,7 @@ class Application:
|
|
|
283
327
|
>>> print(f"Local run completed with ID: {run_id}")
|
|
284
328
|
"""
|
|
285
329
|
|
|
286
|
-
self.__validate_input_dir_path_and_configuration(input_dir_path, configuration)
|
|
330
|
+
configuration = self.__validate_input_dir_path_and_configuration(input_dir_path, configuration)
|
|
287
331
|
|
|
288
332
|
if self.src is None:
|
|
289
333
|
raise ValueError("`src` property for the `Application` must be specified to run the application locally")
|
|
@@ -843,69 +887,54 @@ class Application:
|
|
|
843
887
|
self,
|
|
844
888
|
input_dir_path: Optional[str],
|
|
845
889
|
configuration: Optional[Union[RunConfiguration, dict[str, Any]]],
|
|
846
|
-
) ->
|
|
890
|
+
) -> RunConfiguration:
|
|
847
891
|
"""
|
|
848
892
|
Auxiliary function to validate the directory path and configuration.
|
|
849
893
|
"""
|
|
850
894
|
|
|
895
|
+
if configuration is None:
|
|
896
|
+
if self.manifest.configuration is not None and self.manifest.configuration.content is not None:
|
|
897
|
+
configuration = RunConfiguration(
|
|
898
|
+
format=Format(
|
|
899
|
+
format_input=FormatInput(
|
|
900
|
+
input_type=self.manifest.configuration.content.format,
|
|
901
|
+
),
|
|
902
|
+
),
|
|
903
|
+
)
|
|
904
|
+
elif isinstance(configuration, dict):
|
|
905
|
+
# Forcefully turn the configuration into a RunConfiguration object to
|
|
906
|
+
# make it easier to deal with in the other functions.
|
|
907
|
+
configuration = RunConfiguration.from_dict(configuration)
|
|
908
|
+
|
|
851
909
|
if input_dir_path is None or input_dir_path == "":
|
|
852
|
-
return
|
|
910
|
+
return configuration
|
|
853
911
|
|
|
854
912
|
if configuration is None:
|
|
855
913
|
raise ValueError(
|
|
856
|
-
"If dir_path is provided, a RunConfiguration must also be provided
|
|
914
|
+
"If `dir_path` is provided, either a `RunConfiguration` must also be provided or "
|
|
915
|
+
"the application's manifest (app.yaml) must include the format under "
|
|
916
|
+
"`configuration.content.format`.",
|
|
857
917
|
)
|
|
858
918
|
|
|
859
|
-
config_format =
|
|
860
|
-
|
|
919
|
+
config_format = configuration.format
|
|
861
920
|
if config_format is None:
|
|
862
921
|
raise ValueError(
|
|
863
|
-
"If dir_path is provided, RunConfiguration.format must also be provided.",
|
|
922
|
+
"If `dir_path` is provided, `RunConfiguration.format` must also be provided.",
|
|
864
923
|
)
|
|
865
924
|
|
|
866
|
-
input_type =
|
|
867
|
-
|
|
868
|
-
if input_type is None or input_type in (InputFormat.JSON, InputFormat.TEXT):
|
|
925
|
+
input_type = config_format.format_input
|
|
926
|
+
if input_type is None:
|
|
869
927
|
raise ValueError(
|
|
870
|
-
"If dir_path is provided, RunConfiguration.format.format_input
|
|
871
|
-
f"Valid types are: {[InputFormat.CSV_ARCHIVE, InputFormat.MULTI_FILE]}",
|
|
928
|
+
"If `dir_path` is provided, `RunConfiguration.format.format_input` must also be provided.",
|
|
872
929
|
)
|
|
873
930
|
|
|
874
|
-
|
|
875
|
-
"""Extract format from configuration, handling both RunConfiguration objects and dicts."""
|
|
876
|
-
if isinstance(configuration, RunConfiguration):
|
|
877
|
-
return configuration.format
|
|
878
|
-
|
|
879
|
-
if isinstance(configuration, dict):
|
|
880
|
-
config_format = configuration.get("format")
|
|
881
|
-
if config_format is not None and isinstance(config_format, dict):
|
|
882
|
-
return Format.from_dict(config_format) if hasattr(Format, "from_dict") else config_format
|
|
883
|
-
|
|
884
|
-
return config_format
|
|
885
|
-
|
|
886
|
-
raise ValueError("Configuration must be a RunConfiguration object or a dict.")
|
|
887
|
-
|
|
888
|
-
def __extract_input_type(self, config_format: Any) -> Any:
|
|
889
|
-
"""Extract input type from config format."""
|
|
890
|
-
if isinstance(config_format, dict):
|
|
891
|
-
format_input = config_format.get("format_input") or config_format.get("input")
|
|
892
|
-
if format_input is None:
|
|
893
|
-
raise ValueError(
|
|
894
|
-
"If dir_path is provided, RunConfiguration.format.format_input must also be provided.",
|
|
895
|
-
)
|
|
896
|
-
|
|
897
|
-
if isinstance(format_input, dict):
|
|
898
|
-
return format_input.get("input_type") or format_input.get("type")
|
|
899
|
-
|
|
900
|
-
return getattr(format_input, "input_type", None)
|
|
901
|
-
|
|
902
|
-
# Handle Format object
|
|
903
|
-
if config_format.format_input is None:
|
|
931
|
+
if input_type is None or input_type in (InputFormat.JSON, InputFormat.TEXT):
|
|
904
932
|
raise ValueError(
|
|
905
|
-
"If dir_path is provided, RunConfiguration.format.format_input must
|
|
933
|
+
"If `dir_path` is provided, `RunConfiguration.format.format_input.input_type` must be set to "
|
|
934
|
+
f"a valid type. Valid types are: {[InputFormat.CSV_ARCHIVE, InputFormat.MULTI_FILE]}",
|
|
906
935
|
)
|
|
907
936
|
|
|
908
|
-
return
|
|
937
|
+
return configuration
|
|
909
938
|
|
|
910
939
|
def __extract_input_data(
|
|
911
940
|
self,
|
|
@@ -995,25 +1024,10 @@ class Application:
|
|
|
995
1024
|
input_type = run_result.metadata.format.format_input.input_type
|
|
996
1025
|
|
|
997
1026
|
# Skip runs that have already been synced.
|
|
998
|
-
already_synced = run_result.
|
|
1027
|
+
synced_run, already_synced = run_result.is_synced(app_id=target.id, instance_id=instance_id)
|
|
999
1028
|
if already_synced:
|
|
1000
1029
|
if verbose:
|
|
1001
|
-
log(f" ⏭️ Skipping local run `{run_id}`, already synced
|
|
1002
|
-
|
|
1003
|
-
return False
|
|
1004
|
-
|
|
1005
|
-
# Skip runs that don't have the supported type. TODO: delete this when
|
|
1006
|
-
# external runs support CSV_ARCHIVE and MULTI_FILE. Right now,
|
|
1007
|
-
# submitting an external result with a new run is limited to JSON and
|
|
1008
|
-
# TEXT. After this if statement is removed, the rest of the code should
|
|
1009
|
-
# work with CSV_ARCHIVE and MULTI_FILE as well, as using the input dir
|
|
1010
|
-
# path is already considered.
|
|
1011
|
-
if input_type not in {InputFormat.JSON, InputFormat.TEXT}:
|
|
1012
|
-
if verbose:
|
|
1013
|
-
log(
|
|
1014
|
-
f" ⏭️ Skipping local run `{run_id}`, unsupported input type: {input_type.value}. "
|
|
1015
|
-
f"Supported types are: {[InputFormat.JSON.value, InputFormat.TEXT.value]}",
|
|
1016
|
-
)
|
|
1030
|
+
log(f" ⏭️ Skipping local run `{run_id}`, already synced with {synced_run.to_dict()}.")
|
|
1017
1031
|
|
|
1018
1032
|
return False
|
|
1019
1033
|
|
|
@@ -1031,7 +1045,7 @@ class Application:
|
|
|
1031
1045
|
# Read the logs of the run and place each line as an element in a list
|
|
1032
1046
|
run_dir = os.path.join(runs_dir, run_id)
|
|
1033
1047
|
with open(os.path.join(run_dir, LOGS_KEY, LOGS_FILE)) as f:
|
|
1034
|
-
stderr_logs = f.
|
|
1048
|
+
stderr_logs = f.read()
|
|
1035
1049
|
|
|
1036
1050
|
# Create the tracked run object and start configuring it.
|
|
1037
1051
|
tracked_run = TrackedRun(
|
|
@@ -1055,11 +1069,28 @@ class Application:
|
|
|
1055
1069
|
tracked_run.input_dir_path = inputs_path
|
|
1056
1070
|
|
|
1057
1071
|
# Resolve the output according to its type.
|
|
1058
|
-
|
|
1072
|
+
output_type = run_result.metadata.format.format_output.output_type
|
|
1073
|
+
if output_type == OutputFormat.JSON:
|
|
1059
1074
|
tracked_run.output = run_result.output
|
|
1060
1075
|
else:
|
|
1061
1076
|
tracked_run.output_dir_path = os.path.join(run_dir, OUTPUTS_KEY, SOLUTIONS_KEY)
|
|
1062
1077
|
|
|
1078
|
+
# Resolve the statistics according to their type and presence. If
|
|
1079
|
+
# working with JSON, the statistics should be resolved from the output.
|
|
1080
|
+
if output_type in {OutputFormat.CSV_ARCHIVE, OutputFormat.MULTI_FILE}:
|
|
1081
|
+
stats_file_path = os.path.join(run_dir, OUTPUTS_KEY, STATISTICS_KEY, f"{STATISTICS_KEY}.json")
|
|
1082
|
+
if os.path.exists(stats_file_path):
|
|
1083
|
+
with open(stats_file_path) as f:
|
|
1084
|
+
tracked_run.statistics = json.load(f)
|
|
1085
|
+
|
|
1086
|
+
# Resolve the assets according to their type and presence. If working
|
|
1087
|
+
# with JSON, the assets should be resolved from the output.
|
|
1088
|
+
if output_type in {OutputFormat.CSV_ARCHIVE, OutputFormat.MULTI_FILE}:
|
|
1089
|
+
assets_file_path = os.path.join(run_dir, OUTPUTS_KEY, ASSETS_KEY, f"{ASSETS_KEY}.json")
|
|
1090
|
+
if os.path.exists(assets_file_path):
|
|
1091
|
+
with open(assets_file_path) as f:
|
|
1092
|
+
tracked_run.assets = json.load(f)
|
|
1093
|
+
|
|
1063
1094
|
# Actually sync the run by tracking it remotely on Nextmv Cloud.
|
|
1064
1095
|
configuration = RunConfiguration(
|
|
1065
1096
|
format=Format(
|
|
@@ -1074,13 +1105,18 @@ class Application:
|
|
|
1074
1105
|
)
|
|
1075
1106
|
|
|
1076
1107
|
# Mark the local run as synced by updating the local run info.
|
|
1077
|
-
|
|
1078
|
-
|
|
1108
|
+
synced_run = SyncedRun(
|
|
1109
|
+
run_id=tracked_id,
|
|
1110
|
+
synced_at=datetime.now(timezone.utc),
|
|
1111
|
+
app_id=target.id,
|
|
1112
|
+
instance_id=instance_id,
|
|
1113
|
+
)
|
|
1114
|
+
run_result.add_synced_run(synced_run)
|
|
1079
1115
|
with open(os.path.join(run_dir, f"{run_id}.json"), "w") as f:
|
|
1080
1116
|
json.dump(run_result.to_dict(), f, indent=2)
|
|
1081
1117
|
|
|
1082
1118
|
if verbose:
|
|
1083
|
-
log(f"✅ Synced local run `{run_id}` as remote run `{
|
|
1119
|
+
log(f"✅ Synced local run `{run_id}` as remote run `{synced_run.to_dict()}`.")
|
|
1084
1120
|
|
|
1085
1121
|
return True
|
|
1086
1122
|
|
|
@@ -1116,7 +1152,15 @@ class Application:
|
|
|
1116
1152
|
return False
|
|
1117
1153
|
|
|
1118
1154
|
# Validate outputs
|
|
1119
|
-
|
|
1155
|
+
format_output = run_result.metadata.format.format_output
|
|
1156
|
+
if format_output is None or not format_output:
|
|
1157
|
+
return False
|
|
1158
|
+
|
|
1159
|
+
output_type = format_output.output_type
|
|
1160
|
+
if output_type is None or output_type == "":
|
|
1161
|
+
return False
|
|
1162
|
+
|
|
1163
|
+
if not self.__validate_outputs(run_dir, output_type):
|
|
1120
1164
|
return False
|
|
1121
1165
|
|
|
1122
1166
|
# Validate logs
|