nextmv 0.34.1.dev1__tar.gz → 0.35.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.34.1.dev1 → nextmv-0.35.0.dev0}/PKG-INFO +1 -1
- nextmv-0.35.0.dev0/nextmv/__about__.py +1 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/package.py +14 -3
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/local/application.py +46 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/local/executor.py +120 -177
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/manifest.py +21 -3
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/output.py +21 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/cloud/test_package.py +11 -1
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/local/test_executor.py +414 -5
- nextmv-0.34.1.dev1/nextmv/__about__.py +0 -1
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/.gitignore +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/LICENSE +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/README.md +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/__entrypoint__.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/__init__.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/_serialization.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/base_model.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/__init__.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/acceptance_test.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/account.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/application.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/batch_experiment.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/client.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/ensemble.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/input_set.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/instance.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/scenario.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/secrets.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/url.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/cloud/version.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/default_app/.gitignore +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/default_app/README.md +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/default_app/app.yaml +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/default_app/input.json +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/default_app/main.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/default_app/requirements.txt +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/default_app/src/__init__.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/default_app/src/main.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/default_app/src/visuals.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/deprecated.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/input.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/local/__init__.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/local/geojson_handler.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/local/local.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/local/plotly_handler.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/local/runner.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/logger.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/model.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/options.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/polling.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/run.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/safe.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/nextmv/status.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/pyproject.toml +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/__init__.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/cloud/__init__.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/cloud/app.yaml +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/cloud/test_client.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/cloud/test_scenario.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/local/__init__.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/local/test_application.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/local/test_runner.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/scripts/__init__.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/scripts/options1.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/scripts/options2.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/scripts/options3.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/scripts/options4.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/scripts/options5.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/scripts/options6.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/scripts/options7.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/scripts/options_deprecated.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_base_model.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_entrypoint/__init__.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_entrypoint/test_entrypoint.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_input.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_inputs/test_data.csv +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_inputs/test_data.json +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_inputs/test_data.txt +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_logger.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_manifest.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_model.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_options.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_output.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_polling.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_run.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_safe.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_serialization.py +0 -0
- {nextmv-0.34.1.dev1 → nextmv-0.35.0.dev0}/tests/test_version.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "v0.35.0.dev0"
|
|
@@ -103,6 +103,19 @@ def _run_build_command(
|
|
|
103
103
|
log(result.stdout)
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
def _get_shell_command_elements(pre_push_command):
|
|
107
|
+
"""Get the shell command elements based on the operating system."""
|
|
108
|
+
# Check if we're in a Unix-like shell (including MINGW on Windows)
|
|
109
|
+
if "SHELL" in os.environ and shutil.which("bash"):
|
|
110
|
+
return ["bash", "-c", pre_push_command]
|
|
111
|
+
# Default to cmd on Windows
|
|
112
|
+
elif platform.system() == "Windows":
|
|
113
|
+
return ["cmd", "/c", pre_push_command]
|
|
114
|
+
# Default to sh on Unix-like systems (Linux, macOS)
|
|
115
|
+
else:
|
|
116
|
+
return ["sh", "-c", pre_push_command]
|
|
117
|
+
|
|
118
|
+
|
|
106
119
|
def _run_pre_push_command(
|
|
107
120
|
app_dir: str,
|
|
108
121
|
pre_push_command: Optional[str] = None,
|
|
@@ -113,9 +126,7 @@ def _run_pre_push_command(
|
|
|
113
126
|
if pre_push_command is None or pre_push_command == "":
|
|
114
127
|
return
|
|
115
128
|
|
|
116
|
-
elements =
|
|
117
|
-
if platform.system() == "Windows":
|
|
118
|
-
elements = ["cmd", "/c", pre_push_command]
|
|
129
|
+
elements = _get_shell_command_elements(pre_push_command)
|
|
119
130
|
|
|
120
131
|
command_str = " ".join(elements)
|
|
121
132
|
log(f'🔨 Running pre-push command: "{command_str}"')
|
|
@@ -493,6 +493,52 @@ class Application:
|
|
|
493
493
|
output_dir_path=output_dir_path,
|
|
494
494
|
)
|
|
495
495
|
|
|
496
|
+
def run_logs(self, run_id: str) -> str:
|
|
497
|
+
"""
|
|
498
|
+
Get the logs of a local run.
|
|
499
|
+
|
|
500
|
+
If the run does not have any logs, or they are empty, then this method
|
|
501
|
+
simply returns a blank string. This method is equivalent to fetching
|
|
502
|
+
the content of the `.nextmv/runs/{run_id}/logs/logs.log` file.
|
|
503
|
+
|
|
504
|
+
Parameters
|
|
505
|
+
----------
|
|
506
|
+
run_id : str
|
|
507
|
+
ID of the run to retrieve logs for.
|
|
508
|
+
|
|
509
|
+
Returns
|
|
510
|
+
-------
|
|
511
|
+
str
|
|
512
|
+
The contents of the logs file for the run.
|
|
513
|
+
|
|
514
|
+
Raises
|
|
515
|
+
------
|
|
516
|
+
ValueError
|
|
517
|
+
If the `.nextmv/runs` directory does not exist at the application
|
|
518
|
+
source, or if the specified run ID does not exist.
|
|
519
|
+
"""
|
|
520
|
+
|
|
521
|
+
runs_dir = os.path.join(self.src, NEXTMV_DIR, RUNS_KEY)
|
|
522
|
+
if not os.path.exists(runs_dir):
|
|
523
|
+
raise ValueError(f"`.nextmv/runs` dir does not exist at app source: {self.src}")
|
|
524
|
+
|
|
525
|
+
run_dir = os.path.join(runs_dir, run_id)
|
|
526
|
+
if not os.path.exists(run_dir):
|
|
527
|
+
raise ValueError(f"`{run_id}` run dir does not exist at: {runs_dir}")
|
|
528
|
+
|
|
529
|
+
logs_dir = os.path.join(runs_dir, LOGS_KEY)
|
|
530
|
+
if not os.path.exists(logs_dir):
|
|
531
|
+
return ""
|
|
532
|
+
|
|
533
|
+
logs_file = os.path.join(logs_dir, LOGS_FILE)
|
|
534
|
+
if not os.path.exists(logs_file):
|
|
535
|
+
return ""
|
|
536
|
+
|
|
537
|
+
with open(logs_file) as f:
|
|
538
|
+
logs = f.read()
|
|
539
|
+
|
|
540
|
+
return logs
|
|
541
|
+
|
|
496
542
|
def run_metadata(self, run_id: str) -> RunInformation:
|
|
497
543
|
"""
|
|
498
544
|
Get the metadata of a local run.
|
|
@@ -32,13 +32,12 @@ process_run_visuals
|
|
|
32
32
|
Function to process and save run visuals.
|
|
33
33
|
resolve_stdout
|
|
34
34
|
Function to parse subprocess stdout output.
|
|
35
|
-
ignore_patterns
|
|
36
|
-
Function to filter files and directories during source code copying.
|
|
37
35
|
"""
|
|
38
36
|
|
|
39
37
|
import hashlib
|
|
40
38
|
import json
|
|
41
39
|
import os
|
|
40
|
+
import re
|
|
42
41
|
import shutil
|
|
43
42
|
import subprocess
|
|
44
43
|
import sys
|
|
@@ -129,7 +128,7 @@ def execute_run(
|
|
|
129
128
|
# place to work from, and be cleaned up afterwards.
|
|
130
129
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
131
130
|
temp_src = os.path.join(temp_dir, "src")
|
|
132
|
-
shutil.copytree(src, temp_src, ignore=
|
|
131
|
+
shutil.copytree(src, temp_src, ignore=_ignore_patterns)
|
|
133
132
|
|
|
134
133
|
manifest = Manifest.from_dict(manifest_dict)
|
|
135
134
|
|
|
@@ -353,7 +352,6 @@ def process_run_output(
|
|
|
353
352
|
stdout_output=stdout_output,
|
|
354
353
|
temp_src=temp_src,
|
|
355
354
|
manifest=manifest,
|
|
356
|
-
src=src,
|
|
357
355
|
)
|
|
358
356
|
process_run_assets(
|
|
359
357
|
temp_run_outputs_dir=temp_run_outputs_dir,
|
|
@@ -361,7 +359,6 @@ def process_run_output(
|
|
|
361
359
|
stdout_output=stdout_output,
|
|
362
360
|
temp_src=temp_src,
|
|
363
361
|
manifest=manifest,
|
|
364
|
-
src=src,
|
|
365
362
|
)
|
|
366
363
|
process_run_solutions(
|
|
367
364
|
run_id=run_id,
|
|
@@ -508,7 +505,6 @@ def process_run_statistics(
|
|
|
508
505
|
stdout_output: Union[str, dict[str, Any]],
|
|
509
506
|
temp_src: str,
|
|
510
507
|
manifest: Manifest,
|
|
511
|
-
src: str,
|
|
512
508
|
) -> None:
|
|
513
509
|
"""
|
|
514
510
|
Processes the statistics of the run. Checks for an outputs/statistics folder
|
|
@@ -527,9 +523,6 @@ def process_run_statistics(
|
|
|
527
523
|
The path to the temporary source directory.
|
|
528
524
|
manifest : Manifest
|
|
529
525
|
The application manifest containing configuration and custom paths.
|
|
530
|
-
src : str
|
|
531
|
-
The path to the original application source code, used to avoid copying
|
|
532
|
-
files that are already part of the source.
|
|
533
526
|
"""
|
|
534
527
|
|
|
535
528
|
stats_dst = os.path.join(outputs_dir, STATISTICS_KEY)
|
|
@@ -553,7 +546,7 @@ def process_run_statistics(
|
|
|
553
546
|
|
|
554
547
|
stats_src = os.path.join(temp_run_outputs_dir, STATISTICS_KEY)
|
|
555
548
|
if os.path.exists(stats_src) and os.path.isdir(stats_src):
|
|
556
|
-
|
|
549
|
+
shutil.copytree(stats_src, stats_dst, dirs_exist_ok=True)
|
|
557
550
|
return
|
|
558
551
|
|
|
559
552
|
if not isinstance(stdout_output, dict):
|
|
@@ -573,7 +566,6 @@ def process_run_assets(
|
|
|
573
566
|
stdout_output: Union[str, dict[str, Any]],
|
|
574
567
|
temp_src: str,
|
|
575
568
|
manifest: Manifest,
|
|
576
|
-
src: str,
|
|
577
569
|
) -> None:
|
|
578
570
|
"""
|
|
579
571
|
Processes the assets of the run. Checks for an outputs/assets folder or
|
|
@@ -592,9 +584,6 @@ def process_run_assets(
|
|
|
592
584
|
The path to the temporary source directory.
|
|
593
585
|
manifest : Manifest
|
|
594
586
|
The application manifest containing configuration and custom paths.
|
|
595
|
-
src : str
|
|
596
|
-
The path to the original application source code, used to avoid copying
|
|
597
|
-
files that are already part of the source.
|
|
598
587
|
"""
|
|
599
588
|
|
|
600
589
|
assets_dst = os.path.join(outputs_dir, ASSETS_KEY)
|
|
@@ -618,7 +607,7 @@ def process_run_assets(
|
|
|
618
607
|
|
|
619
608
|
assets_src = os.path.join(temp_run_outputs_dir, ASSETS_KEY)
|
|
620
609
|
if os.path.exists(assets_src) and os.path.isdir(assets_src):
|
|
621
|
-
|
|
610
|
+
shutil.copytree(assets_src, assets_dst, dirs_exist_ok=True)
|
|
622
611
|
return
|
|
623
612
|
|
|
624
613
|
if not isinstance(stdout_output, dict):
|
|
@@ -684,12 +673,9 @@ def process_run_solutions(
|
|
|
684
673
|
solutions_dst = os.path.join(outputs_dir, SOLUTIONS_KEY)
|
|
685
674
|
os.makedirs(solutions_dst, exist_ok=True)
|
|
686
675
|
|
|
687
|
-
# Build list of directories to exclude from copying
|
|
688
|
-
exclusion_dirs = _build_exclusion_directories(src, manifest, outputs_dir, run_dir)
|
|
689
|
-
|
|
690
676
|
if output_format == OutputFormat.CSV_ARCHIVE:
|
|
691
677
|
output_src = os.path.join(temp_src, OUTPUT_KEY)
|
|
692
|
-
|
|
678
|
+
shutil.copytree(output_src, solutions_dst, dirs_exist_ok=True)
|
|
693
679
|
elif output_format == OutputFormat.MULTI_FILE:
|
|
694
680
|
solutions_src = os.path.join(temp_run_outputs_dir, SOLUTIONS_KEY)
|
|
695
681
|
if (
|
|
@@ -700,7 +686,16 @@ def process_run_solutions(
|
|
|
700
686
|
):
|
|
701
687
|
solutions_src = os.path.join(temp_src, manifest.configuration.content.multi_file.output.solutions)
|
|
702
688
|
|
|
703
|
-
_copy_new_or_modified_files(
|
|
689
|
+
_copy_new_or_modified_files(
|
|
690
|
+
runtime_dir=solutions_src,
|
|
691
|
+
dst_dir=solutions_dst,
|
|
692
|
+
original_src_dir=src,
|
|
693
|
+
exclusion_dirs=[
|
|
694
|
+
os.path.join(outputs_dir, STATISTICS_KEY),
|
|
695
|
+
os.path.join(outputs_dir, ASSETS_KEY),
|
|
696
|
+
os.path.join(run_dir, INPUTS_KEY),
|
|
697
|
+
],
|
|
698
|
+
)
|
|
704
699
|
else:
|
|
705
700
|
if bool(stdout_output):
|
|
706
701
|
with open(os.path.join(solutions_dst, DEFAULT_OUTPUT_JSON_FILE), "w") as f:
|
|
@@ -788,7 +783,7 @@ def resolve_stdout(result: subprocess.CompletedProcess[str]) -> Union[str, dict[
|
|
|
788
783
|
return raw_output
|
|
789
784
|
|
|
790
785
|
|
|
791
|
-
def
|
|
786
|
+
def _ignore_patterns(dir_path: str, names: list[str]) -> list[str]:
|
|
792
787
|
"""
|
|
793
788
|
Custom ignore function for copytree that filters files and directories
|
|
794
789
|
during source code copying. Excludes virtual environments, cache files,
|
|
@@ -817,7 +812,7 @@ def ignore_patterns(dir_path: str, names: list[str]) -> list[str]:
|
|
|
817
812
|
continue
|
|
818
813
|
|
|
819
814
|
# Ignore virtual environment directories
|
|
820
|
-
if
|
|
815
|
+
if re.match(r"^\.?(venv|env|virtualenv).*$", name):
|
|
821
816
|
ignored.append(name)
|
|
822
817
|
continue
|
|
823
818
|
|
|
@@ -840,123 +835,127 @@ def ignore_patterns(dir_path: str, names: list[str]) -> list[str]:
|
|
|
840
835
|
return ignored
|
|
841
836
|
|
|
842
837
|
|
|
843
|
-
def
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
----------
|
|
849
|
-
src : str
|
|
850
|
-
The path to the original application source code.
|
|
851
|
-
manifest : Manifest
|
|
852
|
-
The application manifest containing configuration.
|
|
853
|
-
outputs_dir : str
|
|
854
|
-
The path to the outputs directory in the run directory.
|
|
855
|
-
run_dir : str
|
|
856
|
-
The path to the run directory.
|
|
857
|
-
|
|
858
|
-
Returns
|
|
859
|
-
-------
|
|
860
|
-
list[str]
|
|
861
|
-
List of directory paths to exclude from copying.
|
|
862
|
-
"""
|
|
863
|
-
exclusion_dirs = []
|
|
864
|
-
|
|
865
|
-
# Add inputs directory from original source
|
|
866
|
-
inputs_dir_original = os.path.join(src, INPUTS_KEY)
|
|
867
|
-
if os.path.exists(inputs_dir_original):
|
|
868
|
-
exclusion_dirs.append(inputs_dir_original)
|
|
869
|
-
|
|
870
|
-
# Add custom inputs directory if specified in manifest
|
|
871
|
-
if (
|
|
872
|
-
manifest.configuration is not None
|
|
873
|
-
and manifest.configuration.content is not None
|
|
874
|
-
and manifest.configuration.content.format == InputFormat.MULTI_FILE
|
|
875
|
-
and manifest.configuration.content.multi_file is not None
|
|
876
|
-
):
|
|
877
|
-
custom_inputs_dir = os.path.join(src, manifest.configuration.content.multi_file.input.path)
|
|
878
|
-
if os.path.exists(custom_inputs_dir):
|
|
879
|
-
exclusion_dirs.append(custom_inputs_dir)
|
|
880
|
-
|
|
881
|
-
# Add inputs directory from run directory
|
|
882
|
-
inputs_dir_run = os.path.join(run_dir, INPUTS_KEY)
|
|
883
|
-
if os.path.exists(inputs_dir_run):
|
|
884
|
-
exclusion_dirs.append(inputs_dir_run)
|
|
885
|
-
|
|
886
|
-
# Add statistics and assets directories from run outputs
|
|
887
|
-
stats_dir = os.path.join(outputs_dir, STATISTICS_KEY)
|
|
888
|
-
if os.path.exists(stats_dir):
|
|
889
|
-
exclusion_dirs.append(stats_dir)
|
|
890
|
-
|
|
891
|
-
assets_dir = os.path.join(outputs_dir, ASSETS_KEY)
|
|
892
|
-
if os.path.exists(assets_dir):
|
|
893
|
-
exclusion_dirs.append(assets_dir)
|
|
894
|
-
|
|
895
|
-
return exclusion_dirs
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
def _copy_new_or_modified_files(
|
|
899
|
-
src_dir: str, dst_dir: str, original_src_dir: Optional[str] = None, exclusion_dirs: Optional[list[str]] = None
|
|
838
|
+
def _copy_new_or_modified_files( # noqa: C901
|
|
839
|
+
runtime_dir: str,
|
|
840
|
+
dst_dir: str,
|
|
841
|
+
original_src_dir: Optional[str] = None,
|
|
842
|
+
exclusion_dirs: Optional[list[str]] = None,
|
|
900
843
|
) -> None:
|
|
901
844
|
"""
|
|
902
|
-
Copy
|
|
845
|
+
Copy only new or modified files from runtime directory to destination directory.
|
|
903
846
|
|
|
904
|
-
This function
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
Empty directories are not created or are removed after copying to avoid
|
|
911
|
-
cluttering the output with empty folders.
|
|
847
|
+
This function identifies files that are either new (not present in the original
|
|
848
|
+
source) or have been modified (different content, checksum, or modification time)
|
|
849
|
+
compared to the original source. It excludes files that exist in specified
|
|
850
|
+
exclusion directories to avoid copying input data, statistics, or assets as
|
|
851
|
+
solution outputs.
|
|
912
852
|
|
|
913
853
|
Parameters
|
|
914
854
|
----------
|
|
915
|
-
|
|
916
|
-
The
|
|
855
|
+
runtime_dir : str
|
|
856
|
+
The path to the runtime directory containing files to potentially copy.
|
|
917
857
|
dst_dir : str
|
|
918
|
-
The destination directory
|
|
858
|
+
The destination directory where new or modified files will be copied.
|
|
919
859
|
original_src_dir : Optional[str], optional
|
|
920
|
-
The original source directory
|
|
921
|
-
|
|
860
|
+
The path to the original source directory for comparison, by default None.
|
|
861
|
+
If None, all files from runtime_dir are considered new.
|
|
922
862
|
exclusion_dirs : Optional[list[str]], optional
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
863
|
+
List of directory paths containing files to exclude from copying,
|
|
864
|
+
by default None. Files matching those in exclusion directories will
|
|
865
|
+
not be copied even if they are new or modified.
|
|
866
|
+
"""
|
|
867
|
+
|
|
868
|
+
# Gather a list of the files that are created/modified in the runtime dir,
|
|
869
|
+
# this is, the directory where the actual executable code is run from.
|
|
870
|
+
runtime_files_rel = []
|
|
871
|
+
runtime_files_abs = []
|
|
872
|
+
for root, _, files in os.walk(runtime_dir):
|
|
873
|
+
# Skip __pycache__ directories
|
|
874
|
+
if "__pycache__" in root:
|
|
875
|
+
continue
|
|
876
|
+
|
|
877
|
+
for rel_file in files:
|
|
878
|
+
# Skip .pyc files
|
|
879
|
+
if rel_file.endswith(".pyc"):
|
|
880
|
+
continue
|
|
881
|
+
|
|
882
|
+
file_path = os.path.join(root, rel_file)
|
|
883
|
+
runtime_files_rel.append(os.path.relpath(file_path, runtime_dir))
|
|
884
|
+
runtime_files_abs.append(file_path)
|
|
885
|
+
|
|
886
|
+
# Gather a list of the files that exist in the original source dir. Given
|
|
887
|
+
# that the source dir is copied to the runtime dir before execution, we can
|
|
888
|
+
# use this to determine which files are new or modified.
|
|
889
|
+
original_src_files_rel = set()
|
|
928
890
|
if original_src_dir is not None:
|
|
929
|
-
|
|
891
|
+
for root, _, files in os.walk(original_src_dir):
|
|
892
|
+
for rel_file in files:
|
|
893
|
+
file_path = os.path.join(root, rel_file)
|
|
894
|
+
original_src_files_rel.add(os.path.relpath(file_path, original_src_dir))
|
|
895
|
+
|
|
896
|
+
# Gather a list of the files that exist in the exclusion dirs. This is used
|
|
897
|
+
# to avoid copying files that are part of this special exclusion set.
|
|
898
|
+
exclusion_files_rel = set()
|
|
930
899
|
if exclusion_dirs is not None:
|
|
931
|
-
|
|
900
|
+
for exclusion_dir in exclusion_dirs:
|
|
901
|
+
for root, _, files in os.walk(exclusion_dir):
|
|
902
|
+
for rel_file in files:
|
|
903
|
+
file_path = os.path.join(root, rel_file)
|
|
904
|
+
exclusion_files_rel.add(os.path.relpath(file_path, exclusion_dir))
|
|
905
|
+
|
|
906
|
+
# Now we filter the runtime files to only keep those that are new or
|
|
907
|
+
# modified compared to the original source files.
|
|
908
|
+
files_before_exclusion = []
|
|
909
|
+
for ix, rel_file in enumerate(runtime_files_rel):
|
|
910
|
+
abs_file = runtime_files_abs[ix]
|
|
911
|
+
|
|
912
|
+
# If the file is net new, we keep it.
|
|
913
|
+
if rel_file not in original_src_files_rel:
|
|
914
|
+
files_before_exclusion.append(abs_file)
|
|
915
|
+
continue
|
|
916
|
+
|
|
917
|
+
# If content of the file is different, we keep it.
|
|
918
|
+
runtime_checksum = _calculate_file_checksum(abs_file)
|
|
919
|
+
original_abs_file = os.path.join(original_src_dir, rel_file)
|
|
920
|
+
original_checksum = _calculate_file_checksum(original_abs_file)
|
|
921
|
+
if runtime_checksum != original_checksum:
|
|
922
|
+
files_before_exclusion.append(abs_file)
|
|
923
|
+
continue
|
|
932
924
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
925
|
+
# If content of the file is the same, but the date is newer, we keep it.
|
|
926
|
+
src_mtime = os.path.getmtime(abs_file)
|
|
927
|
+
original_mtime = os.path.getmtime(original_abs_file)
|
|
928
|
+
if src_mtime > original_mtime:
|
|
929
|
+
files_before_exclusion.append(abs_file)
|
|
930
|
+
continue
|
|
937
931
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
932
|
+
# Now we filter out any files that are part of the exclusion set.
|
|
933
|
+
final_files = []
|
|
934
|
+
if exclusion_dirs is not None:
|
|
935
|
+
for file in files_before_exclusion:
|
|
936
|
+
rel_file = os.path.relpath(file, runtime_dir)
|
|
937
|
+
if rel_file in exclusion_files_rel:
|
|
942
938
|
continue
|
|
943
939
|
|
|
944
|
-
|
|
945
|
-
|
|
940
|
+
final_files.append(file)
|
|
941
|
+
else:
|
|
942
|
+
final_files = files_before_exclusion
|
|
943
|
+
|
|
944
|
+
# Now that we have a clean list of files that we are going to copy, we
|
|
945
|
+
# proceed to copy them over to the destination directory.
|
|
946
|
+
for file in final_files:
|
|
947
|
+
rel_file = os.path.relpath(file, runtime_dir)
|
|
948
|
+
dst_file = os.path.join(dst_dir, rel_file)
|
|
946
949
|
|
|
947
|
-
|
|
948
|
-
|
|
950
|
+
# Create the directory structure if it doesn't exist
|
|
951
|
+
dst_file_dir = os.path.dirname(dst_file)
|
|
952
|
+
os.makedirs(dst_file_dir, exist_ok=True)
|
|
949
953
|
|
|
950
|
-
#
|
|
951
|
-
|
|
952
|
-
os.makedirs(dst_root, exist_ok=True)
|
|
953
|
-
for src_file, dst_file in files_to_copy:
|
|
954
|
-
shutil.copy2(src_file, dst_file)
|
|
955
|
-
files_copied = True
|
|
954
|
+
# Copy the file
|
|
955
|
+
shutil.copy2(file, dst_file)
|
|
956
956
|
|
|
957
|
-
#
|
|
958
|
-
|
|
959
|
-
_remove_empty_directories(dst_dir)
|
|
957
|
+
# Finally, we remove any empty directories that might have been created.
|
|
958
|
+
_remove_empty_directories(dst_dir)
|
|
960
959
|
|
|
961
960
|
|
|
962
961
|
def _remove_empty_directories(directory: str) -> None:
|
|
@@ -986,33 +985,6 @@ def _remove_empty_directories(directory: str) -> None:
|
|
|
986
985
|
pass
|
|
987
986
|
|
|
988
987
|
|
|
989
|
-
def _should_copy_file(src_file: str, dst_file: str) -> bool:
|
|
990
|
-
"""
|
|
991
|
-
Determine if a file should be copied based on existence and content.
|
|
992
|
-
|
|
993
|
-
Parameters
|
|
994
|
-
----------
|
|
995
|
-
src_file : str
|
|
996
|
-
Path to the source file.
|
|
997
|
-
dst_file : str
|
|
998
|
-
Path to the destination file.
|
|
999
|
-
|
|
1000
|
-
Returns
|
|
1001
|
-
-------
|
|
1002
|
-
bool
|
|
1003
|
-
True if the file should be copied, False otherwise.
|
|
1004
|
-
"""
|
|
1005
|
-
if not os.path.exists(dst_file):
|
|
1006
|
-
return True
|
|
1007
|
-
|
|
1008
|
-
try:
|
|
1009
|
-
src_checksum = _calculate_file_checksum(src_file)
|
|
1010
|
-
dst_checksum = _calculate_file_checksum(dst_file)
|
|
1011
|
-
return src_checksum != dst_checksum
|
|
1012
|
-
except OSError:
|
|
1013
|
-
return True
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
988
|
def _calculate_file_checksum(file_path: str) -> str:
|
|
1017
989
|
"""
|
|
1018
990
|
Calculate MD5 checksum of a file.
|
|
@@ -1034,34 +1006,5 @@ def _calculate_file_checksum(file_path: str) -> str:
|
|
|
1034
1006
|
return hash_md5.hexdigest()
|
|
1035
1007
|
|
|
1036
1008
|
|
|
1037
|
-
def _file_exists_in_exclusion_dirs(file_name: str, rel_root: str, exclusion_dirs: list[str]) -> bool:
|
|
1038
|
-
"""
|
|
1039
|
-
Check if a file exists in any of the exclusion directories.
|
|
1040
|
-
|
|
1041
|
-
Parameters
|
|
1042
|
-
----------
|
|
1043
|
-
file_name : str
|
|
1044
|
-
The name of the file to check.
|
|
1045
|
-
rel_root : str
|
|
1046
|
-
The relative root path from the source directory.
|
|
1047
|
-
exclusion_dirs : list[str]
|
|
1048
|
-
List of directories to check against.
|
|
1049
|
-
|
|
1050
|
-
Returns
|
|
1051
|
-
-------
|
|
1052
|
-
bool
|
|
1053
|
-
True if the file exists in any exclusion directory, False otherwise.
|
|
1054
|
-
"""
|
|
1055
|
-
for exclusion_dir in exclusion_dirs:
|
|
1056
|
-
if rel_root != ".":
|
|
1057
|
-
exclusion_file = os.path.join(exclusion_dir, rel_root, file_name)
|
|
1058
|
-
else:
|
|
1059
|
-
exclusion_file = os.path.join(exclusion_dir, file_name)
|
|
1060
|
-
|
|
1061
|
-
if os.path.exists(exclusion_file):
|
|
1062
|
-
return True
|
|
1063
|
-
return False
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
1009
|
if __name__ == "__main__":
|
|
1067
1010
|
main()
|
|
@@ -984,8 +984,10 @@ class ManifestConfiguration(BaseModel):
|
|
|
984
984
|
|
|
985
985
|
Parameters
|
|
986
986
|
----------
|
|
987
|
-
options : ManifestOptions
|
|
987
|
+
options : Optional[ManifestOptions], default=None
|
|
988
988
|
Options for the decision model.
|
|
989
|
+
content : Optional[ManifestContent], default=None
|
|
990
|
+
Content configuration for specifying how the app input/output is handled.
|
|
989
991
|
|
|
990
992
|
Examples
|
|
991
993
|
--------
|
|
@@ -1254,13 +1256,25 @@ class Manifest(BaseModel):
|
|
|
1254
1256
|
width=120,
|
|
1255
1257
|
)
|
|
1256
1258
|
|
|
1257
|
-
def extract_options(self) -> Optional[Options]:
|
|
1259
|
+
def extract_options(self, should_parse: bool = True) -> Optional[Options]:
|
|
1258
1260
|
"""
|
|
1259
1261
|
Convert the manifest options to a `nextmv.Options` object.
|
|
1260
1262
|
|
|
1261
1263
|
If the manifest does not have valid options defined in
|
|
1262
1264
|
`.configuration.options.items`, this method returns `None`.
|
|
1263
1265
|
|
|
1266
|
+
Use the `should_parse` argument to decide if you want the options
|
|
1267
|
+
parsed, or not. For more information on option parsing, please read the
|
|
1268
|
+
docstrings on the `.parse()` method of the `nextmv.Options` object.
|
|
1269
|
+
|
|
1270
|
+
Parameters
|
|
1271
|
+
----------
|
|
1272
|
+
should_parse : bool, default=True
|
|
1273
|
+
Whether to parse the options, or not. By default, options are
|
|
1274
|
+
parsed. When command-line arguments are parsed, the help menu is
|
|
1275
|
+
created, thus parsing Options more than once may result in
|
|
1276
|
+
unexpected behavior.
|
|
1277
|
+
|
|
1264
1278
|
Returns
|
|
1265
1279
|
-------
|
|
1266
1280
|
Optional[nextmv.options.Options]
|
|
@@ -1293,7 +1307,11 @@ class Manifest(BaseModel):
|
|
|
1293
1307
|
|
|
1294
1308
|
options = [option.to_option() for option in self.configuration.options.items]
|
|
1295
1309
|
|
|
1296
|
-
|
|
1310
|
+
opt = Options(*options)
|
|
1311
|
+
if should_parse:
|
|
1312
|
+
opt.parse()
|
|
1313
|
+
|
|
1314
|
+
return opt
|
|
1297
1315
|
|
|
1298
1316
|
@classmethod
|
|
1299
1317
|
def from_model_configuration(
|
|
@@ -887,6 +887,27 @@ class Output:
|
|
|
887
887
|
Configuration for writing JSON files. Default is None.
|
|
888
888
|
assets : Optional[list[Union[Asset, dict[str, Any]]]], optional
|
|
889
889
|
List of assets to be included in the output. Default is None.
|
|
890
|
+
solution_files: Optional[list[SolutionFile]], default = None
|
|
891
|
+
Optional list of solution files to be included in the output. These
|
|
892
|
+
files are of type `SolutionFile`, which allows for custom serialization
|
|
893
|
+
and writing of the solution data to files. When this field is
|
|
894
|
+
specified, then the `output_format` must be set to
|
|
895
|
+
`OutputFormat.MULTI_FILE`, otherwise an exception will be raised. The
|
|
896
|
+
`SolutionFile` class allows you to define the name of the file, the
|
|
897
|
+
data to be written, and the writer function that will handle the
|
|
898
|
+
serialization of the data. This is useful when you need to write the
|
|
899
|
+
solution to multiple files with different formats or configurations.
|
|
900
|
+
|
|
901
|
+
There are convenience functions to create `SolutionFile` objects for
|
|
902
|
+
common use cases, such as:
|
|
903
|
+
|
|
904
|
+
- `json_solution_file`: for writing JSON data to a file.
|
|
905
|
+
- `csv_solution_file`: for writing CSV data to a file.
|
|
906
|
+
- `text_solution_file`: for writing utf-8 encoded data to a file.
|
|
907
|
+
|
|
908
|
+
For other data types, such as Excel, you can create your own
|
|
909
|
+
`SolutionFile` objects by providing a `name`, `data`, and a `writer`
|
|
910
|
+
function that will handle the serialization of the data.
|
|
890
911
|
|
|
891
912
|
Raises
|
|
892
913
|
------
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import platform
|
|
2
3
|
import shutil
|
|
4
|
+
import subprocess
|
|
3
5
|
import tempfile
|
|
4
6
|
import unittest
|
|
5
7
|
|
|
6
|
-
from nextmv.cloud.package import _package
|
|
8
|
+
from nextmv.cloud.package import _get_shell_command_elements, _package
|
|
7
9
|
from nextmv.manifest import Manifest, ManifestType
|
|
8
10
|
|
|
9
11
|
|
|
@@ -71,3 +73,11 @@ class TestPackageDir(unittest.TestCase):
|
|
|
71
73
|
with self.assertRaises(Exception) as context:
|
|
72
74
|
_package(self.app_dir, self.manifest, verbose=False)
|
|
73
75
|
self.assertIn("missing mandatory files", str(context.exception))
|
|
76
|
+
|
|
77
|
+
def test_get_shell_command_elements(self):
|
|
78
|
+
if platform.system() == "Windows":
|
|
79
|
+
command = _get_shell_command_elements("echo Hello World")
|
|
80
|
+
subprocess.run(command, check=True)
|
|
81
|
+
else:
|
|
82
|
+
command = _get_shell_command_elements("echo 'Hello World'")
|
|
83
|
+
subprocess.run(command, check=True)
|