nextmv 0.33.0__tar.gz → 0.34.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.33.0 → nextmv-0.34.0.dev0}/PKG-INFO +1 -1
- nextmv-0.34.0.dev0/nextmv/__about__.py +1 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/__init__.py +1 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/application.py +50 -49
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/local/application.py +50 -25
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/local/executor.py +377 -66
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/local/local.py +1 -1
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/run.py +175 -13
- nextmv-0.33.0/nextmv/__about__.py +0 -1
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/.gitignore +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/LICENSE +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/README.md +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/__entrypoint__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/_serialization.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/base_model.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/acceptance_test.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/account.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/batch_experiment.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/client.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/ensemble.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/input_set.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/instance.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/package.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/scenario.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/secrets.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/url.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/cloud/version.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/default_app/.gitignore +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/default_app/README.md +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/default_app/app.yaml +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/default_app/input.json +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/default_app/main.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/default_app/requirements.txt +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/default_app/src/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/default_app/src/main.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/default_app/src/visuals.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/deprecated.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/input.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/local/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/local/geojson_handler.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/local/plotly_handler.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/local/runner.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/logger.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/manifest.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/model.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/options.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/output.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/polling.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/safe.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/nextmv/status.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/pyproject.toml +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/cloud/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/cloud/app.yaml +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/cloud/test_client.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/cloud/test_package.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/cloud/test_scenario.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/local/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/local/test_application.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/local/test_executor.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/local/test_runner.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/scripts/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/scripts/options1.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/scripts/options2.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/scripts/options3.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/scripts/options4.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/scripts/options5.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/scripts/options6.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/scripts/options7.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/scripts/options_deprecated.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_base_model.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_entrypoint/__init__.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_entrypoint/test_entrypoint.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_input.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_inputs/test_data.csv +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_inputs/test_data.json +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_inputs/test_data.txt +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_logger.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_manifest.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_model.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_options.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_output.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_polling.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_run.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_safe.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_serialization.py +0 -0
- {nextmv-0.33.0 → nextmv-0.34.0.dev0}/tests/test_version.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "v0.34.0.dev0"
|
|
@@ -69,6 +69,7 @@ from .run import RunResult as RunResult
|
|
|
69
69
|
from .run import RunType as RunType
|
|
70
70
|
from .run import RunTypeConfiguration as RunTypeConfiguration
|
|
71
71
|
from .run import StatisticsIndicator as StatisticsIndicator
|
|
72
|
+
from .run import SyncedRun as SyncedRun
|
|
72
73
|
from .run import TrackedRun as TrackedRun
|
|
73
74
|
from .run import TrackedRunStatus as TrackedRunStatus
|
|
74
75
|
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
|
|
@@ -35,9 +35,19 @@ from nextmv.local.runner import run
|
|
|
35
35
|
from nextmv.logger import log
|
|
36
36
|
from nextmv.manifest import 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
|
+
Run,
|
|
44
|
+
RunConfiguration,
|
|
45
|
+
RunInformation,
|
|
46
|
+
RunResult,
|
|
47
|
+
SyncedRun,
|
|
48
|
+
TrackedRun,
|
|
49
|
+
TrackedRunStatus,
|
|
50
|
+
)
|
|
41
51
|
from nextmv.safe import safe_id
|
|
42
52
|
from nextmv.status import StatusV2
|
|
43
53
|
|
|
@@ -995,25 +1005,10 @@ class Application:
|
|
|
995
1005
|
input_type = run_result.metadata.format.format_input.input_type
|
|
996
1006
|
|
|
997
1007
|
# Skip runs that have already been synced.
|
|
998
|
-
already_synced = run_result.
|
|
1008
|
+
synced_run, already_synced = run_result.is_synced(app_id=target.id, instance_id=instance_id)
|
|
999
1009
|
if already_synced:
|
|
1000
1010
|
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
|
-
)
|
|
1011
|
+
log(f" ⏭️ Skipping local run `{run_id}`, already synced with {synced_run.to_dict()}.")
|
|
1017
1012
|
|
|
1018
1013
|
return False
|
|
1019
1014
|
|
|
@@ -1031,7 +1026,7 @@ class Application:
|
|
|
1031
1026
|
# Read the logs of the run and place each line as an element in a list
|
|
1032
1027
|
run_dir = os.path.join(runs_dir, run_id)
|
|
1033
1028
|
with open(os.path.join(run_dir, LOGS_KEY, LOGS_FILE)) as f:
|
|
1034
|
-
stderr_logs = f.readlines()
|
|
1029
|
+
stderr_logs = [line.rstrip("\n") for line in f.readlines()]
|
|
1035
1030
|
|
|
1036
1031
|
# Create the tracked run object and start configuring it.
|
|
1037
1032
|
tracked_run = TrackedRun(
|
|
@@ -1055,11 +1050,28 @@ class Application:
|
|
|
1055
1050
|
tracked_run.input_dir_path = inputs_path
|
|
1056
1051
|
|
|
1057
1052
|
# Resolve the output according to its type.
|
|
1058
|
-
|
|
1053
|
+
output_type = run_result.metadata.format.format_output.output_type
|
|
1054
|
+
if output_type == OutputFormat.JSON:
|
|
1059
1055
|
tracked_run.output = run_result.output
|
|
1060
1056
|
else:
|
|
1061
1057
|
tracked_run.output_dir_path = os.path.join(run_dir, OUTPUTS_KEY, SOLUTIONS_KEY)
|
|
1062
1058
|
|
|
1059
|
+
# Resolve the statistics according to their type and presence. If
|
|
1060
|
+
# working with JSON, the statistics should be resolved from the output.
|
|
1061
|
+
if output_type in {OutputFormat.CSV_ARCHIVE, OutputFormat.MULTI_FILE}:
|
|
1062
|
+
stats_file_path = os.path.join(run_dir, OUTPUTS_KEY, STATISTICS_KEY, f"{STATISTICS_KEY}.json")
|
|
1063
|
+
if os.path.exists(stats_file_path):
|
|
1064
|
+
with open(stats_file_path) as f:
|
|
1065
|
+
tracked_run.statistics = json.load(f)
|
|
1066
|
+
|
|
1067
|
+
# Resolve the assets according to their type and presence. If working
|
|
1068
|
+
# with JSON, the assets should be resolved from the output.
|
|
1069
|
+
if output_type in {OutputFormat.CSV_ARCHIVE, OutputFormat.MULTI_FILE}:
|
|
1070
|
+
assets_file_path = os.path.join(run_dir, OUTPUTS_KEY, ASSETS_KEY, f"{ASSETS_KEY}.json")
|
|
1071
|
+
if os.path.exists(assets_file_path):
|
|
1072
|
+
with open(assets_file_path) as f:
|
|
1073
|
+
tracked_run.assets = json.load(f)
|
|
1074
|
+
|
|
1063
1075
|
# Actually sync the run by tracking it remotely on Nextmv Cloud.
|
|
1064
1076
|
configuration = RunConfiguration(
|
|
1065
1077
|
format=Format(
|
|
@@ -1074,13 +1086,18 @@ class Application:
|
|
|
1074
1086
|
)
|
|
1075
1087
|
|
|
1076
1088
|
# Mark the local run as synced by updating the local run info.
|
|
1077
|
-
|
|
1078
|
-
|
|
1089
|
+
synced_run = SyncedRun(
|
|
1090
|
+
run_id=tracked_id,
|
|
1091
|
+
synced_at=datetime.now(timezone.utc),
|
|
1092
|
+
app_id=target.id,
|
|
1093
|
+
instance_id=instance_id,
|
|
1094
|
+
)
|
|
1095
|
+
run_result.add_synced_run(synced_run)
|
|
1079
1096
|
with open(os.path.join(run_dir, f"{run_id}.json"), "w") as f:
|
|
1080
1097
|
json.dump(run_result.to_dict(), f, indent=2)
|
|
1081
1098
|
|
|
1082
1099
|
if verbose:
|
|
1083
|
-
log(f"✅ Synced local run `{run_id}` as remote run `{
|
|
1100
|
+
log(f"✅ Synced local run `{run_id}` as remote run `{synced_run.to_dict()}`.")
|
|
1084
1101
|
|
|
1085
1102
|
return True
|
|
1086
1103
|
|
|
@@ -1116,7 +1133,15 @@ class Application:
|
|
|
1116
1133
|
return False
|
|
1117
1134
|
|
|
1118
1135
|
# Validate outputs
|
|
1119
|
-
|
|
1136
|
+
format_output = run_result.metadata.format.format_output
|
|
1137
|
+
if format_output is None or not format_output:
|
|
1138
|
+
return False
|
|
1139
|
+
|
|
1140
|
+
output_type = format_output.output_type
|
|
1141
|
+
if output_type is None or output_type == "":
|
|
1142
|
+
return False
|
|
1143
|
+
|
|
1144
|
+
if not self.__validate_outputs(run_dir, output_type):
|
|
1120
1145
|
return False
|
|
1121
1146
|
|
|
1122
1147
|
# Validate logs
|