album-solution-api 0.7.2__tar.gz → 0.7.3__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.
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/PKG-INFO +2 -1
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/pyproject.toml +2 -1
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/api.py +80 -4
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/model/solution_script.py +52 -13
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/schema/solution_schema.json +1 -1
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album_solution_api.egg-info/PKG-INFO +2 -1
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/LICENSE +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/README.md +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/setup.cfg +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/__init__.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/__init__.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/album_logging.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/__init__.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/api/__init__.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/api/model/__init__.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/api/model/coordinates.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/api/model/solution.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/default_values_runner.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/model/__init__.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/model/coordinates.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/model/solution.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/schema/__init__.py +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album_solution_api.egg-info/SOURCES.txt +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album_solution_api.egg-info/dependency_links.txt +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album_solution_api.egg-info/top_level.txt +0 -0
- {album_solution_api-0.7.2 → album_solution_api-0.7.3}/tests/test_unit_common.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: album-solution-api
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: Album is a framework for unifying computational tools across frameworks, languages, and environments.
|
|
5
5
|
Author-email: Jan Philipp Albrecht <album@jpalbrecht.de>, Kyle Harrington <album@kyleharrington.com>, Deborah Schmidt <mail@frauzufall.de>
|
|
6
6
|
Maintainer-email: Jan Philipp Albrecht <album@jpalbrecht.de>, Kyle Harrington <album@kyleharrington.com>, Deborah Schmidt <mail@frauzufall.de>
|
|
@@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
25
|
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
27
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
27
28
|
Classifier: Topic :: Utilities
|
|
28
29
|
Requires-Python: >=3.6
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "album-solution-api"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.3"
|
|
8
8
|
authors = [
|
|
9
9
|
{name = "Jan Philipp Albrecht", email = "album@jpalbrecht.de"},
|
|
10
10
|
{name = "Kyle Harrington", email = "album@kyleharrington.com"},
|
|
@@ -33,6 +33,7 @@ classifiers = [
|
|
|
33
33
|
"Programming Language :: Python :: 3.11",
|
|
34
34
|
"Programming Language :: Python :: 3.12",
|
|
35
35
|
"Programming Language :: Python :: 3.13",
|
|
36
|
+
"Programming Language :: Python :: 3.14",
|
|
36
37
|
"Programming Language :: Python :: Implementation :: PyPy",
|
|
37
38
|
"Topic :: Utilities"
|
|
38
39
|
]
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""This module provides the API for the album runner."""
|
|
2
|
+
import hashlib
|
|
2
3
|
import json
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
5
6
|
import tarfile
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from typing import Any, Dict, Optional
|
|
8
|
+
from typing import Any, Dict, Optional, Tuple, Union
|
|
8
9
|
from urllib.request import urlretrieve
|
|
9
10
|
|
|
10
11
|
from album.runner.album_logging import configure_logging, get_active_logger, to_loglevel
|
|
@@ -17,6 +18,8 @@ from album.runner.core.model.solution_script import SolutionScript
|
|
|
17
18
|
def download_if_not_exists(url, file_name):
|
|
18
19
|
"""Download resource if not already cached and returns local resource path.
|
|
19
20
|
|
|
21
|
+
Its content is not checked, download_verified checks it against a known hash.
|
|
22
|
+
|
|
20
23
|
Args:
|
|
21
24
|
url: The URL of the download.
|
|
22
25
|
file_name: The local filename of the download.
|
|
@@ -40,6 +43,76 @@ def download_if_not_exists(url, file_name):
|
|
|
40
43
|
return download_path
|
|
41
44
|
|
|
42
45
|
|
|
46
|
+
def download_verified(url: str, file_name: Union[str, Path], known_hash: str) -> Path:
|
|
47
|
+
"""Download a file into the cache of the solution if it is not there, and check its content.
|
|
48
|
+
|
|
49
|
+
The content has to have the hash known_hash, written as in the resource file of a solution:
|
|
50
|
+
``"sha256:<hex digest>"``, another algorithm of hashlib instead of sha256 (e.g. ``"md5:<hex digest>"``), or the
|
|
51
|
+
hex digest alone for sha256. A file in the cache with another content is downloaded again; a download with
|
|
52
|
+
another content is removed.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
url: The URL of the download.
|
|
56
|
+
file_name: The local filename of the download, in the cache of the solution.
|
|
57
|
+
known_hash: The hash of the content.
|
|
58
|
+
|
|
59
|
+
Returns: The path to the file.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If known_hash names an algorithm hashlib does not know, or the download has another hash.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
algorithm, digest = _split_hash(known_hash)
|
|
66
|
+
download_path = get_cache_path().joinpath(file_name)
|
|
67
|
+
|
|
68
|
+
if download_path.exists():
|
|
69
|
+
if _file_hash(download_path, algorithm) == digest:
|
|
70
|
+
get_active_logger().info(f"Found cache of {url}: {download_path}...")
|
|
71
|
+
return download_path
|
|
72
|
+
get_active_logger().info(
|
|
73
|
+
f"The cache of {url} has another content: {download_path}..."
|
|
74
|
+
)
|
|
75
|
+
download_path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
|
|
77
|
+
get_active_logger().info(f"Downloading {url} to {download_path}...")
|
|
78
|
+
|
|
79
|
+
# downloaded next to the file, which it replaces only with the right content
|
|
80
|
+
part_path = download_path.with_name(download_path.name + ".part")
|
|
81
|
+
try:
|
|
82
|
+
urlretrieve(url, part_path)
|
|
83
|
+
download_hash = _file_hash(part_path, algorithm)
|
|
84
|
+
if download_hash == digest:
|
|
85
|
+
os.replace(part_path, download_path)
|
|
86
|
+
return download_path
|
|
87
|
+
finally:
|
|
88
|
+
if part_path.exists():
|
|
89
|
+
part_path.unlink()
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"The download of {url} has the {algorithm} hash {download_hash}, not {digest}."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _split_hash(known_hash: str) -> Tuple[str, str]:
|
|
96
|
+
"""Return the algorithm and the hex digest of a hash, sha256 if it names no algorithm."""
|
|
97
|
+
algorithm, _, digest = known_hash.rpartition(":")
|
|
98
|
+
algorithm = (algorithm or "sha256").lower()
|
|
99
|
+
# a shake algorithm needs the length of its digest
|
|
100
|
+
if algorithm not in hashlib.algorithms_guaranteed or algorithm.startswith("shake_"):
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f'The hash "{known_hash}" names an algorithm hashlib does not know.'
|
|
103
|
+
)
|
|
104
|
+
return algorithm, digest.lower()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _file_hash(path: Path, algorithm: str) -> str:
|
|
108
|
+
"""Return the hex digest of the content of a file."""
|
|
109
|
+
file_hash = hashlib.new(algorithm)
|
|
110
|
+
with open(path, "rb") as file:
|
|
111
|
+
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
|
112
|
+
file_hash.update(chunk)
|
|
113
|
+
return file_hash.hexdigest()
|
|
114
|
+
|
|
115
|
+
|
|
43
116
|
def extract_tar(in_tar, out_dir):
|
|
44
117
|
"""Extract a TAR file to a directory.
|
|
45
118
|
|
|
@@ -103,11 +176,14 @@ def get_cache_path() -> Path:
|
|
|
103
176
|
|
|
104
177
|
|
|
105
178
|
def get_resources_dict(os_specific: bool = False) -> Dict[str, Any]:
|
|
106
|
-
"""Return the
|
|
179
|
+
"""Return the resources of the solution, as declared in its resource file, with the path of each file.
|
|
180
|
+
|
|
181
|
+
The dictionary has the form ``{"resources": {<key>: {"name": ..., "path": ..., ...}}}``. A resource
|
|
182
|
+
meant for another OS is not downloaded, its path is None.
|
|
107
183
|
|
|
108
184
|
Args:
|
|
109
|
-
os_specific: If True,
|
|
110
|
-
|
|
185
|
+
os_specific: If True, only the resources meant for the current OS are returned, otherwise all
|
|
186
|
+
resources are returned.
|
|
111
187
|
"""
|
|
112
188
|
resources_dict = {}
|
|
113
189
|
active_solution = get_active_solution()
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/model/solution_script.py
RENAMED
|
@@ -33,9 +33,24 @@ class SolutionScript:
|
|
|
33
33
|
SolutionScript.api_access(solution, package_path, installation_base_path, environment_path)
|
|
34
34
|
parser = None
|
|
35
35
|
if solution.setup().args:
|
|
36
|
-
|
|
37
|
-
if append_arguments:
|
|
36
|
+
if goal == Solution.Action.RUN:
|
|
38
37
|
parser = SolutionScript.append_arguments(solution)
|
|
38
|
+
elif goal == Solution.Action.TEST:
|
|
39
|
+
# For the test goal the arguments come from the command line and from
|
|
40
|
+
# pre_test(), which has not run at this point. Enforcing `required` here
|
|
41
|
+
# made argparse exit with "the following arguments are required: ..." for
|
|
42
|
+
# every solution that marks an argument required -- before pre_test() had
|
|
43
|
+
# any chance to provide it. Solutions worked around this by declaring
|
|
44
|
+
# required=False and writing "(required)" into the description, which hid
|
|
45
|
+
# the requirement from every consumer of the metadata. So parse the command
|
|
46
|
+
# line without enforcing `required`: pre_test() can read get_args() and
|
|
47
|
+
# adapt what the caller passed, and an invalid command line still fails
|
|
48
|
+
# before pre_test() runs. The parse that enforces `required` happens below,
|
|
49
|
+
# after pre_test()'s values are on the command line.
|
|
50
|
+
parser = SolutionScript.build_parser(solution)
|
|
51
|
+
if parser is not None:
|
|
52
|
+
lenient_parser = SolutionScript.build_parser(solution, enforce_required=False)
|
|
53
|
+
solution.set_args(lenient_parser.parse_args())
|
|
39
54
|
if goal == Solution.Action.INSTALL:
|
|
40
55
|
solution.setup().install()
|
|
41
56
|
if goal == Solution.Action.UNINSTALL:
|
|
@@ -51,8 +66,10 @@ class SolutionScript:
|
|
|
51
66
|
d = {}
|
|
52
67
|
sys.argv = sys.argv + ["=".join([c, d[c]]) for c in d]
|
|
53
68
|
|
|
54
|
-
# parse
|
|
55
|
-
|
|
69
|
+
# The parse that counts for the test goal: pre_test()'s arguments now follow
|
|
70
|
+
# the caller's, so they win, and a required argument pre_test() supplies is
|
|
71
|
+
# satisfied rather than fatal.
|
|
72
|
+
if parser is not None:
|
|
56
73
|
args = parser.parse_args()
|
|
57
74
|
solution.set_args(args)
|
|
58
75
|
|
|
@@ -111,16 +128,32 @@ class SolutionScript:
|
|
|
111
128
|
raise ArgumentError(argument=args, message=message)
|
|
112
129
|
|
|
113
130
|
@staticmethod
|
|
114
|
-
def
|
|
131
|
+
def build_parser(solution: ISolution, enforce_required=True):
|
|
132
|
+
"""The argument parser for a solution's declared arguments, built but not yet parsed.
|
|
133
|
+
|
|
134
|
+
Returns None when the solution declares its arguments as a string ('pass-through'),
|
|
135
|
+
which is validated the same way append_arguments() validates it. Separated from the
|
|
136
|
+
parse so that the test goal can enforce required arguments only after pre_test() has
|
|
137
|
+
run. With enforce_required=False, no argument is marked required: the test goal parses
|
|
138
|
+
the command line that way for pre_test(), which may still supply them.
|
|
139
|
+
"""
|
|
140
|
+
if isinstance(solution.setup().args, str):
|
|
141
|
+
SolutionScript._handle_args_string(solution.setup().args)
|
|
142
|
+
return None
|
|
115
143
|
parser = argparse.ArgumentParser(description='album run %s' % solution.setup().name)
|
|
116
144
|
for arg in solution.setup().args:
|
|
117
|
-
SolutionScript._add_parser_argument(solution, parser, arg)
|
|
145
|
+
SolutionScript._add_parser_argument(solution, parser, arg, enforce_required)
|
|
146
|
+
return parser
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _handle_args_list(solution: ISolution):
|
|
150
|
+
parser = SolutionScript.build_parser(solution)
|
|
118
151
|
args = parser.parse_args()
|
|
119
152
|
solution.set_args(args)
|
|
120
153
|
return parser
|
|
121
154
|
|
|
122
155
|
@staticmethod
|
|
123
|
-
def _add_parser_argument(solution, parser, arg):
|
|
156
|
+
def _add_parser_argument(solution, parser, arg, enforce_required=True):
|
|
124
157
|
keys = arg.keys()
|
|
125
158
|
|
|
126
159
|
if 'default' in keys and 'action' in keys:
|
|
@@ -135,8 +168,8 @@ class SolutionScript:
|
|
|
135
168
|
if 'description' in keys:
|
|
136
169
|
args['help'] = arg['description']
|
|
137
170
|
if 'type' in keys:
|
|
138
|
-
args['type'] = SolutionScript._parse_type(arg['type'])
|
|
139
|
-
if 'required' in keys:
|
|
171
|
+
args['type'] = SolutionScript._parse_type(arg['type'], arg['name'])
|
|
172
|
+
if 'required' in keys and enforce_required:
|
|
140
173
|
args['required'] = arg['required']
|
|
141
174
|
parser.add_argument('--%s' % arg['name'], **args)
|
|
142
175
|
|
|
@@ -156,16 +189,22 @@ class SolutionScript:
|
|
|
156
189
|
raise ValueError("invalid truth value %r" % (val,))
|
|
157
190
|
|
|
158
191
|
@staticmethod
|
|
159
|
-
def _parse_type(type_str):
|
|
160
|
-
|
|
192
|
+
def _parse_type(type_str, arg_name):
|
|
193
|
+
# str, int and bool are the Python names of string, integer and boolean
|
|
194
|
+
if type_str in ('string', 'str'):
|
|
161
195
|
return str
|
|
162
196
|
if type_str == 'file':
|
|
163
197
|
return Path
|
|
164
198
|
if type_str == 'directory':
|
|
165
199
|
return Path
|
|
166
|
-
if type_str
|
|
200
|
+
if type_str in ('integer', 'int'):
|
|
167
201
|
return int
|
|
168
202
|
if type_str == 'float':
|
|
169
203
|
return float
|
|
170
|
-
if type_str
|
|
204
|
+
if type_str in ('boolean', 'bool'):
|
|
171
205
|
return SolutionScript.strtobool
|
|
206
|
+
# without a type function argparse would pass the value on as text
|
|
207
|
+
raise ValueError(
|
|
208
|
+
'The argument %s has the unknown type %r. Known types are str, int, float, bool, file and directory, '
|
|
209
|
+
'and string, integer and boolean.' % (arg_name, type_str)
|
|
210
|
+
)
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"type": {
|
|
65
65
|
"title": "type",
|
|
66
66
|
"type": "string",
|
|
67
|
-
"enum": ["string", "file", "directory", "boolean", "integer", "float"]
|
|
67
|
+
"enum": ["string", "file", "directory", "boolean", "integer", "float", "str", "bool", "int"]
|
|
68
68
|
},
|
|
69
69
|
"default": {
|
|
70
70
|
"title": "default"
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album_solution_api.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: album-solution-api
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
4
4
|
Summary: Album is a framework for unifying computational tools across frameworks, languages, and environments.
|
|
5
5
|
Author-email: Jan Philipp Albrecht <album@jpalbrecht.de>, Kyle Harrington <album@kyleharrington.com>, Deborah Schmidt <mail@frauzufall.de>
|
|
6
6
|
Maintainer-email: Jan Philipp Albrecht <album@jpalbrecht.de>, Kyle Harrington <album@kyleharrington.com>, Deborah Schmidt <mail@frauzufall.de>
|
|
@@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
25
|
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
27
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
27
28
|
Classifier: Topic :: Utilities
|
|
28
29
|
Requires-Python: >=3.6
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/api/model/__init__.py
RENAMED
|
File without changes
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/api/model/coordinates.py
RENAMED
|
File without changes
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/api/model/solution.py
RENAMED
|
File without changes
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/default_values_runner.py
RENAMED
|
File without changes
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/model/__init__.py
RENAMED
|
File without changes
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/model/coordinates.py
RENAMED
|
File without changes
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/model/solution.py
RENAMED
|
File without changes
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album/runner/core/schema/__init__.py
RENAMED
|
File without changes
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album_solution_api.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{album_solution_api-0.7.2 → album_solution_api-0.7.3}/src/album_solution_api.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|