nextmv 0.33.0.dev0__py3-none-any.whl → 0.34.0.dev0__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 +1 -1
- nextmv/__init__.py +2 -0
- nextmv/cloud/acceptance_test.py +16 -6
- nextmv/cloud/application.py +51 -79
- nextmv/cloud/package.py +62 -24
- nextmv/local/application.py +50 -25
- nextmv/local/executor.py +377 -66
- nextmv/local/local.py +1 -1
- nextmv/manifest.py +61 -2
- nextmv/run.py +175 -13
- {nextmv-0.33.0.dev0.dist-info → nextmv-0.34.0.dev0.dist-info}/METADATA +1 -1
- {nextmv-0.33.0.dev0.dist-info → nextmv-0.34.0.dev0.dist-info}/RECORD +14 -14
- {nextmv-0.33.0.dev0.dist-info → nextmv-0.34.0.dev0.dist-info}/WHEEL +0 -0
- {nextmv-0.33.0.dev0.dist-info → nextmv-0.34.0.dev0.dist-info}/licenses/LICENSE +0 -0
nextmv/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "v0.
|
|
1
|
+
__version__ = "v0.34.0.dev0"
|
nextmv/__init__.py
CHANGED
|
@@ -21,6 +21,7 @@ from .manifest import Manifest as Manifest
|
|
|
21
21
|
from .manifest import ManifestBuild as ManifestBuild
|
|
22
22
|
from .manifest import ManifestOption as ManifestOption
|
|
23
23
|
from .manifest import ManifestPython as ManifestPython
|
|
24
|
+
from .manifest import ManifestPythonArch as ManifestPythonArch
|
|
24
25
|
from .manifest import ManifestPythonModel as ManifestPythonModel
|
|
25
26
|
from .manifest import ManifestRuntime as ManifestRuntime
|
|
26
27
|
from .manifest import ManifestType as ManifestType
|
|
@@ -68,6 +69,7 @@ from .run import RunResult as RunResult
|
|
|
68
69
|
from .run import RunType as RunType
|
|
69
70
|
from .run import RunTypeConfiguration as RunTypeConfiguration
|
|
70
71
|
from .run import StatisticsIndicator as StatisticsIndicator
|
|
72
|
+
from .run import SyncedRun as SyncedRun
|
|
71
73
|
from .run import TrackedRun as TrackedRun
|
|
72
74
|
from .run import TrackedRunStatus as TrackedRunStatus
|
|
73
75
|
from .run import run_duration as run_duration
|
nextmv/cloud/acceptance_test.py
CHANGED
|
@@ -256,16 +256,25 @@ class ToleranceType(str, Enum):
|
|
|
256
256
|
"""ToleranceType is deprecated, please use MetricToleranceType instead.
|
|
257
257
|
Relative tolerance type."""
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
|
|
260
|
+
# Override __getattribute__ to emit deprecation warnings when enum values are accessed
|
|
261
|
+
_original_getattribute = ToleranceType.__class__.__getattribute__
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _deprecated_getattribute(cls, name: str):
|
|
265
|
+
# Only emit deprecation warning if this is specifically the ToleranceType class
|
|
266
|
+
if cls is ToleranceType and name in ("undefined", "absolute", "relative"):
|
|
261
267
|
deprecated(
|
|
262
|
-
"ToleranceType",
|
|
268
|
+
f"ToleranceType.{name}",
|
|
263
269
|
"ToleranceType is deprecated and will be removed in a future version. "
|
|
264
270
|
"Please use MetricToleranceType instead",
|
|
265
271
|
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
272
|
+
|
|
273
|
+
return _original_getattribute(cls, name)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
ToleranceType.__class__.__getattribute__ = _deprecated_getattribute
|
|
277
|
+
|
|
269
278
|
|
|
270
279
|
class MetricToleranceType(str, Enum):
|
|
271
280
|
"""
|
|
@@ -304,6 +313,7 @@ class MetricToleranceType(str, Enum):
|
|
|
304
313
|
relative = "relative"
|
|
305
314
|
"""Relative tolerance type."""
|
|
306
315
|
|
|
316
|
+
|
|
307
317
|
class MetricTolerance(BaseModel):
|
|
308
318
|
"""
|
|
309
319
|
Tolerance used for a metric in an acceptance test.
|
nextmv/cloud/application.py
CHANGED
|
@@ -46,11 +46,7 @@ from nextmv.cloud.batch_experiment import (
|
|
|
46
46
|
to_runs,
|
|
47
47
|
)
|
|
48
48
|
from nextmv.cloud.client import Client, get_size
|
|
49
|
-
from nextmv.cloud.ensemble import
|
|
50
|
-
EnsembleDefinition,
|
|
51
|
-
EvaluationRule,
|
|
52
|
-
RunGroup,
|
|
53
|
-
)
|
|
49
|
+
from nextmv.cloud.ensemble import EnsembleDefinition, EvaluationRule, RunGroup
|
|
54
50
|
from nextmv.cloud.input_set import InputSet, ManagedInput
|
|
55
51
|
from nextmv.cloud.instance import Instance, InstanceConfiguration
|
|
56
52
|
from nextmv.cloud.scenario import Scenario, ScenarioInputType, _option_sets, _scenarios_by_id
|
|
@@ -62,7 +58,7 @@ from nextmv.logger import log
|
|
|
62
58
|
from nextmv.manifest import Manifest
|
|
63
59
|
from nextmv.model import Model, ModelConfiguration
|
|
64
60
|
from nextmv.options import Options
|
|
65
|
-
from nextmv.output import Output, OutputFormat
|
|
61
|
+
from nextmv.output import ASSETS_KEY, STATISTICS_KEY, Asset, Output, OutputFormat, Statistics
|
|
66
62
|
from nextmv.polling import DEFAULT_POLLING_OPTIONS, PollingOptions, poll
|
|
67
63
|
from nextmv.run import (
|
|
68
64
|
ExternalRunResult,
|
|
@@ -191,17 +187,20 @@ class Application:
|
|
|
191
187
|
>>> app = Application.new(client=client, name="My New App", id="my-app")
|
|
192
188
|
"""
|
|
193
189
|
|
|
190
|
+
if id is None:
|
|
191
|
+
id = safe_id("app")
|
|
192
|
+
|
|
194
193
|
if exist_ok and cls.exists(client=client, id=id):
|
|
195
194
|
return Application(client=client, id=id)
|
|
196
195
|
|
|
197
196
|
payload = {
|
|
198
197
|
"name": name,
|
|
198
|
+
"id": id,
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
if description is not None:
|
|
202
202
|
payload["description"] = description
|
|
203
|
-
|
|
204
|
-
payload["id"] = id
|
|
203
|
+
|
|
205
204
|
if is_workflow is not None:
|
|
206
205
|
payload["is_pipeline"] = is_workflow
|
|
207
206
|
|
|
@@ -1794,7 +1793,7 @@ class Application:
|
|
|
1794
1793
|
when the run is part of a batch experiment.
|
|
1795
1794
|
external_result: Optional[Union[ExternalRunResult, dict[str, Any]]]
|
|
1796
1795
|
External result to use for the run. This can be a
|
|
1797
|
-
`
|
|
1796
|
+
`nextmv.ExternalRunResult` object or a dict. If the object is used,
|
|
1798
1797
|
then the `.to_dict()` method is applied to extract the
|
|
1799
1798
|
configuration. This is used when the run is an external run. We
|
|
1800
1799
|
suggest that instead of specifying this parameter, you use the
|
|
@@ -1824,8 +1823,6 @@ class Application:
|
|
|
1824
1823
|
not `JSON`. If the final `options` are not of type `dict[str,str]`.
|
|
1825
1824
|
"""
|
|
1826
1825
|
|
|
1827
|
-
self.__validate_input_dir_path_and_configuration(input_dir_path, configuration)
|
|
1828
|
-
|
|
1829
1826
|
tar_file = ""
|
|
1830
1827
|
if input_dir_path is not None and input_dir_path != "":
|
|
1831
1828
|
if not os.path.exists(input_dir_path):
|
|
@@ -1883,6 +1880,7 @@ class Application:
|
|
|
1883
1880
|
query_params = {}
|
|
1884
1881
|
if instance_id is not None or self.default_instance_id is not None:
|
|
1885
1882
|
query_params["instance_id"] = instance_id if instance_id is not None else self.default_instance_id
|
|
1883
|
+
|
|
1886
1884
|
response = self.client.request(
|
|
1887
1885
|
method="POST",
|
|
1888
1886
|
endpoint=f"{self.endpoint}/runs",
|
|
@@ -2997,6 +2995,7 @@ class Application:
|
|
|
2997
2995
|
execution_duration=tracked_run.duration,
|
|
2998
2996
|
)
|
|
2999
2997
|
|
|
2998
|
+
# Handle the stderr logs if provided.
|
|
3000
2999
|
if tracked_run.logs is not None:
|
|
3001
3000
|
url_stderr = self.upload_url()
|
|
3002
3001
|
self.upload_large_input(input=tracked_run.logs_text(), upload_url=url_stderr)
|
|
@@ -3005,6 +3004,47 @@ class Application:
|
|
|
3005
3004
|
if tracked_run.error is not None and tracked_run.error != "":
|
|
3006
3005
|
external_result.error_message = tracked_run.error
|
|
3007
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
|
+
|
|
3008
3048
|
return self.new_run(
|
|
3009
3049
|
upload_id=url_input.upload_id,
|
|
3010
3050
|
external_result=external_result,
|
|
@@ -3864,74 +3904,6 @@ class Application:
|
|
|
3864
3904
|
|
|
3865
3905
|
raise ValueError(f"Unknown scenario input type: {scenario.scenario_input.scenario_input_type}")
|
|
3866
3906
|
|
|
3867
|
-
def __validate_input_dir_path_and_configuration(
|
|
3868
|
-
self,
|
|
3869
|
-
input_dir_path: Optional[str],
|
|
3870
|
-
configuration: Optional[Union[RunConfiguration, dict[str, Any]]],
|
|
3871
|
-
) -> None:
|
|
3872
|
-
"""
|
|
3873
|
-
Auxiliary function to validate the directory path and configuration.
|
|
3874
|
-
"""
|
|
3875
|
-
|
|
3876
|
-
if input_dir_path is None or input_dir_path == "":
|
|
3877
|
-
return
|
|
3878
|
-
|
|
3879
|
-
if configuration is None:
|
|
3880
|
-
raise ValueError(
|
|
3881
|
-
"If dir_path is provided, a RunConfiguration must also be provided.",
|
|
3882
|
-
)
|
|
3883
|
-
|
|
3884
|
-
config_format = self.__extract_config_format(configuration)
|
|
3885
|
-
|
|
3886
|
-
if config_format is None:
|
|
3887
|
-
raise ValueError(
|
|
3888
|
-
"If dir_path is provided, RunConfiguration.format must also be provided.",
|
|
3889
|
-
)
|
|
3890
|
-
|
|
3891
|
-
input_type = self.__extract_input_type(config_format)
|
|
3892
|
-
|
|
3893
|
-
if input_type is None or input_type in (InputFormat.JSON, InputFormat.TEXT):
|
|
3894
|
-
raise ValueError(
|
|
3895
|
-
"If dir_path is provided, RunConfiguration.format.format_input.input_type must be set to a valid type. "
|
|
3896
|
-
f"Valid types are: {[InputFormat.CSV_ARCHIVE, InputFormat.MULTI_FILE]}",
|
|
3897
|
-
)
|
|
3898
|
-
|
|
3899
|
-
def __extract_config_format(self, configuration: Union[RunConfiguration, dict[str, Any]]) -> Any:
|
|
3900
|
-
"""Extract format from configuration, handling both RunConfiguration objects and dicts."""
|
|
3901
|
-
if isinstance(configuration, RunConfiguration):
|
|
3902
|
-
return configuration.format
|
|
3903
|
-
|
|
3904
|
-
if isinstance(configuration, dict):
|
|
3905
|
-
config_format = configuration.get("format")
|
|
3906
|
-
if config_format is not None and isinstance(config_format, dict):
|
|
3907
|
-
return Format.from_dict(config_format) if hasattr(Format, "from_dict") else config_format
|
|
3908
|
-
|
|
3909
|
-
return config_format
|
|
3910
|
-
|
|
3911
|
-
raise ValueError("Configuration must be a RunConfiguration object or a dict.")
|
|
3912
|
-
|
|
3913
|
-
def __extract_input_type(self, config_format: Any) -> Any:
|
|
3914
|
-
"""Extract input type from config format."""
|
|
3915
|
-
if isinstance(config_format, dict):
|
|
3916
|
-
format_input = config_format.get("format_input") or config_format.get("input")
|
|
3917
|
-
if format_input is None:
|
|
3918
|
-
raise ValueError(
|
|
3919
|
-
"If dir_path is provided, RunConfiguration.format.format_input must also be provided.",
|
|
3920
|
-
)
|
|
3921
|
-
|
|
3922
|
-
if isinstance(format_input, dict):
|
|
3923
|
-
return format_input.get("input_type") or format_input.get("type")
|
|
3924
|
-
|
|
3925
|
-
return getattr(format_input, "input_type", None)
|
|
3926
|
-
|
|
3927
|
-
# Handle Format object
|
|
3928
|
-
if config_format.format_input is None:
|
|
3929
|
-
raise ValueError(
|
|
3930
|
-
"If dir_path is provided, RunConfiguration.format.format_input must also be provided.",
|
|
3931
|
-
)
|
|
3932
|
-
|
|
3933
|
-
return config_format.format_input.input_type
|
|
3934
|
-
|
|
3935
3907
|
def __package_inputs(self, dir_path: str) -> str:
|
|
3936
3908
|
"""
|
|
3937
3909
|
This is an auxiliary function for packaging the inputs found in the
|
nextmv/cloud/package.py
CHANGED
|
@@ -222,7 +222,7 @@ def __handle_python(
|
|
|
222
222
|
__install_dependencies(manifest, app_dir, temp_dir)
|
|
223
223
|
|
|
224
224
|
|
|
225
|
-
def __install_dependencies(
|
|
225
|
+
def __install_dependencies( # noqa: C901 # complexity
|
|
226
226
|
manifest: Manifest,
|
|
227
227
|
app_dir: str,
|
|
228
228
|
temp_dir: str,
|
|
@@ -253,31 +253,58 @@ def __install_dependencies(
|
|
|
253
253
|
if not os.path.isfile(os.path.join(app_dir, pip_requirements)):
|
|
254
254
|
raise FileNotFoundError(f"pip requirements file '{pip_requirements}' not found in '{app_dir}'")
|
|
255
255
|
|
|
256
|
+
platform_filter = []
|
|
257
|
+
if not manifest.python.arch or manifest.python.arch == "arm64":
|
|
258
|
+
platform_filter.extend(
|
|
259
|
+
[
|
|
260
|
+
"--platform=manylinux2014_aarch64",
|
|
261
|
+
"--platform=manylinux_2_17_aarch64",
|
|
262
|
+
"--platform=manylinux_2_24_aarch64",
|
|
263
|
+
"--platform=manylinux_2_28_aarch64",
|
|
264
|
+
"--platform=linux_aarch64",
|
|
265
|
+
]
|
|
266
|
+
)
|
|
267
|
+
elif manifest.python.arch == "amd64":
|
|
268
|
+
platform_filter.extend(
|
|
269
|
+
[
|
|
270
|
+
"--platform=manylinux2014_x86_64",
|
|
271
|
+
"--platform=manylinux_2_17_x86_64",
|
|
272
|
+
"--platform=manylinux_2_24_x86_64",
|
|
273
|
+
"--platform=manylinux_2_28_x86_64",
|
|
274
|
+
"--platform=linux_x86_64",
|
|
275
|
+
]
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
raise Exception(f"unknown architecture '{manifest.python.arch}' specified in manifest")
|
|
279
|
+
|
|
280
|
+
version_filter = ["--python-version=3.11"]
|
|
281
|
+
if manifest.python.version:
|
|
282
|
+
__confirm_python_bundling_version(manifest.python.version)
|
|
283
|
+
version_filter = [f"--python-version={manifest.python.version}"]
|
|
284
|
+
|
|
256
285
|
py_cmd = __get_python_command()
|
|
257
286
|
dep_dir = os.path.join(".nextmv", "python", "deps")
|
|
258
|
-
command =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
"--quiet",
|
|
280
|
-
]
|
|
287
|
+
command = (
|
|
288
|
+
[
|
|
289
|
+
py_cmd,
|
|
290
|
+
"-m",
|
|
291
|
+
"pip",
|
|
292
|
+
"install",
|
|
293
|
+
"-r",
|
|
294
|
+
pip_requirements,
|
|
295
|
+
"--only-binary=:all:",
|
|
296
|
+
"--implementation=cp",
|
|
297
|
+
"--upgrade",
|
|
298
|
+
"--no-warn-conflicts",
|
|
299
|
+
"--target",
|
|
300
|
+
os.path.join(temp_dir, dep_dir),
|
|
301
|
+
"--no-user", # We explicitly avoid user mode (mainly to fix issues with Windows store Python installations)
|
|
302
|
+
"--no-input",
|
|
303
|
+
"--quiet",
|
|
304
|
+
]
|
|
305
|
+
+ platform_filter
|
|
306
|
+
+ version_filter
|
|
307
|
+
)
|
|
281
308
|
result = subprocess.run(
|
|
282
309
|
command,
|
|
283
310
|
cwd=app_dir,
|
|
@@ -381,6 +408,17 @@ def __confirm_python_version(output: str) -> None:
|
|
|
381
408
|
raise Exception("python version 3.9 or higher is required")
|
|
382
409
|
|
|
383
410
|
|
|
411
|
+
def __confirm_python_bundling_version(version: str) -> None:
|
|
412
|
+
# Only accept versions in the form "major.minor" where both are integers
|
|
413
|
+
re_version = re.compile(r"^(\d+)\.(\d+)$")
|
|
414
|
+
match = re_version.fullmatch(version)
|
|
415
|
+
if match:
|
|
416
|
+
major, minor = int(match.group(1)), int(match.group(2))
|
|
417
|
+
if major == 3 and minor >= 9:
|
|
418
|
+
return
|
|
419
|
+
raise Exception(f"python version 3.9 or higher is required for bundling, got {version}")
|
|
420
|
+
|
|
421
|
+
|
|
384
422
|
def __compress_tar(source: str, target: str) -> tuple[str, int]:
|
|
385
423
|
"""Compress the source directory into a tar.gz file in the target"""
|
|
386
424
|
|
nextmv/local/application.py
CHANGED
|
@@ -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
|