album-solution-api 0.6.0__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.
Files changed (26) hide show
  1. album-solution-api-0.6.0/LICENSE +21 -0
  2. album-solution-api-0.6.0/PKG-INFO +29 -0
  3. album-solution-api-0.6.0/README.md +3 -0
  4. album-solution-api-0.6.0/pyproject.toml +6 -0
  5. album-solution-api-0.6.0/setup.cfg +44 -0
  6. album-solution-api-0.6.0/setup.py +3 -0
  7. album-solution-api-0.6.0/src/album/__init__.py +1 -0
  8. album-solution-api-0.6.0/src/album/runner/__init__.py +0 -0
  9. album-solution-api-0.6.0/src/album/runner/album_logging.py +196 -0
  10. album-solution-api-0.6.0/src/album/runner/api.py +187 -0
  11. album-solution-api-0.6.0/src/album/runner/core/__init__.py +0 -0
  12. album-solution-api-0.6.0/src/album/runner/core/api/__init__.py +0 -0
  13. album-solution-api-0.6.0/src/album/runner/core/api/model/__init__.py +0 -0
  14. album-solution-api-0.6.0/src/album/runner/core/api/model/coordinates.py +18 -0
  15. album-solution-api-0.6.0/src/album/runner/core/api/model/solution.py +103 -0
  16. album-solution-api-0.6.0/src/album/runner/core/default_values_runner.py +20 -0
  17. album-solution-api-0.6.0/src/album/runner/core/model/__init__.py +0 -0
  18. album-solution-api-0.6.0/src/album/runner/core/model/coordinates.py +31 -0
  19. album-solution-api-0.6.0/src/album/runner/core/model/solution.py +112 -0
  20. album-solution-api-0.6.0/src/album/runner/core/model/solution_script.py +173 -0
  21. album-solution-api-0.6.0/src/album/runner/core/schema/__init__.py +0 -0
  22. album-solution-api-0.6.0/src/album/runner/core/schema/solution_schema.json +287 -0
  23. album-solution-api-0.6.0/src/album_solution_api.egg-info/PKG-INFO +29 -0
  24. album-solution-api-0.6.0/src/album_solution_api.egg-info/SOURCES.txt +25 -0
  25. album-solution-api-0.6.0/src/album_solution_api.egg-info/dependency_links.txt +1 -0
  26. album-solution-api-0.6.0/src/album_solution_api.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021, album developers.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.1
