digitalhub-runtime-python 0.5.0__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.
@@ -0,0 +1,251 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util as imputil
4
+ from pathlib import Path
5
+ from typing import Callable
6
+
7
+ from digitalhub_core.utils.generic_utils import (
8
+ decode_string,
9
+ extract_archive,
10
+ get_bucket_and_key,
11
+ get_s3_source,
12
+ requests_chunk_download,
13
+ )
14
+ from digitalhub_core.utils.git_utils import clone_repository
15
+ from digitalhub_core.utils.logger import LOGGER
16
+ from digitalhub_core.utils.uri_utils import map_uri_scheme
17
+
18
+
19
+ def get_function_from_source(path: Path, source_spec: dict) -> Callable:
20
+ """
21
+ Get function from source.
22
+
23
+ Parameters
24
+ ----------
25
+ path : Path
26
+ Path where to save the function source.
27
+ source_spec : dict
28
+ Funcrion source spec.
29
+
30
+ Returns
31
+ -------
32
+ Callable
33
+ Function.
34
+ """
35
+ try:
36
+ function_code = save_function_source(path, source_spec)
37
+ handler_path, function_name = parse_handler(source_spec["handler"])
38
+ function_path = (function_code / handler_path).with_suffix(".py")
39
+ return import_function(function_path, function_name)
40
+ except Exception as e:
41
+ msg = f"Some error occurred while getting function. Exception: {e.__class__}. Error: {e.args}"
42
+ LOGGER.exception(msg)
43
+ raise RuntimeError(msg) from e
44
+
45
+
46
+ def parse_handler(handler: str) -> tuple:
47
+ """
48
+ Parse handler.
49
+
50
+ Parameters
51
+ ----------
52
+ handler : str
53
+ Function handler
54
+
55
+ Returns
56
+ -------
57
+ str
58
+ Function handler.
59
+ """
60
+ parsed = handler.split(":")
61
+ if len(parsed) == 1:
62
+ return Path(""), parsed[0]
63
+ return Path(*parsed[0].split(".")), parsed[1]
64
+
65
+
66
+ def save_function_source(path: Path, source_spec: dict) -> Path:
67
+ """
68
+ Save function source.
69
+
70
+ Parameters
71
+ ----------
72
+ path : Path
73
+ Path where to save the function source.
74
+ source_spec : dict
75
+ Function source spec.
76
+
77
+ Returns
78
+ -------
79
+ Path
80
+ Path to the function source.
81
+ """
82
+ # Prepare path
83
+ path.mkdir(parents=True, exist_ok=True)
84
+
85
+ # Get relevant information
86
+ base64 = source_spec.get("base64")
87
+ source = source_spec.get("source")
88
+
89
+ scheme = None
90
+ if source is not None:
91
+ scheme = map_uri_scheme(source)
92
+
93
+ # Base64
94
+ if base64 is not None:
95
+ filename = "main.py"
96
+ if scheme == "local":
97
+ filename = Path(source).name
98
+
99
+ base64_path = path / filename
100
+ base64_path.write_text(decode_base64(base64))
101
+
102
+ if scheme is None or scheme == "local":
103
+ return base64_path
104
+
105
+ # Git repo
106
+ if scheme == "git":
107
+ get_repository(path, source)
108
+
109
+ # Http(s) or s3 presigned urls
110
+ elif scheme == "remote":
111
+ filename = path / "archive.zip"
112
+ get_remote_source(source, filename)
113
+ unzip(path, filename)
114
+
115
+ # S3 path
116
+ elif scheme == "s3":
117
+ filename = path / "archive.zip"
118
+ bucket, key = get_bucket_and_key(source)
119
+ get_s3_source(bucket, key, filename)
120
+ unzip(path, filename)
121
+
122
+ # Unsupported scheme
123
+ else:
124
+ raise RuntimeError(f"Unsupported scheme: {scheme}")
125
+
126
+ return path
127
+
128
+
129
+ def get_remote_source(source: str, filename: Path) -> None:
130
+ """
131
+ Get remote source.
132
+
133
+ Parameters
134
+ ----------
135
+ source : str
136
+ HTTP(S) or S3 presigned URL.
137
+ filename : Path
138
+ Path where to save the function source.
139
+
140
+ Returns
141
+ -------
142
+ str
143
+ Function code.
144
+ """
145
+ try:
146
+ requests_chunk_download(source, filename)
147
+ except Exception as e:
148
+ msg = f"Some error occurred while downloading function source. Exception: {e.__class__}. Error: {e.args}"
149
+ LOGGER.exception(msg)
150
+ raise RuntimeError(msg) from e
151
+
152
+
153
+ def unzip(path: Path, filename: Path) -> None:
154
+ """
155
+ Extract an archive.
156
+
157
+ Parameters
158
+ ----------
159
+ path : Path
160
+ Path where to extract the archive.
161
+ filename : Path
162
+ Path to the archive.
163
+
164
+ Returns
165
+ -------
166
+ None
167
+ """
168
+
169
+ try:
170
+ extract_archive(path, filename)
171
+ except Exception as e:
172
+ msg = f"Source must be a valid zipfile. Exception: {e.__class__}. Error: {e.args}"
173
+ LOGGER.exception(msg)
174
+ raise RuntimeError(msg) from e
175
+
176
+
177
+ def get_repository(path: Path, source: str) -> str:
178
+ """
179
+ Get repository.
180
+
181
+ Parameters
182
+ ----------
183
+ path : Path
184
+ Path where to save the function source.
185
+ source : str
186
+ Git repository URL in format git://<url>.
187
+
188
+ Returns
189
+ -------
190
+ None
191
+ """
192
+ try:
193
+ clone_repository(path, source)
194
+ except Exception as e:
195
+ msg = f"Some error occurred while downloading function repo source. Exception: {e.__class__}. Error: {e.args}"
196
+ LOGGER.exception(msg)
197
+ raise RuntimeError(msg) from e
198
+
199
+
200
+ def decode_base64(base64: str) -> str:
201
+ """
202
+ Decode base64 encoded code.
203
+
204
+ Parameters
205
+ ----------
206
+ base64 : str
207
+ The encoded code.
208
+
209
+ Returns
210
+ -------
211
+ str
212
+ The decoded code.
213
+
214
+ Raises
215
+ ------
216
+ RuntimeError
217
+ Error while decoding code.
218
+ """
219
+ try:
220
+ return decode_string(base64)
221
+ except Exception as e:
222
+ msg = f"Some error occurred while decoding function source. Exception: {e.__class__}. Error: {e.args}"
223
+ LOGGER.exception(msg)
224
+ raise RuntimeError(msg) from e
225
+
226
+
227
+ def import_function(path: Path, handler: str) -> Callable:
228
+ """
229
+ Import a function from a module.
230
+
231
+ Parameters
232
+ ----------
233
+ path : Path
234
+ Path where the function source is located.
235
+ handler : str
236
+ Function name.
237
+
238
+ Returns
239
+ -------
240
+ Callable
241
+ Function.
242
+ """
243
+ try:
244
+ spec = imputil.spec_from_file_location(path.stem, path)
245
+ mod = imputil.module_from_spec(spec)
246
+ spec.loader.exec_module(mod)
247
+ return getattr(mod, handler)
248
+ except Exception as e:
249
+ msg = f"Some error occurred while importing function. Exception: {e.__class__}. Error: {e.args}"
250
+ LOGGER.exception(msg)
251
+ raise RuntimeError(msg) from e
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import typing
5
+ from typing import Any, Callable
6
+
7
+ from digitalhub_core.context.builder import get_context
8
+ from digitalhub_core.entities.artifacts.crud import artifact_from_dict
9
+ from digitalhub_core.utils.generic_utils import parse_entity_key
10
+ from digitalhub_core.utils.logger import LOGGER
11
+ from digitalhub_data.entities.dataitems.crud import dataitem_from_dict
12
+ from digitalhub_ml.entities.entity_types import EntityTypes
13
+ from digitalhub_ml.entities.models.crud import model_from_dict
14
+ from digitalhub_ml.entities.projects.crud import get_project
15
+
16
+ if typing.TYPE_CHECKING:
17
+ from digitalhub_core.entities._base.entity import Entity
18
+ from digitalhub_core.entities.projects.entity import Project
19
+
20
+
21
+ def get_project_(project_name: str) -> Project:
22
+ """
23
+ Get project.
24
+
25
+ Parameters
26
+ ----------
27
+ project_name : str
28
+ Project name.
29
+
30
+ Returns
31
+ -------
32
+ Project
33
+ Project.
34
+ """
35
+ try:
36
+ ctx = get_context(project_name)
37
+ return get_project(project_name, local=ctx.local)
38
+ except Exception as e:
39
+ msg = f"Error during project collection. Exception: {e.__class__}. Error: {e.args}"
40
+ LOGGER.exception(msg)
41
+ raise RuntimeError(msg)
42
+
43
+
44
+ def get_entity_inputs(inputs: dict) -> dict[str, Entity]:
45
+ """
46
+ Set inputs.
47
+
48
+ Parameters
49
+ ----------
50
+ inputs : dict
51
+ Run inputs.
52
+ parameters : dict
53
+ Run parameters.
54
+ tmp_dir : Path
55
+ Temporary directory for storing dataitms and artifacts.
56
+
57
+ Returns
58
+ -------
59
+ dict
60
+ Mlrun inputs.
61
+ """
62
+ try:
63
+ inputs_objects = {}
64
+ for k, v in inputs.items():
65
+ _, entity_type, _, _, _ = parse_entity_key(v.get("key"))
66
+ if entity_type == EntityTypes.DATAITEMS.value:
67
+ inputs_objects[k] = dataitem_from_dict(v)
68
+ elif entity_type == EntityTypes.ARTIFACTS.value:
69
+ inputs_objects[k] = artifact_from_dict(v)
70
+ elif entity_type == EntityTypes.MODELS.value:
71
+ inputs_objects[k] = model_from_dict(v)
72
+ return inputs_objects
73
+ except Exception as e:
74
+ msg = f"Error during inputs collection. Exception: {e.__class__}. Error: {e.args}"
75
+ LOGGER.exception(msg)
76
+ raise RuntimeError(msg) from e
77
+
78
+
79
+ def compose_inputs(
80
+ inputs: dict,
81
+ parameters: dict,
82
+ local_execution: bool,
83
+ func: Callable,
84
+ project: str | Project,
85
+ context: Any | None = None,
86
+ event: Any | None = None,
87
+ ) -> dict:
88
+ """
89
+ Compose inputs.
90
+
91
+ Parameters
92
+ ----------
93
+ inputs : dict
94
+ Run inputs.
95
+ parameters : dict
96
+ Run parameters.
97
+ local_execution : bool
98
+ Local execution.
99
+ func : Callable
100
+ Function to execute.
101
+ project : str
102
+ Project name.
103
+ context : nuclio_sdk.Context
104
+ Nuclio context.
105
+ event : nuclio_sdk.Event
106
+ Nuclio event.
107
+
108
+ Returns
109
+ -------
110
+ dict
111
+ Function inputs.
112
+ """
113
+ try:
114
+ entity_inputs = get_entity_inputs(inputs)
115
+ fnc_args = {**parameters, **entity_inputs}
116
+
117
+ fnc_parameters = inspect.signature(func).parameters
118
+ LOGGER.info(f"Function parameters: {'project' in fnc_parameters}")
119
+
120
+ _has_project = "project" in fnc_parameters
121
+ _has_context = "context" in fnc_parameters
122
+ _has_event = "event" in fnc_parameters
123
+
124
+ if _has_project:
125
+ if _has_context:
126
+ fnc_args["project"] = context.project
127
+ elif isinstance(project, str):
128
+ fnc_args["project"] = get_project_(project)
129
+ else:
130
+ fnc_args["project"] = project
131
+
132
+ if _has_context:
133
+ if context is not None and not local_execution:
134
+ fnc_args["context"] = context
135
+ else:
136
+ raise RuntimeError("Context is not available on local execution.")
137
+
138
+ if _has_event:
139
+ if event is not None and not local_execution:
140
+ fnc_args["event"] = event
141
+ else:
142
+ raise RuntimeError("Event is not available on local execution.")
143
+
144
+ return fnc_args
145
+
146
+ except Exception as e:
147
+ msg = f"Error during function arguments compostion. Exception: {e.__class__}. Error: {e.args}"
148
+ LOGGER.exception(msg)
149
+ raise RuntimeError(msg) from e
@@ -0,0 +1,204 @@
1
+ from __future__ import annotations
2
+
3
+ import pickle
4
+ from typing import Any
5
+
6
+ from digitalhub_core.entities._base.status import State
7
+ from digitalhub_core.entities.artifacts.crud import new_artifact
8
+ from digitalhub_core.entities.artifacts.entity import Artifact
9
+ from digitalhub_core.utils.logger import LOGGER
10
+ from digitalhub_data.entities.dataitems.crud import new_dataitem
11
+ from digitalhub_data.entities.dataitems.entity.table import DataitemTable
12
+ from digitalhub_data.readers.registry import DATAFRAME_TYPES
13
+
14
+
15
+ def collect_outputs(results: Any, outputs: list[str], project_name: str) -> dict:
16
+ """
17
+ Collect outputs. Use the produced results directly.
18
+
19
+ Parameters
20
+ ----------
21
+ results : Any
22
+ Function outputs.
23
+ project : Project
24
+ Project object.
25
+
26
+ Returns
27
+ -------
28
+ dict
29
+ Function outputs.
30
+ """
31
+ objects = {}
32
+ results = listify_results(results)
33
+
34
+ for idx, item in enumerate(results):
35
+ try:
36
+ name = outputs[idx]
37
+ except IndexError:
38
+ name = f"output_{idx}"
39
+
40
+ if isinstance(item, (str, int, float, bool, bytes)):
41
+ objects[name] = item
42
+
43
+ elif f"{item.__class__.__module__}.{item.__class__.__name__}" in DATAFRAME_TYPES:
44
+ objects[name] = build_and_load_dataitem(name, project_name, item)
45
+
46
+ else:
47
+ objects[name] = build_and_load_artifact(name, project_name, item)
48
+
49
+ return objects
50
+
51
+
52
+ def parse_outputs(results: Any, run_outputs: list, project_name: str) -> dict:
53
+ """
54
+ Parse outputs.
55
+
56
+ Parameters
57
+ ----------
58
+ results : Any
59
+ Function outputs.
60
+ project : Project
61
+ Project object.
62
+
63
+ Returns
64
+ -------
65
+ dict
66
+ Function outputs.
67
+ """
68
+ results_list = listify_results(results)
69
+ out_list = []
70
+ for idx, _ in enumerate(results_list):
71
+ try:
72
+ out_list.append(run_outputs.pop(0))
73
+ except IndexError:
74
+ out_list.append(f"output_{idx}")
75
+ return collect_outputs(results, out_list, project_name)
76
+
77
+
78
+ def listify_results(results: Any) -> list:
79
+ """
80
+ Listify results.
81
+
82
+ Parameters
83
+ ----------
84
+ results : Any
85
+ Function outputs.
86
+
87
+ Returns
88
+ -------
89
+ list
90
+ Function outputs.
91
+ """
92
+ if results is None:
93
+ return []
94
+
95
+ if not isinstance(results, (tuple, list)):
96
+ results = [results]
97
+
98
+ return results
99
+
100
+
101
+ def build_and_load_dataitem(name: str, project_name: str, data: Any) -> DataitemTable:
102
+ """
103
+ Build and load dataitem.
104
+
105
+ Parameters
106
+ ----------
107
+ name : str
108
+ Dataitem name.
109
+ project_name : str
110
+ Project name.
111
+ data : Any
112
+ Data.
113
+
114
+ Returns
115
+ -------
116
+ str
117
+ Dataitem key.
118
+ """
119
+ try:
120
+ path = f"s3://datalake/{project_name}/dataitems/table/{name}.parquet"
121
+ di: DataitemTable = new_dataitem(project=project_name, name=name, kind="table", path=path)
122
+ di.write_df(df=data)
123
+ return di
124
+ except Exception as e:
125
+ msg = f"Some error occurred while building and loading dataitem. Exception: {e.__class__}. Error: {e.args}"
126
+ LOGGER.exception(msg)
127
+ raise RuntimeError(msg)
128
+
129
+
130
+ def build_and_load_artifact(name: str, project_name: str, data: Any) -> Artifact:
131
+ """
132
+ Build and load artifact.
133
+
134
+ Parameters
135
+ ----------
136
+ name : str
137
+ Artifact name.
138
+ project_name : str
139
+ Project name.
140
+ data : Any
141
+ Data.
142
+
143
+ Returns
144
+ -------
145
+ str
146
+ Artifact key.
147
+ """
148
+ try:
149
+ path = f"s3://datalake/{project_name}/artifacts/artifact/{name}.pickle"
150
+
151
+ # Dump item to pickle
152
+ with open(f"{name}.pickle", "wb") as f:
153
+ f.write(pickle.dumps(data))
154
+
155
+ art = new_artifact(project=project_name, name=name, kind="artifact", path=path)
156
+ art.upload(source=f"{name}.pickle")
157
+ return art
158
+
159
+ except Exception as e:
160
+ msg = f"Some error occurred while building and loading artifact. Exception: {e.__class__}. Error: {e.args}"
161
+ LOGGER.exception(msg)
162
+ raise RuntimeError(msg)
163
+
164
+
165
+ def build_status(
166
+ parsed_execution: dict,
167
+ mapped_outputs: dict | None = None,
168
+ ) -> dict:
169
+ """
170
+ Collect outputs.
171
+
172
+ Parameters
173
+ ----------
174
+ parsed_execution : dict
175
+ Parsed execution dict.
176
+ mapped_outputs : dict
177
+ Mapped outputs.
178
+
179
+ Returns
180
+ -------
181
+ dict
182
+ Status dict.
183
+ """
184
+ results = {}
185
+ outputs = {}
186
+ if mapped_outputs is None:
187
+ mapped_outputs = {}
188
+
189
+ try:
190
+ for key, _ in mapped_outputs.items():
191
+ if key in parsed_execution:
192
+ if isinstance(parsed_execution[key], (DataitemTable, Artifact)):
193
+ outputs[key] = parsed_execution[key].key
194
+ else:
195
+ results[key] = parsed_execution[key]
196
+ return {
197
+ "state": State.COMPLETED.value,
198
+ "outputs": outputs,
199
+ "results": results,
200
+ }
201
+ except Exception as e:
202
+ msg = f"Something got wrong during run status building. Exception: {e.__class__}. Error: {e.args}"
203
+ LOGGER.exception(msg)
204
+ raise RuntimeError(msg) from e
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ from typing import Callable
5
+
6
+ from digitalhub_runtime_python.utils.outputs import collect_outputs
7
+
8
+
9
+ def handler(outputs: list[str] | None = None) -> Callable:
10
+ """
11
+ Decorator that handles the outputs of the function.
12
+
13
+ Parameters
14
+ ----------
15
+ outputs : list[str]
16
+ List of named outputs to collect.
17
+
18
+ Returns
19
+ -------
20
+ Callable
21
+ Decorated function.
22
+ """
23
+
24
+ def decorator(func: Callable) -> Callable:
25
+ """
26
+ Decorator that handles the outputs of the function.
27
+
28
+ Parameters
29
+ ----------
30
+ func : Callable
31
+ Function to decorate.
32
+
33
+ Returns
34
+ -------
35
+ Callable
36
+ Decorated function.
37
+ """
38
+
39
+ def wrapper(*args, **kwargs) -> dict:
40
+ """
41
+ Wrapper that handles the outputs of the function.
42
+
43
+ Parameters
44
+ ----------
45
+ args : tuple
46
+ Function arguments.
47
+ kwargs : dict
48
+ Function keyword arguments.
49
+
50
+ Returns
51
+ -------
52
+ Any
53
+ Function outputs.
54
+ """
55
+
56
+ # Initialize outputs
57
+ nonlocal outputs
58
+
59
+ # We pass the first argument as the project name
60
+ project_name = args[0]
61
+ args = args[1:]
62
+
63
+ # Execute the function
64
+ results = func(*args, **kwargs)
65
+
66
+ # Parse outputs based on the decorator signature
67
+ return collect_outputs(results, outputs, project_name)
68
+
69
+ wrapper = functools.wraps(func)(wrapper)
70
+
71
+ return wrapper
72
+
73
+ return decorator