nextmv 0.34.0.dev3__py3-none-any.whl → 0.34.1__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/local/application.py +13 -13
- nextmv/local/executor.py +120 -177
- nextmv/manifest.py +73 -4
- nextmv/run.py +4 -3
- {nextmv-0.34.0.dev3.dist-info → nextmv-0.34.1.dist-info}/METADATA +1 -1
- {nextmv-0.34.0.dev3.dist-info → nextmv-0.34.1.dist-info}/RECORD +9 -9
- {nextmv-0.34.0.dev3.dist-info → nextmv-0.34.1.dist-info}/WHEEL +0 -0
- {nextmv-0.34.0.dev3.dist-info → nextmv-0.34.1.dist-info}/licenses/LICENSE +0 -0
nextmv/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "v0.34.
|
|
1
|
+
__version__ = "v0.34.1"
|
nextmv/local/application.py
CHANGED
|
@@ -892,9 +892,6 @@ class Application:
|
|
|
892
892
|
Auxiliary function to validate the directory path and configuration.
|
|
893
893
|
"""
|
|
894
894
|
|
|
895
|
-
if input_dir_path is None or input_dir_path == "":
|
|
896
|
-
return
|
|
897
|
-
|
|
898
895
|
if configuration is None:
|
|
899
896
|
if self.manifest.configuration is not None and self.manifest.configuration.content is not None:
|
|
900
897
|
configuration = RunConfiguration(
|
|
@@ -904,18 +901,21 @@ class Application:
|
|
|
904
901
|
),
|
|
905
902
|
),
|
|
906
903
|
)
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
"the application's manifest (app.yaml) must include the format under "
|
|
911
|
-
"`configuration.content.format`.",
|
|
912
|
-
)
|
|
913
|
-
|
|
914
|
-
# Forcefully turn the configuration into a RunConfiguration object to
|
|
915
|
-
# make it easier to deal with in the other functions.
|
|
916
|
-
if isinstance(configuration, dict):
|
|
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.
|
|
917
907
|
configuration = RunConfiguration.from_dict(configuration)
|
|
918
908
|
|
|
909
|
+
if input_dir_path is None or input_dir_path == "":
|
|
910
|
+
return configuration
|
|
911
|
+
|
|
912
|
+
if configuration is None:
|
|
913
|
+
raise ValueError(
|
|
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`.",
|
|
917
|
+
)
|
|
918
|
+
|
|
919
919
|
config_format = configuration.format
|
|
920
920
|
if config_format is None:
|
|
921
921
|
raise ValueError(
|
nextmv/local/executor.py
CHANGED
|
@@ -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()
|
nextmv/manifest.py
CHANGED
|
@@ -11,6 +11,8 @@ ManifestType
|
|
|
11
11
|
Enum for application types based on programming language.
|
|
12
12
|
ManifestRuntime
|
|
13
13
|
Enum for runtime environments where apps run on Nextmv.
|
|
14
|
+
ManifestPythonArch
|
|
15
|
+
Enum for target architecture for bundling Python apps.
|
|
14
16
|
ManifestBuild
|
|
15
17
|
Class for build-specific attributes in the manifest.
|
|
16
18
|
ManifestPythonModel
|
|
@@ -38,6 +40,11 @@ ManifestConfiguration
|
|
|
38
40
|
Manifest
|
|
39
41
|
Main class representing an app manifest for Nextmv.
|
|
40
42
|
|
|
43
|
+
Functions
|
|
44
|
+
---------
|
|
45
|
+
default_python_manifest
|
|
46
|
+
Creates a default Python manifest as a starting point for applications.
|
|
47
|
+
|
|
41
48
|
Constants
|
|
42
49
|
--------
|
|
43
50
|
MANIFEST_FILE_NAME
|
|
@@ -331,7 +338,7 @@ class ManifestPython(BaseModel):
|
|
|
331
338
|
|
|
332
339
|
Parameters
|
|
333
340
|
----------
|
|
334
|
-
pip_requirements : Optional[str], default=None
|
|
341
|
+
pip_requirements : Optional[Union[str, list[str]]], default=None
|
|
335
342
|
Path to a requirements.txt file containing (additional) Python
|
|
336
343
|
dependencies that will be bundled with the app. Alternatively, you can provide a
|
|
337
344
|
list of strings, each representing a package to install, e.g.,
|
|
@@ -358,10 +365,11 @@ class ManifestPython(BaseModel):
|
|
|
358
365
|
default=None,
|
|
359
366
|
)
|
|
360
367
|
"""
|
|
361
|
-
Path to a requirements.txt file.
|
|
368
|
+
Path to a requirements.txt file or list of packages.
|
|
362
369
|
|
|
363
370
|
Contains (additional) Python dependencies that will be bundled with the
|
|
364
|
-
app.
|
|
371
|
+
app. Can be either a string path to a requirements.txt file or a list
|
|
372
|
+
of package specifications.
|
|
365
373
|
"""
|
|
366
374
|
arch: Optional[ManifestPythonArch] = None
|
|
367
375
|
"""
|
|
@@ -383,6 +391,31 @@ class ManifestPython(BaseModel):
|
|
|
383
391
|
@field_validator("version", mode="before")
|
|
384
392
|
@classmethod
|
|
385
393
|
def validate_version(cls, v: Optional[Union[str, float]]) -> Optional[str]:
|
|
394
|
+
"""
|
|
395
|
+
Validate and convert the Python version field to a string.
|
|
396
|
+
|
|
397
|
+
This validator allows the version to be specified as either a float or string
|
|
398
|
+
in the manifest for convenience, but ensures it's stored internally as a string.
|
|
399
|
+
|
|
400
|
+
Parameters
|
|
401
|
+
----------
|
|
402
|
+
v : Optional[Union[str, float]]
|
|
403
|
+
The version value to validate. Can be None, a string, or a float.
|
|
404
|
+
|
|
405
|
+
Returns
|
|
406
|
+
-------
|
|
407
|
+
Optional[str]
|
|
408
|
+
The version as a string, or None if the input was None.
|
|
409
|
+
|
|
410
|
+
Examples
|
|
411
|
+
--------
|
|
412
|
+
>>> ManifestPython.validate_version(3.11)
|
|
413
|
+
'3.11'
|
|
414
|
+
>>> ManifestPython.validate_version("3.11")
|
|
415
|
+
'3.11'
|
|
416
|
+
>>> ManifestPython.validate_version(None) is None
|
|
417
|
+
True
|
|
418
|
+
"""
|
|
386
419
|
# We allow the version to be a float in the manifest for convenience, but we want
|
|
387
420
|
# to store it as a string internally.
|
|
388
421
|
if v is None:
|
|
@@ -917,7 +950,23 @@ class ManifestContent(BaseModel):
|
|
|
917
950
|
"""Configuration for multi-file content format."""
|
|
918
951
|
|
|
919
952
|
def model_post_init(self, __context) -> None:
|
|
920
|
-
"""
|
|
953
|
+
"""
|
|
954
|
+
Post-initialization validation to ensure format field contains valid values.
|
|
955
|
+
|
|
956
|
+
This method is automatically called by Pydantic after the model is initialized
|
|
957
|
+
to validate that the format field contains one of the acceptable values.
|
|
958
|
+
|
|
959
|
+
Parameters
|
|
960
|
+
----------
|
|
961
|
+
__context : Any
|
|
962
|
+
Pydantic context (unused in this implementation).
|
|
963
|
+
|
|
964
|
+
Raises
|
|
965
|
+
------
|
|
966
|
+
ValueError
|
|
967
|
+
If the format field contains an invalid value that is not one of the
|
|
968
|
+
acceptable formats (JSON, MULTI_FILE, or CSV_ARCHIVE).
|
|
969
|
+
"""
|
|
921
970
|
acceptable_formats = [InputFormat.JSON, InputFormat.MULTI_FILE, InputFormat.CSV_ARCHIVE]
|
|
922
971
|
if self.format not in acceptable_formats:
|
|
923
972
|
raise ValueError(f"Invalid format: {self.format}. Must be one of {acceptable_formats}.")
|
|
@@ -1084,6 +1133,26 @@ class Manifest(BaseModel):
|
|
|
1084
1133
|
"""
|
|
1085
1134
|
|
|
1086
1135
|
def model_post_init(self, __context) -> None:
|
|
1136
|
+
"""
|
|
1137
|
+
Post-initialization to set default entrypoint based on runtime if not specified.
|
|
1138
|
+
|
|
1139
|
+
This method is automatically called by Pydantic after the model is initialized.
|
|
1140
|
+
If no entrypoint is provided, it sets a default entrypoint based on the runtime:
|
|
1141
|
+
- Python runtimes (PYTHON, HEXALY, PYOMO, CUOPT): "./main.py"
|
|
1142
|
+
- DEFAULT runtime: "./main"
|
|
1143
|
+
- JAVA runtime: "./main.jar"
|
|
1144
|
+
|
|
1145
|
+
Parameters
|
|
1146
|
+
----------
|
|
1147
|
+
__context : Any
|
|
1148
|
+
Pydantic context (unused in this implementation).
|
|
1149
|
+
|
|
1150
|
+
Raises
|
|
1151
|
+
------
|
|
1152
|
+
ValueError
|
|
1153
|
+
If no entrypoint is provided and the runtime cannot be resolved to
|
|
1154
|
+
establish a default entrypoint.
|
|
1155
|
+
"""
|
|
1087
1156
|
if self.entrypoint is None:
|
|
1088
1157
|
if self.runtime in (
|
|
1089
1158
|
ManifestRuntime.PYTHON,
|
nextmv/run.py
CHANGED
|
@@ -682,9 +682,10 @@ class Metadata(BaseModel):
|
|
|
682
682
|
"""Format of the input and output of the run."""
|
|
683
683
|
status_v2: StatusV2
|
|
684
684
|
"""Status of the run."""
|
|
685
|
-
|
|
686
685
|
status: Optional[Status] = None
|
|
687
686
|
"""Deprecated: use status_v2."""
|
|
687
|
+
statistics: Optional[dict[str, Any]] = None
|
|
688
|
+
"""User defined statistics of the run."""
|
|
688
689
|
|
|
689
690
|
|
|
690
691
|
class SyncedRun(BaseModel):
|
|
@@ -1560,9 +1561,9 @@ class TrackedRun:
|
|
|
1560
1561
|
|
|
1561
1562
|
if isinstance(self.output, Output):
|
|
1562
1563
|
try:
|
|
1563
|
-
_ = serialize_json(self.output.
|
|
1564
|
+
_ = serialize_json(self.output.solution)
|
|
1564
1565
|
except (TypeError, OverflowError) as e:
|
|
1565
|
-
raise ValueError("Output.
|
|
1566
|
+
raise ValueError("`Output.solution` is not JSON serializable") from e
|
|
1566
1567
|
elif isinstance(self.output, dict):
|
|
1567
1568
|
try:
|
|
1568
1569
|
_ = serialize_json(self.output)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
nextmv/__about__.py,sha256=
|
|
1
|
+
nextmv/__about__.py,sha256=EUse0zN7HR8nM-KxKO37c1BrPbYediyzvlZFeAsg5ng,24
|
|
2
2
|
nextmv/__entrypoint__.py,sha256=dA0iwwHtrq6Z9w9FxmxKLoBGLyhe7jWtUAU-Y3PEgHg,1094
|
|
3
3
|
nextmv/__init__.py,sha256=C2f8MteVvvOX1Wj-0GFjfUt-0RzCT0zpLpLI2yyZQw8,3796
|
|
4
4
|
nextmv/_serialization.py,sha256=JlSl6BL0M2Esf7F89GsGIZ__Pp8RnFRNM0UxYhuuYU4,2853
|
|
@@ -6,12 +6,12 @@ nextmv/base_model.py,sha256=qmJ4AsYr9Yv01HQX_BERrn3229gyoZrYyP9tcyqNfeU,2311
|
|
|
6
6
|
nextmv/deprecated.py,sha256=kEVfyQ-nT0v2ePXTNldjQG9uH5IlfQVy3L4tztIxwmU,1638
|
|
7
7
|
nextmv/input.py,sha256=m9sVfO9ZL3F5i1l8amEtlWlbkekyUP4C3y9DduHWGFs,40211
|
|
8
8
|
nextmv/logger.py,sha256=kNIbu46MisrzYe4T0hNMpWfRTKKacDVvbtQcNys_c_E,2513
|
|
9
|
-
nextmv/manifest.py,sha256=
|
|
9
|
+
nextmv/manifest.py,sha256=vo4GJk2yF6-6cA1M5YrynlXKqupH2vQE7HByph6ne4k,49256
|
|
10
10
|
nextmv/model.py,sha256=vI3pSV3iTwjRPflar7nAg-6h98XRUyi9II5O2J06-Kc,15018
|
|
11
11
|
nextmv/options.py,sha256=yPJu5lYMbV6YioMwAXv7ctpZUggLXKlZc9CqIbUFvE4,37895
|
|
12
12
|
nextmv/output.py,sha256=HdvWYG3gIzwoXquulaEVI4LLchXJDjkbag0BkBPM0vQ,55128
|
|
13
13
|
nextmv/polling.py,sha256=nfefvWI1smm-lIzaXE-4DMlojp6KXIvVi88XLJYUmo8,9724
|
|
14
|
-
nextmv/run.py,sha256=
|
|
14
|
+
nextmv/run.py,sha256=DcVM67ot961RDeuP-jFqNj0Xq4uxXIap_hRNIdCAT_I,52504
|
|
15
15
|
nextmv/safe.py,sha256=VAK4fGEurbLNji4Pg5Okga5XQSbI4aI9JJf95_68Z20,3867
|
|
16
16
|
nextmv/status.py,sha256=SCDLhh2om3yeO5FxO0x-_RShQsZNXEpjHNdCGdb3VUI,2787
|
|
17
17
|
nextmv/cloud/__init__.py,sha256=2wI72lhWq81BYv1OpS0OOTT5-3sivpX0H4z5ANPoLMc,5051
|
|
@@ -38,13 +38,13 @@ nextmv/default_app/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
|
38
38
|
nextmv/default_app/src/main.py,sha256=WWeN_xl_mcPhICl3rSCvdEjRkFXGmAnej88FhS-fAmc,884
|
|
39
39
|
nextmv/default_app/src/visuals.py,sha256=WYK_YBnLmYo3TpVev1CpoNCuW5R7hk9QIkeCmvMn1Fs,1014
|
|
40
40
|
nextmv/local/__init__.py,sha256=6BsoqlK4dw6X11_uKzz9gBPfxKpdiol2FYO8R3X73SE,116
|
|
41
|
-
nextmv/local/application.py,sha256=
|
|
42
|
-
nextmv/local/executor.py,sha256=
|
|
41
|
+
nextmv/local/application.py,sha256=yJDlbQB_mh29Y541Mt6a6zyT3XutdtzjqSd0mlOeteo,47002
|
|
42
|
+
nextmv/local/executor.py,sha256=c0R6Pr2Lawx08YguxMydsXSGAc0PgMR3iTxpNnPpaqY,35459
|
|
43
43
|
nextmv/local/geojson_handler.py,sha256=7FavJdkUonop-yskjis0x3qFGB8A5wZyoBUblw-bVhw,12540
|
|
44
44
|
nextmv/local/local.py,sha256=cp56UpI8h19Ob6Jvb_Ni0ceXH5Vv3ET_iPTDe6ftq3Y,2617
|
|
45
45
|
nextmv/local/plotly_handler.py,sha256=bLb50e3AkVr_W-F6S7lXfeRdN60mG2jk3UElNmhoMWU,1930
|
|
46
46
|
nextmv/local/runner.py,sha256=hwkITHrQG_J9TzxufnaP1mjLWG-iSsNQD66UFZY4pp4,8602
|
|
47
|
-
nextmv-0.34.
|
|
48
|
-
nextmv-0.34.
|
|
49
|
-
nextmv-0.34.
|
|
50
|
-
nextmv-0.34.
|
|
47
|
+
nextmv-0.34.1.dist-info/METADATA,sha256=1f7yKaCgvQuaFl97AfIYo4xbI9ncWM5s1Z7xLwEIHfE,16008
|
|
48
|
+
nextmv-0.34.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
49
|
+
nextmv-0.34.1.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
|
|
50
|
+
nextmv-0.34.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|