2
+ Name: album-solution-api
3
+ Version: 0.6.0
4
+ Summary: Album is a framework for unifying computational tools across frameworks, languages, and environments.
5
+ Home-page: https://gitlab.com/album-app/album-solution-api
6
+ Author: Kyle Harrington, Jan Philipp Albrecht, Deborah Schmidt
7
+ Maintainer: Kyle Harrington, Jan Philipp Albrecht, Deborah Schmidt
8
+ Maintainer-email: album@kyleharrington.com
9
+ License: MIT
10
+ Project-URL: Source, https://gitlab.com/album-app/album-solution-api
11
+ Project-URL: Tracker, https://gitlab.com/album-app/album-solution-api/issues
12
+ Keywords: virtual,environments,imaging
13
+ Platform: any
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS :: MacOS X
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: POSIX
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.6
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+
27
+ # album-solution-api
28
+
29
+ Package installed in any Album solution environment. Provides the API for the solution script.
@@ -0,0 +1,3 @@
1
+ # album-solution-api
2
+
3
+ Package installed in any Album solution environment. Provides the API for the solution script.
@@ -0,0 +1,6 @@
1
+ [build-system]
2
+ requires = [
3
+ "setuptools>=42",
4
+ "wheel"
5
+ ]
6
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,44 @@
1
+ [metadata]
2
+ name = album-solution-api
3
+ version = 0.6.0
4
+ description = Album is a framework for unifying computational tools across frameworks, languages, and environments.
5
+ long_description = file: README.md
6
+ long_description_content_type = text/markdown
7
+ url = https://gitlab.com/album-app/album-solution-api
8
+ author = Kyle Harrington, Jan Philipp Albrecht, Deborah Schmidt
9
+ maintainer = Kyle Harrington, Jan Philipp Albrecht, Deborah Schmidt
10
+ maintainer_email = album@kyleharrington.com
11
+ license = MIT
12
+ license_file = LICENSE
13
+ platforms = any
14
+ classifiers =
15
+ Development Status :: 3 - Alpha
16
+ License :: OSI Approved :: MIT License
17
+ Operating System :: MacOS :: MacOS X
18
+ Operating System :: Microsoft :: Windows
19
+ Operating System :: POSIX
20
+ Programming Language :: Python :: 3.8
21
+ Programming Language :: Python :: 3.9
22
+ Programming Language :: Python :: Implementation :: PyPy
23
+ Topic :: Utilities
24
+ keywords = virtual, environments, imaging
25
+ project_urls =
26
+ Source=https://gitlab.com/album-app/album-solution-api
27
+ Tracker=https://gitlab.com/album-app/album-solution-api/issues
28
+
29
+ [options]
30
+ package_dir =
31
+ =src
32
+ packages = find_namespace:
33
+ python_requires = >=3.6
34
+
35
+ [options.package_data]
36
+ * = *.json
37
+
38
+ [options.packages.find]
39
+ where = src
40
+
41
+ [egg_info]
42
+ tag_build =
43
+ tag_date = 0
44
+
@@ -0,0 +1,3 @@
1
+ from setuptools import setup
2
+
3
+ setup()
@@ -0,0 +1 @@
1
+ __import__('pkg_resources').declare_namespace(__name__)
File without changes
@@ -0,0 +1,196 @@
1
+ import logging
2
+ import threading
3
+ from enum import Enum, unique
4
+
5
+ """
6
+ Global variable for tracking the currently active logger. Do not use this
7
+ directly instead use get_active_logger()
8
+ """
9
+ _active_logger = {}
10
+
11
+ DEBUG = False
12
+
13
+
14
+ def thread_stack():
15
+ global _active_logger
16
+ thread_id = threading.current_thread().ident
17
+ if thread_id not in _active_logger:
18
+ _active_logger[thread_id] = []
19
+ return _active_logger.get(thread_id)
20
+
21
+
22
+ def push_active_logger(logger):
23
+ """Insert a logger to the _active_logger stack."""
24
+ thread_stack().insert(0, logger)
25
+
26
+
27
+ def get_active_logger():
28
+ """Return the currently active logger, which is defined globally."""
29
+ stack = thread_stack()
30
+ if len(stack) > 0:
31
+ return stack[0]
32
+ return logging.getLogger() # root logger
33
+
34
+
35
+ def get_active_logger_in_thread(thread_ident):
36
+ """Return the currently active logger, which is defined globally."""
37
+ if thread_ident in _active_logger:
38
+ stack = _active_logger.get(thread_ident)
39
+ if len(stack) > 0:
40
+ return stack[0]
41
+ return logging.getLogger() # root logger
42
+
43
+
44
+ def pop_active_logger():
45
+ """Pop the currently active logger from the _active_solution stack."""
46
+
47
+ stack = thread_stack()
48
+ if len(stack) > 0:
49
+ logger = stack.pop(0)
50
+ logger.handlers.clear()
51
+ return logger
52
+ else:
53
+ return logging.getLogger() # root logger
54
+
55
+
56
+ @unique
57
+ class LogLevel(Enum):
58
+ """LogLevel album allows.
59
+
60
+ Notes:
61
+ Only add Names available in python standard logging module.
62
+
63
+ """
64
+ DEBUG = 1
65
+ INFO = 0
66
+ WARNING = 2
67
+ ERROR = 3
68
+
69
+
70
+ def to_loglevel(value):
71
+ """Converts a string value to a @LogLevel.
72
+
73
+ Args:
74
+ value:
75
+ The string value
76
+
77
+ Returns:
78
+ The LovLevel class enum
79
+
80
+ Raises:
81
+ KeyError when loglevel unknown.
82
+
83
+ """
84
+ try:
85
+ return LogLevel[value]
86
+ except KeyError as err:
87
+ logger = get_active_logger()
88
+ logger.error('Loglevel %s not allowed or unknown!' % value)
89
+ raise err
90
+
91
+
92
+ def configure_logging(name, loglevel=None, stream_handler=None, formatter_string=None, parent_thread_id=None,
93
+ parent_name=None):
94
+ """Configures a logger with a certain name and loglevel.
95
+
96
+ loglevel:
97
+ The Loglevel to use. Either DEBUG or INFO.
98
+ name:
99
+ The name of the logger.
100
+
101
+ Returns:
102
+ The logger object.
103
+
104
+ """
105
+ # create or get currently active logger
106
+ if parent_thread_id is None:
107
+ parent = get_active_logger()
108
+ else:
109
+ parent = get_active_logger_in_thread(parent_thread_id)
110
+
111
+ if not parent_name:
112
+ parent_name = parent.name
113
+
114
+ # if currently active logger has the same name, just return it
115
+ if parent_name == name:
116
+ return parent
117
+
118
+ # make sure the new logger is registered as a child of the currently active logger in order to propagate logs
119
+ if parent_name.endswith("." + name):
120
+ name = parent_name
121
+ else:
122
+ if not name.startswith(parent_name + "."):
123
+ name = parent_name + "." + name
124
+
125
+ logger = logging.getLogger(name)
126
+ if loglevel is None:
127
+ logger.setLevel(parent.level)
128
+ else:
129
+ logger.setLevel(loglevel.name)
130
+
131
+ if stream_handler:
132
+ # create formatter
133
+ if not formatter_string:
134
+ formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
135
+ else:
136
+ formatter = logging.Formatter(formatter_string)
137
+ # create console handler and set level to debug
138
+ # ToDo: different handlers necessary? e.g. logging additional into a file?
139
+ ch = logging.StreamHandler(stream_handler)
140
+ ch.setLevel(loglevel.name)
141
+ ch.setFormatter(formatter)
142
+ logger.addHandler(ch)
143
+
144
+ # push logger if not already active
145
+ if get_active_logger() != logger:
146
+ push_active_logger(logger)
147
+
148
+ return logger
149
+
150
+
151
+ def get_loglevel():
152
+ """Returns the loglevel of the current active logger."""
153
+ return get_active_logger().level
154
+
155
+
156
+ def get_loglevel_name():
157
+ """Returns the Name of the loglevel of the current active logger."""
158
+ logger = get_active_logger()
159
+ return logging.getLevelName(logger.level)
160
+
161
+
162
+ def set_loglevel(loglevel):
163
+ """ Sets logLevel for a logger with a certain name for ALL available handlers.
164
+
165
+ Args:
166
+ loglevel:
167
+ The Loglevel to use. Either DEBUG or INFO.
168
+
169
+ """
170
+ # logger loglevel
171
+ active_logger = get_active_logger()
172
+ active_logger.debug('Set loglevel to %s...' % loglevel.name)
173
+
174
+ active_logger.setLevel(loglevel.name)
175
+
176
+ # set handler loglevel
177
+ for handler in active_logger.handlers:
178
+ handler_name = handler.stream.name if hasattr(handler, "stream") and hasattr(handler.stream,
179
+ active_logger.name) else "default handler"
180
+ active_logger.debug('Set loglevel for handler %s to %s...' % (handler_name, loglevel.name))
181
+ handler.setLevel(loglevel.name)
182
+
183
+
184
+ class LogEntry:
185
+ name = None
186
+ level = None
187
+ message = None
188
+
189
+ def __init__(self, name, level, message):
190
+ self.name = name
191
+ self.level = level
192
+ self.message = message
193
+
194
+
195
+ def debug_settings():
196
+ return DEBUG
@@ -0,0 +1,187 @@
1
+ import os
2
+ import sys
3
+ import tarfile
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from urllib.request import urlretrieve
7
+
8
+ from album.runner import album_logging
9
+ from album.runner.album_logging import get_active_logger, configure_logging
10
+ from album.runner.core.api.model.solution import ISolution
11
+ from album.runner.core.default_values_runner import DefaultValuesRunner
12
+ from album.runner.core.model.solution import Solution
13
+ from album.runner.core.model.solution_script import SolutionScript
14
+
15
+
16
+ def download_if_not_exists(url, file_name):
17
+ """Downloads resource if not already cached and returns local resource path.
18
+
19
+ Args:
20
+ url: The URL of the download.
21
+ file_name: The local filename of the download.
22
+
23
+ Returns: The path to the downloaded resource.
24
+
25
+ """
26
+ download_dir = get_cache_path()
27
+ download_path = download_dir.joinpath(file_name)
28
+
29
+ if download_path.exists():
30
+ get_active_logger().info(f"Found cache of {url}: {download_path}...")
31
+ return download_path
32
+ if not download_dir.exists():
33
+ download_dir.mkdir(parents=True)
34
+
35
+ get_active_logger().info(f"Downloading {url} to {download_path}...")
36
+
37
+ urlretrieve(url, download_path)
38
+
39
+ return download_path
40
+
41
+
42
+ def extract_tar(in_tar, out_dir):
43
+ """
44
+
45
+ Args:
46
+ out_dir: Directory where the TAR file should be extracted to
47
+ in_tar: TAR file to be extracted
48
+ """
49
+ out_path = Path(out_dir)
50
+
51
+ if not out_path.exists():
52
+ out_path.mkdir(parents=True)
53
+
54
+ get_active_logger().info(f"Extracting {in_tar} to {out_dir}...")
55
+
56
+ my_tar = tarfile.open(in_tar)
57
+ my_tar.extractall(out_dir)
58
+ my_tar.close()
59
+
60
+
61
+ # todo: extract_zip
62
+
63
+
64
+ def get_environment_path() -> Path:
65
+ """Returns the path of the environment the solution runs in."""
66
+ active_solution = get_active_solution()
67
+ res = active_solution.installation().environment_path()
68
+ return Path(res) if res else res
69
+
70
+
71
+ def get_data_path() -> Path:
72
+ """Returns the data path provided for the solution."""
73
+ active_solution = get_active_solution()
74
+ res = active_solution.installation().data_path()
75
+ return Path(res) if res else res
76
+
77
+
78
+ def get_package_path() -> Path:
79
+ """Returns the package path provided for the solution."""
80
+ active_solution = get_active_solution()
81
+ res = active_solution.installation().package_path()
82
+ return Path(res) if res else res
83
+
84
+
85
+ def get_app_path() -> Path:
86
+ """Returns the app path provided for the solution."""
87
+ active_solution = get_active_solution()
88
+ res = active_solution.installation().app_path()
89
+ return Path(res) if res else res
90
+
91
+
92
+ def get_cache_path() -> Path:
93
+ """Returns the cache path provided for the solution."""
94
+ active_solution = get_active_solution()
95
+ res = active_solution.installation().user_cache_path()
96
+ return Path(res) if res else res
97
+
98
+
99
+ def in_target_environment() -> bool:
100
+ """Gives the boolean information whether or not current python is the python from the album target environment.
101
+
102
+ Returns:
103
+ True when current active python is the album target environment else False.
104
+
105
+ """
106
+ active_solution = get_active_solution()
107
+
108
+ return True if sys.executable.startswith(
109
+ active_solution.installation().environment_path()) else False
110
+
111
+
112
+ def get_args():
113
+ """Get the parsed argument from the solution call.
114
+
115
+ Returns:
116
+ The namespace object of the parsed arguments.
117
+
118
+ """
119
+ active_solution = get_active_solution()
120
+
121
+ return active_solution.args()
122
+
123
+
124
+ """
125
+ Global variable for tracking the currently active solution. Do not use this
126
+ directly instead use get_active_solution()
127
+ """
128
+ _active_solution = []
129
+
130
+ enc = sys.getfilesystemencoding()
131
+
132
+
133
+ def setup(**attrs):
134
+ """This configures a solution for the use by the main album tool."""
135
+ global _active_solution
136
+ loglevel = os.getenv(DefaultValuesRunner.env_variable_logger_level.value, "INFO")
137
+ configure_logging("script", loglevel=album_logging.to_loglevel(loglevel),
138
+ stream_handler=sys.stdout, formatter_string=SolutionScript.get_script_logging_formatter_str())
139
+ next_solution = Solution(attrs)
140
+ push_active_solution(next_solution)
141
+ goal = os.getenv(DefaultValuesRunner.env_variable_action.value, None)
142
+ if goal:
143
+ goal = Solution.Action[goal]
144
+ package_path = os.getenv(DefaultValuesRunner.env_variable_package.value, os.path.abspath(__file__))
145
+ if package_path:
146
+ package_path = Path(package_path)
147
+ installation_base_path = os.getenv(DefaultValuesRunner.env_variable_installation.value, None)
148
+ if installation_base_path:
149
+ installation_base_path = Path(installation_base_path)
150
+ environment_path = os.getenv(DefaultValuesRunner.env_variable_environment.value, None)
151
+ if environment_path:
152
+ environment_path = Path(environment_path)
153
+ if goal:
154
+ next_solution.installation().set_package_path(package_path)
155
+ SolutionScript.trigger_solution_goal(next_solution, goal, package_path, installation_base_path, environment_path)
156
+
157
+
158
+ def push_active_solution(solution_object: ISolution):
159
+ """Pop a solution to the _active_solution stack."""
160
+ global _active_solution
161
+ _active_solution.insert(0, solution_object)
162
+
163
+
164
+ def get_parent_solution() -> Optional[ISolution]:
165
+ """Return the parent solution of the currently active solution."""
166
+ global _active_solution
167
+ if len(_active_solution) > 1:
168
+ return _active_solution[1]
169
+ return None
170
+
171
+
172
+ def get_active_solution() -> Optional[ISolution]:
173
+ """Return the currently active solution, which is defined globally."""
174
+ global _active_solution
175
+ if len(_active_solution) > 0:
176
+ return _active_solution[0]
177
+ return None
178
+
179
+
180
+ def pop_active_solution():
181
+ """Pop the currently active solution from the _active_solution stack."""
182
+ global _active_solution
183
+
184
+ if len(_active_solution) > 0:
185
+ return _active_solution.pop(0)
186
+ else:
187
+ return None
@@ -0,0 +1,18 @@
1
+ from abc import ABCMeta, abstractmethod
2
+
3
+
4
+ class ICoordinates:
5
+
6
+ __metaclass__ = ABCMeta
7
+
8
+ @abstractmethod
9
+ def name(self):
10
+ raise NotImplementedError
11
+
12
+ @abstractmethod
13
+ def group(self):
14
+ raise NotImplementedError
15
+
16
+ @abstractmethod
17
+ def version(self):
18
+ raise NotImplementedError
@@ -0,0 +1,103 @@
1
+ from abc import ABCMeta, abstractmethod
2
+ from enum import Enum
3
+ from pathlib import Path
4
+ from typing import List
5
+
6
+ from album.runner.core.api.model.coordinates import ICoordinates
7
+
8
+
9
+ class ISolution:
10
+
11
+ __metaclass__ = ABCMeta
12
+
13
+ class Action(Enum):
14
+ NO_ACTION = 0
15
+ INSTALL = 1
16
+ RUN = 2
17
+ TEST = 3
18
+ UNINSTALL = 4
19
+
20
+ class ISetup(dict):
21
+ __metaclass__ = ABCMeta
22
+ __getattr__ = dict.get
23
+ __setattr__ = dict.__setitem__
24
+ __delattr__ = dict.__delitem__
25
+
26
+ class IInstallation:
27
+ __metaclass__ = ABCMeta
28
+
29
+ @abstractmethod
30
+ def environment_path(self) -> Path:
31
+ raise NotImplementedError
32
+
33
+ @abstractmethod
34
+ def user_cache_path(self) -> Path:
35
+ raise NotImplementedError
36
+
37
+ @abstractmethod
38
+ def internal_cache_path(self) -> Path:
39
+ raise NotImplementedError
40
+
41
+ @abstractmethod
42
+ def package_path(self) -> Path:
43
+ raise NotImplementedError
44
+
45
+ @abstractmethod
46
+ def data_path(self) -> Path:
47
+ raise NotImplementedError
48
+
49
+ @abstractmethod
50
+ def app_path(self) -> Path:
51
+ raise NotImplementedError
52
+
53
+ @abstractmethod
54
+ def installation_path(self) -> Path:
55
+ raise NotImplementedError
56
+
57
+ @abstractmethod
58
+ def set_environment_path(self, path: Path):
59
+ raise NotImplementedError
60
+
61
+ @abstractmethod
62
+ def set_package_path(self, package_path: Path):
63
+ raise NotImplementedError
64
+
65
+ @abstractmethod
66
+ def set_installation_path(self, solution_base_path: Path):
67
+ raise NotImplementedError
68
+
69
+ @abstractmethod
70
+ def setup(self) -> ISetup:
71
+ raise NotImplementedError
72
+
73
+ @abstractmethod
74
+ def installation(self) -> IInstallation:
75
+ raise NotImplementedError
76
+
77
+ @abstractmethod
78
+ def coordinates(self) -> ICoordinates:
79
+ raise NotImplementedError
80
+
81
+ @abstractmethod
82
+ def script(self) -> Path:
83
+ raise NotImplementedError
84
+
85
+ @abstractmethod
86
+ def args(self) -> List:
87
+ raise NotImplementedError
88
+
89
+ @abstractmethod
90
+ def get_arg(self, name: str):
91
+ raise NotImplementedError
92
+
93
+ @abstractmethod
94
+ def get_identifier(self) -> str:
95
+ raise NotImplementedError
96
+
97
+ @abstractmethod
98
+ def set_script(self, script: str):
99
+ raise NotImplementedError
100
+
101
+ @abstractmethod
102
+ def set_args(self, args: List):
103
+ raise NotImplementedError
@@ -0,0 +1,20 @@
1
+ from enum import Enum
2
+
3
+
4
+ class DefaultValuesRunner(Enum):
5
+ """Add an entry here to initialize default attributes for a album runner instance."""
6
+
7
+ solution_app_prefix = "app" # solution specific app files
8
+ solution_data_prefix = "data" # solution specific data files
9
+ solution_internal_cache_prefix = (
10
+ "icache" # solution specific album internal cache files
11
+ )
12
+ solution_user_cache_prefix = (
13
+ "ucache" # solution specific user cache files, accessible via runner API
14
+ )
15
+
16
+ env_variable_action = "ALBUM_SOLUTION_ACTION"
17
+ env_variable_logger_level = "ALBUM_LOGGER_LEVEL"
18
+ env_variable_package = "ALBUM_SOLUTION_PACKAGE"
19
+ env_variable_installation = "ALBUM_SOLUTION_INSTALLATION"
20
+ env_variable_environment = "ALBUM_SOLUTION_ENVIRONMENT"
@@ -0,0 +1,31 @@
1
+ from album.runner.core.api.model.coordinates import ICoordinates
2
+
3
+
4
+ class Coordinates(ICoordinates):
5
+ """Class for the Coordinates of a solution."""
6
+
7
+ def __init__(self, group: str, name: str, version: str) -> None:
8
+ self._group = group
9
+ self._name = name
10
+ self._version = version
11
+
12
+
13
+ def name(self):
14
+ return self._name
15
+
16
+ def group(self):
17
+ return self._group
18
+
19
+ def version(self):
20
+ return self._version
21
+
22
+ def __str__(self) -> str:
23
+ return f"{self._group}:{self._name}:{self._version}"
24
+
25
+ def __eq__(self, o: object) -> bool:
26
+ return isinstance(
27
+ o, ICoordinates
28
+ ) and o.group() == self._group and o.name() == self._name and o.version() == self._version
29
+
30
+ def __hash__(self):
31
+ return hash(self.__str__())
@@ -0,0 +1,112 @@
1
+ from pathlib import Path
2
+ from typing import List
3
+
4
+ from album.runner.core.api.model.coordinates import ICoordinates
5
+ from album.runner.core.api.model.solution import ISolution
6
+ from album.runner.core.default_values_runner import DefaultValuesRunner
7
+ from album.runner.core.model.coordinates import Coordinates
8
+
9
+
10
+ class Solution(ISolution):
11
+ """Encapsulates a album solution configuration."""
12
+
13
+ class Setup(ISolution.ISetup):
14
+
15
+ def __init__(self, attrs=None):
16
+ """sets object attributes
17
+
18
+ Args:
19
+ attrs:
20
+ Dictionary containing the attributes.
21
+ """
22
+ if attrs:
23
+ super().__init__(attrs)
24
+ else:
25
+ super().__init__()
26
+
27
+ def __str__(self, indent=2):
28
+ s = '\n'
29
+ for attr in self.__dict__:
30
+ for i in range(0, indent):
31
+ s += '\t'
32
+ s += (attr + ':\t' + str(getattr(self, attr))) + '\n'
33
+ return s
34
+
35
+ class Installation (ISolution.IInstallation):
36
+ def __init__(self):
37
+ super().__init__()
38
+ # API keywords
39
+ self._installation_path = None
40
+ self._environment_path = None
41
+ self._package_path = None
42
+
43
+ def environment_path(self) -> Path:
44
+ return self._environment_path
45
+
46
+ def user_cache_path(self) -> Path:
47
+ return self._installation_path.joinpath(DefaultValuesRunner.solution_user_cache_prefix.value)
48
+
49
+ def internal_cache_path(self) -> Path:
50
+ return self._installation_path.joinpath(DefaultValuesRunner.solution_internal_cache_prefix.value)
51
+
52
+ def package_path(self) -> Path:
53
+ return self._package_path
54
+
55
+ def installation_path(self) -> Path:
56
+ return self._installation_path
57
+
58
+ def data_path(self) -> Path:
59
+ return self._installation_path.joinpath(DefaultValuesRunner.solution_data_prefix.value)
60
+
61
+ def app_path(self) -> Path:
62
+ return self._installation_path.joinpath(DefaultValuesRunner.solution_app_prefix.value)
63
+
64
+ def set_environment_path(self, path: Path):
65
+ self._environment_path = path
66
+
67
+ def set_package_path(self, package_path: Path):
68
+ self._package_path = package_path
69
+
70
+ def set_installation_path(self, solution_base_path: Path):
71
+ self._installation_path = solution_base_path
72
+
73
+ def __init__(self, attrs=None):
74
+ self._installation = Solution.Installation()
75
+ self._setup = Solution.Setup(attrs)
76
+ self._coordinates = Coordinates(attrs['group'], attrs['name'], attrs['version'])
77
+ self._args = None
78
+ self._script = None
79
+
80
+ def setup(self) -> ISolution.ISetup:
81
+ return self._setup
82
+
83
+ def installation(self) -> ISolution.IInstallation:
84
+ return self._installation
85
+
86
+ def coordinates(self) -> ICoordinates:
87
+ return self._coordinates
88
+
89
+ def script(self) -> Path:
90
+ return self._script
91
+
92
+ def get_arg(self, k):
93
+ """Get a specific named argument for this album if it exists."""
94
+ matches = [arg for arg in self._setup.args if arg['name'] == k]
95
+ return matches[0]
96
+
97
+ def get_identifier(self) -> str:
98
+ identifier = '_'.join([self._setup.group, self._setup.name, self._setup.version])
99
+ return identifier
100
+
101
+ def set_script(self, script: str):
102
+ self._script = script
103
+
104
+ def __eq__(self, other):
105
+ return isinstance(other, Solution) and \
106
+ other.coordinates() == self._coordinates
107
+
108
+ def set_args(self, args: List):
109
+ self._args = args
110
+
111
+ def args(self) -> List:
112
+ return self._args
@@ -0,0 +1,173 @@
1
+ import argparse
2
+ import sys
3
+ from argparse import ArgumentError
4
+ from pathlib import Path
5
+
6
+ from album.runner import album_logging
7
+ from album.runner.album_logging import get_active_logger, configure_logging
8
+ from album.runner.core.api.model.solution import ISolution
9
+ from album.runner.core.model.solution import Solution
10
+
11
+
12
+ class SolutionScript:
13
+
14
+ @staticmethod
15
+ def make_action(solution, mydest):
16
+ class CustomAction(argparse.Action):
17
+ def __call__(self, parser, namespace, values, option_string=None):
18
+ setattr(namespace, mydest, solution.get_arg(mydest)['action'](values))
19
+
20
+ return CustomAction
21
+
22
+ @staticmethod
23
+ def get_script_logging_formatter_str():
24
+ return '%(levelname)-7s %(name)s - %(message)s'
25
+
26
+ @staticmethod
27
+ def get_script_logging_formatter_regex():
28
+ regex_log_level = 'DEBUG|INFO|WARNING|ERROR'
29
+ return r'(%s)\s+([\s\S]+) - ([\s\S]+)?' % regex_log_level
30
+
31
+ @staticmethod
32
+ def trigger_solution_goal(solution, goal, package_path=None, installation_base_path=None, environment_path=None):
33
+ SolutionScript.api_access(solution, package_path, installation_base_path, environment_path)
34
+ parser = None
35
+ if package_path:
36
+ sys.path.append(package_path)
37
+ if solution.setup().args:
38
+ append_arguments = (goal == Solution.Action.RUN) or (goal == Solution.Action.TEST)
39
+ if append_arguments:
40
+ parser = SolutionScript.append_arguments(solution)
41
+ if goal == Solution.Action.INSTALL:
42
+ solution.setup().install()
43
+ if goal == Solution.Action.UNINSTALL:
44
+ solution.setup().uninstall()
45
+ if goal == Solution.Action.RUN:
46
+ SolutionScript.execute_run_action(solution)
47
+ if goal == Solution.Action.TEST:
48
+ if 'pre_test' in solution.setup():
49
+ d = solution.setup().pre_test()
50
+ else:
51
+ d = {}
52
+ if d is None:
53
+ d = {}
54
+ sys.argv = sys.argv + ["=".join([c, d[c]]) for c in d]
55
+
56
+ # parse args again after pre_test() routine if necessary.
57
+ if parser and "args" in solution.setup().keys():
58
+ args = parser.parse_args()
59
+ solution.set_args(args)
60
+
61
+ SolutionScript.execute_run_action(solution)
62
+ solution.setup().test()
63
+
64
+ @staticmethod
65
+ def execute_run_action(solution):
66
+ get_active_logger().info("Starting %s" % solution.setup().name)
67
+ if solution.setup().run and callable(solution.setup().run):
68
+ solution.setup().run()
69
+ else:
70
+ get_active_logger().warn(
71
+ "No \"run\" routine configured for solution \"%s\"." % solution.setup().name)
72
+ if solution.setup().close and callable(solution.setup().close):
73
+ solution.setup().close()
74
+ get_active_logger().info("Finished %s" % solution.setup().name)
75
+
76
+ @staticmethod
77
+ def init_logging():
78
+ configure_logging("script", loglevel=album_logging.to_loglevel(album_logging.get_loglevel_name()),
79
+ stream_handler=sys.stdout,
80
+ formatter_string=SolutionScript.get_script_logging_formatter_str())
81
+
82
+ @staticmethod
83
+ def api_access(solution: ISolution, package_path, installation_base_path, environment_path):
84
+ if package_path:
85
+ solution.installation().set_package_path(package_path)
86
+ sys.path.insert(0, solution.installation().package_path())
87
+ if installation_base_path:
88
+ solution.installation().set_installation_path(installation_base_path)
89
+ # add app_path to syspath
90
+ sys.path.insert(0, solution.installation().app_path())
91
+ if environment_path:
92
+ solution.installation().set_environment_path(environment_path)
93
+
94
+ @staticmethod
95
+ def append_arguments(solution: ISolution):
96
+ parser = None
97
+ if isinstance(solution.setup().args, str):
98
+ SolutionScript._handle_args_string(solution.setup().args)
99
+ else:
100
+ parser = SolutionScript._handle_args_list(solution)
101
+ return parser
102
+
103
+ @staticmethod
104
+ def _handle_args_string(args):
105
+ # pass through to module
106
+ if args == 'pass-through':
107
+ get_active_logger().info(
108
+ 'Argument parsing not specified in album solution. Passing arguments through...'
109
+ )
110
+ else:
111
+ message = 'Argument keyword \'%s\' not supported!' % args
112
+ get_active_logger().error(message)
113
+ raise ArgumentError(argument=args, message=message)
114
+
115
+ @staticmethod
116
+ def _handle_args_list(solution: ISolution):
117
+ parser = argparse.ArgumentParser(description='album run %s' % solution.setup().name)
118
+ for arg in solution.setup().args:
119
+ SolutionScript._add_parser_argument(solution, parser, arg)
120
+ args = parser.parse_args()
121
+ solution.set_args(args)
122
+ return parser
123
+
124
+ @staticmethod
125
+ def _add_parser_argument(solution, parser, arg):
126
+ keys = arg.keys()
127
+
128
+ if 'default' in keys and 'action' in keys:
129
+ get_active_logger().warning("Default values cannot be automatically set when an action is provided! "
130
+ "Ignoring default values...")
131
+
132
+ args = {}
133
+ if 'action' in keys:
134
+ args['action'] = SolutionScript.make_action(solution, arg['name'])
135
+ if 'default' in keys:
136
+ args['default'] = arg['default']
137
+ if 'description' in keys:
138
+ args['help'] = arg['description']
139
+ if 'type' in keys:
140
+ args['type'] = SolutionScript._parse_type(arg['type'])
141
+ if 'required' in keys:
142
+ args['required'] = arg['required']
143
+ parser.add_argument('--%s' % arg['name'], **args)
144
+
145
+ @staticmethod
146
+ def _get_action_class_name(name):
147
+ class_name = '%sAction' % name.capitalize()
148
+ return class_name
149
+
150
+ @staticmethod
151
+ def strtobool(val):
152
+ val = val.lower()
153
+ if val in ('y', 'yes', 't', 'true', 'on', '1'):
154
+ return True
155
+ elif val in ('n', 'no', 'f', 'false', 'off', '0'):
156
+ return False
157
+ else:
158
+ raise ValueError("invalid truth value %r" % (val,))
159
+
160
+ @staticmethod
161
+ def _parse_type(type_str):
162
+ if type_str == 'string':
163
+ return str
164
+ if type_str == 'file':
165
+ return Path
166
+ if type_str == 'directory':
167
+ return Path
168
+ if type_str == 'integer':
169
+ return int
170
+ if type_str == 'float':
171
+ return float
172
+ if type_str == 'boolean':
173
+ return SolutionScript.strtobool
@@ -0,0 +1,287 @@
1
+ {
2
+ "$ref": "#/definitions/RDF",
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "definitions": {
5
+ "Parent": {
6
+ "anyOf": [
7
+ {
8
+ "type": "object",
9
+ "properties": {
10
+ "group": {
11
+ "title": "group",
12
+ "type": "string"
13
+ },
14
+ "name": {
15
+ "title": "name",
16
+ "type": "string"
17
+ },
18
+ "version": {
19
+ "title": "version",
20
+ "type": "string"
21
+ }
22
+ },
23
+ "required": [
24
+ "group",
25
+ "name",
26
+ "version"
27
+ ]
28
+ }, {
29
+ "type": "object",
30
+ "properties": {
31
+ "resolve_solution": {
32
+ "title": "resolve_solution",
33
+ "type": "string"
34
+ }
35
+ },
36
+ "required": [
37
+ "resolve_solution"
38
+ ]
39
+ }, {
40
+ "type": "object",
41
+ "properties": {
42
+ "doi": {
43
+ "title": "doi",
44
+ "type": "string"
45
+ }
46
+ },
47
+ "required": [
48
+ "doi"
49
+ ]
50
+ }
51
+ ]
52
+ },
53
+ "Argument": {
54
+ "additionalProperties": false,
55
+ "properties": {
56
+ "name": {
57
+ "title": "name",
58
+ "type": "string"
59
+ },
60
+ "description": {
61
+ "title": "description",
62
+ "type": "string"
63
+ },
64
+ "type": {
65
+ "title": "type",
66
+ "type": "string",
67
+ "enum": ["string", "file", "directory", "boolean", "integer", "float"]
68
+ },
69
+ "default": {
70
+ "title": "default"
71
+ },
72
+ "required": {
73
+ "title": "required",
74
+ "type": "boolean"
75
+ },
76
+ "action": {
77
+ "title": "action"
78
+ }
79
+ },
80
+ "required": [
81
+ "name"
82
+ ],
83
+ "type": "object"
84
+ },
85
+ "Dependencies": {
86
+ "additionalProperties": false,
87
+ "properties": {
88
+ "parent": {
89
+ "title": "parent",
90
+ "$ref": "#/definitions/Parent"
91
+ },
92
+ "environment_file": {
93
+ "title": "environment_file"
94
+ },
95
+ "plugins": {
96
+ "items": {
97
+ "$ref": "#/definitions/Plugin"
98
+ },
99
+ "title": "plugins",
100
+ "type": "array"
101
+ }
102
+ },
103
+ "required": [],
104
+ "type": "object"
105
+ },
106
+ "Plugin": {
107
+ "additionalProperties": false,
108
+ "properties": {
109
+ "id": {
110
+ "title": "id",
111
+ "type": "string"
112
+ },
113
+ "args": {
114
+ "title": "args",
115
+ "type": "object"
116
+ }
117
+ },
118
+ "required": [
119
+ "id"
120
+ ],
121
+ "type": "object"
122
+ },
123
+ "Citation": {
124
+ "additionalProperties": false,
125
+ "properties": {
126
+ "doi": {
127
+ "title": "doi",
128
+ "type": "string"
129
+ },
130
+ "text": {
131
+ "title": "text",
132
+ "type": "string"
133
+ },
134
+ "url": {
135
+ "title": "url",
136
+ "type": "string"
137
+ }
138
+ },
139
+ "required": [
140
+ "text"
141
+ ],
142
+ "type": "object"
143
+ },
144
+ "Cover": {
145
+ "additionalProperties": false,
146
+ "properties": {
147
+ "source": {
148
+ "title": "source",
149
+ "type": "string"
150
+ },
151
+ "description": {
152
+ "title": "description",
153
+ "type": "string"
154
+ }
155
+ },
156
+ "type": "object"
157
+ },
158
+ "RDF": {
159
+ "additionalProperties": false,
160
+ "properties": {
161
+ "solution_creators": {
162
+ "type": "array",
163
+ "title": "solution_creators",
164
+ "items": {
165
+ "type": "string"
166
+ }
167
+ },
168
+ "args": {
169
+ "items": {
170
+ "$ref": "#/definitions/Argument"
171
+ },
172
+ "type": "array"
173
+ },
174
+ "cite": {
175
+ "items": {
176
+ "$ref": "#/definitions/Citation"
177
+ },
178
+ "type": "array"
179
+ },
180
+ "covers": {
181
+ "items": {
182
+ "$ref": "#/definitions/Cover"
183
+ },
184
+ "title": "covers",
185
+ "type": "array"
186
+ },
187
+ "description": {
188
+ "title": "description",
189
+ "type": "string"
190
+ },
191
+ "documentation": {
192
+ "title": "documentation",
193
+ "anyOf": [
194
+ {
195
+ "type": "string"
196
+ },
197
+ {
198
+ "type": "array",
199
+ "items": {
200
+ "type": "string"
201
+ }
202
+ }
203
+ ]
204
+ },
205
+ "acknowledgement": {
206
+ "title": "acknowledgement",
207
+ "type": "string"
208
+ },
209
+ "license": {
210
+ "title": "license",
211
+ "type": "string"
212
+ },
213
+ "name": {
214
+ "title": "name",
215
+ "type": "string"
216
+ },
217
+ "group": {
218
+ "title": "group",
219
+ "type": "string"
220
+ },
221
+ "title": {
222
+ "title": "title",
223
+ "type": "string"
224
+ },
225
+ "album_api_version": {
226
+ "title": "album_api_version",
227
+ "type": "string"
228
+ },
229
+ "tags": {
230
+ "items": {
231
+ "type": "string"
232
+ },
233
+ "title": "tags",
234
+ "type": "array"
235
+ },
236
+ "version": {
237
+ "title": "version",
238
+ "type": "string"
239
+ },
240
+ "changelog": {
241
+ "title": "changelog",
242
+ "type": "string"
243
+ },
244
+ "doi": {
245
+ "title": "doi",
246
+ "type": "string"
247
+ },
248
+ "dependencies": {
249
+ "title": "dependencies",
250
+ "type": "object",
251
+ "$ref": "#/definitions/Dependencies"
252
+ },
253
+ "steps": {
254
+ "title": "steps"
255
+ },
256
+ "custom": {
257
+ "title": "custom",
258
+ "type": "object"
259
+ },
260
+ "install": {
261
+ "title": "install"
262
+ },
263
+ "uninstall": {
264
+ "title": "uninstall"
265
+ },
266
+ "run": {
267
+ "title": "run"
268
+ },
269
+ "test": {
270
+ "title": "test"
271
+ },
272
+ "pre_test": {
273
+ "title": "pre_test"
274
+ },
275
+ "close": {
276
+ "title": "close"
277
+ }
278
+ },
279
+ "required": [
280
+ "group",
281
+ "name",
282
+ "version"
283
+ ],
284
+ "type": "object"
285
+ }
286
+ }
287
+ }
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.1
2
+ Name: album-solution-api
3
+ Version: 0.6.0
4
+ Summary: Album is a framework for unifying computational tools across frameworks, languages, and environments.
5
+ Home-page: https://gitlab.com/album-app/album-solution-api
6
+ Author: Kyle Harrington, Jan Philipp Albrecht, Deborah Schmidt
7
+ Maintainer: Kyle Harrington, Jan Philipp Albrecht, Deborah Schmidt
8
+ Maintainer-email: album@kyleharrington.com
9
+ License: MIT
10
+ Project-URL: Source, https://gitlab.com/album-app/album-solution-api
11
+ Project-URL: Tracker, https://gitlab.com/album-app/album-solution-api/issues
12
+ Keywords: virtual,environments,imaging
13
+ Platform: any
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS :: MacOS X
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: POSIX
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.6
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+
27
+ # album-solution-api
28
+
29
+ Package installed in any Album solution environment. Provides the API for the solution script.
@@ -0,0 +1,25 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.cfg
5
+ setup.py
6
+ src/album/__init__.py
7
+ src/album/runner/__init__.py
8
+ src/album/runner/album_logging.py
9
+ src/album/runner/api.py
10
+ src/album/runner/core/__init__.py
11
+ src/album/runner/core/default_values_runner.py
12
+ src/album/runner/core/api/__init__.py
13
+ src/album/runner/core/api/model/__init__.py
14
+ src/album/runner/core/api/model/coordinates.py
15
+ src/album/runner/core/api/model/solution.py
16
+ src/album/runner/core/model/__init__.py
17
+ src/album/runner/core/model/coordinates.py
18
+ src/album/runner/core/model/solution.py
19
+ src/album/runner/core/model/solution_script.py
20
+ src/album/runner/core/schema/__init__.py
21
+ src/album/runner/core/schema/solution_schema.json
22
+ src/album_solution_api.egg-info/PKG-INFO
23
+ src/album_solution_api.egg-info/SOURCES.txt
24
+ src/album_solution_api.egg-info/dependency_links.txt
25
+ src/album_solution_api.egg-info/top_level.txt