runnable 0.50.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.
- extensions/README.md +0 -0
- extensions/__init__.py +0 -0
- extensions/catalog/README.md +0 -0
- extensions/catalog/any_path.py +214 -0
- extensions/catalog/file_system.py +52 -0
- extensions/catalog/minio.py +72 -0
- extensions/catalog/pyproject.toml +14 -0
- extensions/catalog/s3.py +11 -0
- extensions/job_executor/README.md +0 -0
- extensions/job_executor/__init__.py +236 -0
- extensions/job_executor/emulate.py +70 -0
- extensions/job_executor/k8s.py +553 -0
- extensions/job_executor/k8s_job_spec.yaml +37 -0
- extensions/job_executor/local.py +35 -0
- extensions/job_executor/local_container.py +161 -0
- extensions/job_executor/pyproject.toml +16 -0
- extensions/nodes/README.md +0 -0
- extensions/nodes/__init__.py +0 -0
- extensions/nodes/conditional.py +301 -0
- extensions/nodes/fail.py +78 -0
- extensions/nodes/loop.py +394 -0
- extensions/nodes/map.py +477 -0
- extensions/nodes/parallel.py +281 -0
- extensions/nodes/pyproject.toml +15 -0
- extensions/nodes/stub.py +93 -0
- extensions/nodes/success.py +78 -0
- extensions/nodes/task.py +156 -0
- extensions/pipeline_executor/README.md +0 -0
- extensions/pipeline_executor/__init__.py +871 -0
- extensions/pipeline_executor/argo.py +1266 -0
- extensions/pipeline_executor/emulate.py +119 -0
- extensions/pipeline_executor/local.py +226 -0
- extensions/pipeline_executor/local_container.py +369 -0
- extensions/pipeline_executor/mocked.py +159 -0
- extensions/pipeline_executor/pyproject.toml +16 -0
- extensions/run_log_store/README.md +0 -0
- extensions/run_log_store/__init__.py +0 -0
- extensions/run_log_store/any_path.py +100 -0
- extensions/run_log_store/chunked_fs.py +122 -0
- extensions/run_log_store/chunked_minio.py +141 -0
- extensions/run_log_store/file_system.py +91 -0
- extensions/run_log_store/generic_chunked.py +549 -0
- extensions/run_log_store/minio.py +114 -0
- extensions/run_log_store/pyproject.toml +15 -0
- extensions/secrets/README.md +0 -0
- extensions/secrets/dotenv.py +62 -0
- extensions/secrets/pyproject.toml +15 -0
- runnable/__init__.py +108 -0
- runnable/catalog.py +141 -0
- runnable/cli.py +484 -0
- runnable/context.py +730 -0
- runnable/datastore.py +1058 -0
- runnable/defaults.py +159 -0
- runnable/entrypoints.py +390 -0
- runnable/exceptions.py +137 -0
- runnable/executor.py +561 -0
- runnable/gantt.py +1646 -0
- runnable/graph.py +501 -0
- runnable/names.py +546 -0
- runnable/nodes.py +593 -0
- runnable/parameters.py +217 -0
- runnable/pickler.py +96 -0
- runnable/sdk.py +1277 -0
- runnable/secrets.py +92 -0
- runnable/tasks.py +1268 -0
- runnable/telemetry.py +142 -0
- runnable/utils.py +423 -0
- runnable-0.50.0.dist-info/METADATA +189 -0
- runnable-0.50.0.dist-info/RECORD +72 -0
- runnable-0.50.0.dist-info/WHEEL +4 -0
- runnable-0.50.0.dist-info/entry_points.txt +53 -0
- runnable-0.50.0.dist-info/licenses/LICENSE +201 -0
runnable/parameters.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any, Dict, Optional, Type, get_origin
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict
|
|
9
|
+
from typing_extensions import Callable
|
|
10
|
+
|
|
11
|
+
from runnable import defaults
|
|
12
|
+
from runnable.datastore import JsonParameter, ObjectParameter
|
|
13
|
+
from runnable.defaults import IterableParameterModel
|
|
14
|
+
from runnable.utils import remove_prefix
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(defaults.LOGGER_NAME)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _safe_issubclass_basemodel(annotation) -> bool:
|
|
20
|
+
"""Safely check if annotation is BaseModel, handling any type annotation."""
|
|
21
|
+
try:
|
|
22
|
+
return inspect.isclass(annotation) and issubclass(annotation, BaseModel)
|
|
23
|
+
except TypeError:
|
|
24
|
+
# Handle generic types
|
|
25
|
+
origin = get_origin(annotation)
|
|
26
|
+
if origin and inspect.isclass(origin):
|
|
27
|
+
return issubclass(origin, BaseModel)
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_user_set_parameters(remove: bool = False) -> Dict[str, JsonParameter]:
|
|
32
|
+
"""
|
|
33
|
+
Scans the environment variables for any user returned parameters that have a prefix runnable_PRM_.
|
|
34
|
+
|
|
35
|
+
This function does not deal with any type conversion of the parameters.
|
|
36
|
+
It just deserializes the parameters and returns them as a dictionary.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
remove (bool, optional): Flag to remove the parameter if needed. Defaults to False.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
dict: The dictionary of found user returned parameters
|
|
43
|
+
"""
|
|
44
|
+
parameters: Dict[str, JsonParameter] = {}
|
|
45
|
+
for env_var, value in os.environ.items():
|
|
46
|
+
if env_var.startswith(defaults.PARAMETER_PREFIX):
|
|
47
|
+
key = remove_prefix(env_var, defaults.PARAMETER_PREFIX)
|
|
48
|
+
try:
|
|
49
|
+
parameters[key.lower()] = JsonParameter(
|
|
50
|
+
kind="json", value=json.loads(value)
|
|
51
|
+
)
|
|
52
|
+
except json.decoder.JSONDecodeError:
|
|
53
|
+
logger.warning(
|
|
54
|
+
f"Parameter {key} could not be JSON decoded, adding the literal value"
|
|
55
|
+
)
|
|
56
|
+
parameters[key.lower()] = JsonParameter(kind="json", value=value)
|
|
57
|
+
|
|
58
|
+
if remove:
|
|
59
|
+
del os.environ[env_var]
|
|
60
|
+
return parameters
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def return_json_parameters(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
64
|
+
"""
|
|
65
|
+
Returns the parameters as a JSON serializable dictionary.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
params (dict): The parameters to serialize.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
dict: The JSON serializable dictionary.
|
|
72
|
+
"""
|
|
73
|
+
return_params = {}
|
|
74
|
+
for key, value in params.items():
|
|
75
|
+
if isinstance(value, ObjectParameter):
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
return_params[key] = value.get_value()
|
|
79
|
+
return return_params
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def filter_arguments_for_func(
|
|
83
|
+
func: Callable[..., Any],
|
|
84
|
+
params: Dict[str, Any],
|
|
85
|
+
iter_variable: Optional[IterableParameterModel] = None,
|
|
86
|
+
) -> Dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Inspects the function to be called as part of the pipeline to find the arguments of the function.
|
|
89
|
+
Matches the function arguments to the parameters available either by static parameters or by up stream steps.
|
|
90
|
+
|
|
91
|
+
The function "func" signature could be:
|
|
92
|
+
- def my_function(arg1: int, arg2: str, arg3: float):
|
|
93
|
+
- def my_function(arg1: int, arg2: str, arg3: float, **kwargs):
|
|
94
|
+
in this case, we would need to send in remaining keyword arguments as a dictionary.
|
|
95
|
+
- def my_function(arg1: int, arg2: str, arg3: float, args: argparse.Namespace):
|
|
96
|
+
In this case, we need to send the rest of the parameters as attributes of the args object.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
func (Callable): The function to inspect
|
|
100
|
+
parameters (dict): The parameters available for the run
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
dict: The parameters matching the function signature
|
|
104
|
+
"""
|
|
105
|
+
function_args = inspect.signature(func).parameters
|
|
106
|
+
|
|
107
|
+
# Update parameters with the map variables
|
|
108
|
+
for key, v in (
|
|
109
|
+
(iter_variable.map_variable if iter_variable else None) or {}
|
|
110
|
+
).items():
|
|
111
|
+
params[key] = JsonParameter(kind="json", value=v.value)
|
|
112
|
+
|
|
113
|
+
bound_args = {}
|
|
114
|
+
var_keyword_param = None
|
|
115
|
+
namespace_param = None
|
|
116
|
+
|
|
117
|
+
# First pass: Handle regular parameters and identify special parameters
|
|
118
|
+
for name, value in function_args.items():
|
|
119
|
+
# Ignore any *args
|
|
120
|
+
if value.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
121
|
+
logger.warning(f"Ignoring parameter {name} as it is VAR_POSITIONAL")
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Check for **kwargs parameter, we need to send in all the unnamed values in this as a dict
|
|
125
|
+
if value.kind == inspect.Parameter.VAR_KEYWORD:
|
|
126
|
+
var_keyword_param = name
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
# Check for argparse.Namespace parameter, we need to send in all the unnamed values in this as a namespace
|
|
130
|
+
if value.annotation == argparse.Namespace:
|
|
131
|
+
namespace_param = name
|
|
132
|
+
continue
|
|
133
|
+
|
|
134
|
+
# Handle regular parameters
|
|
135
|
+
if name not in params:
|
|
136
|
+
if value.default != inspect.Parameter.empty:
|
|
137
|
+
# Default value is given in the function signature, we can use it
|
|
138
|
+
bound_args[name] = value.default
|
|
139
|
+
else:
|
|
140
|
+
# This is a required parameter that's missing - error immediately
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"Function {func.__name__} has required parameter '{name}' that is not present in the parameters"
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
# We have a parameter of this name, lets bind it
|
|
146
|
+
param_value = params[name]
|
|
147
|
+
|
|
148
|
+
if _safe_issubclass_basemodel(value.annotation) and not isinstance(
|
|
149
|
+
param_value, ObjectParameter
|
|
150
|
+
):
|
|
151
|
+
# Even if the annotation is a pydantic model, it can be passed as an object parameter
|
|
152
|
+
# We try to cast it as a pydantic model if asked
|
|
153
|
+
named_param = params[name].get_value()
|
|
154
|
+
|
|
155
|
+
if not isinstance(named_param, dict):
|
|
156
|
+
# A case where the parameter is a one attribute model
|
|
157
|
+
named_param = {name: named_param}
|
|
158
|
+
|
|
159
|
+
bound_model = bind_args_for_pydantic_model(
|
|
160
|
+
named_param, value.annotation
|
|
161
|
+
)
|
|
162
|
+
bound_args[name] = bound_model
|
|
163
|
+
elif isinstance(param_value, ObjectParameter):
|
|
164
|
+
# Directly pass the object parameter value
|
|
165
|
+
bound_args[name] = param_value.get_value()
|
|
166
|
+
elif value.annotation is not inspect.Parameter.empty and callable(
|
|
167
|
+
value.annotation
|
|
168
|
+
):
|
|
169
|
+
# Cast it if its a primitive type. Ensure the type matches the annotation.
|
|
170
|
+
try:
|
|
171
|
+
# Handle typing generics like Dict[str, int], List[str] by using their origin
|
|
172
|
+
origin = get_origin(value.annotation)
|
|
173
|
+
if origin is not None:
|
|
174
|
+
# For generics like Dict[str, int], use dict() instead of Dict[str, int]()
|
|
175
|
+
bound_args[name] = origin(params[name].get_value())
|
|
176
|
+
else:
|
|
177
|
+
# Regular callable types like int, str, float, etc.
|
|
178
|
+
bound_args[name] = value.annotation(params[name].get_value())
|
|
179
|
+
except (ValueError, TypeError) as e:
|
|
180
|
+
annotation_name = getattr(
|
|
181
|
+
value.annotation, "__name__", str(value.annotation)
|
|
182
|
+
)
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"Cannot cast parameter '{name}' to {annotation_name}: {e}"
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
# We do not know type of parameter, we send the value as found
|
|
188
|
+
bound_args[name] = params[name].get_value()
|
|
189
|
+
|
|
190
|
+
# Find extra parameters (parameters in params but not consumed by regular function parameters)
|
|
191
|
+
consumed_param_names = set(bound_args.keys())
|
|
192
|
+
extra_params = {k: v for k, v in params.items() if k not in consumed_param_names}
|
|
193
|
+
|
|
194
|
+
# Second pass: Handle **kwargs and argparse.Namespace parameters
|
|
195
|
+
if var_keyword_param is not None:
|
|
196
|
+
# Function accepts **kwargs - add all extra parameters directly to bound_args
|
|
197
|
+
for param_name, param_value in extra_params.items():
|
|
198
|
+
bound_args[param_name] = param_value.get_value()
|
|
199
|
+
elif namespace_param is not None:
|
|
200
|
+
# Function accepts argparse.Namespace - create namespace with extra parameters
|
|
201
|
+
args_namespace = argparse.Namespace()
|
|
202
|
+
for param_name, param_value in extra_params.items():
|
|
203
|
+
setattr(args_namespace, param_name, param_value.get_value())
|
|
204
|
+
bound_args[namespace_param] = args_namespace
|
|
205
|
+
|
|
206
|
+
return bound_args
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def bind_args_for_pydantic_model(
|
|
210
|
+
params: Dict[str, Any], model: Type[BaseModel]
|
|
211
|
+
) -> BaseModel:
|
|
212
|
+
class EasyModel(model): # type: ignore
|
|
213
|
+
model_config = ConfigDict(extra="ignore")
|
|
214
|
+
|
|
215
|
+
swallow_all = EasyModel(**params)
|
|
216
|
+
bound_model = model(**swallow_all.model_dump())
|
|
217
|
+
return bound_model
|
runnable/pickler.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import dill as pickle
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BasePickler(ABC, BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
The base class for all pickler.
|
|
11
|
+
|
|
12
|
+
We are still in the process of hardening the design of this class.
|
|
13
|
+
For now, we are just going to use pickle.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
extension: str = ""
|
|
17
|
+
service_name: str = ""
|
|
18
|
+
service_type: str = "pickler"
|
|
19
|
+
model_config = ConfigDict(extra="forbid")
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def dump(self, data: Any, path: str):
|
|
23
|
+
"""
|
|
24
|
+
Dump an object to the specified path.
|
|
25
|
+
The path is the full path.
|
|
26
|
+
|
|
27
|
+
To correctly identify the pickler from possible implementations, we use the extension.
|
|
28
|
+
An extension is added automatically, if not provided.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
data (Any): The object to pickle
|
|
32
|
+
path (str): The path to save the pickle file
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
NotImplementedError: Base class has no implementation
|
|
36
|
+
"""
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def load(self, path: str) -> Any:
|
|
41
|
+
"""
|
|
42
|
+
Load the object from the specified path.
|
|
43
|
+
|
|
44
|
+
To correctly identify the pickler from possible implementations, we use the extension.
|
|
45
|
+
An extension is added automatically, if not provided.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
path (str): The path to load the pickled file from.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
NotImplementedError: Base class has no implementation.
|
|
52
|
+
"""
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class NativePickler(BasePickler):
|
|
57
|
+
"""
|
|
58
|
+
Uses native python pickle to load and dump files
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
extension: str = ".dill"
|
|
62
|
+
service_name: str = "pickle"
|
|
63
|
+
|
|
64
|
+
def dump(self, data: Any, path: str):
|
|
65
|
+
"""
|
|
66
|
+
Dump an object to the specified path.
|
|
67
|
+
The path is the full path.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
data (Any): The data to pickle
|
|
71
|
+
path (str): The path to save the pickle file
|
|
72
|
+
"""
|
|
73
|
+
if not path.endswith(self.extension):
|
|
74
|
+
path = path + self.extension
|
|
75
|
+
|
|
76
|
+
with open(path, "wb") as f:
|
|
77
|
+
pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)
|
|
78
|
+
|
|
79
|
+
def load(self, path: str) -> Any:
|
|
80
|
+
"""
|
|
81
|
+
Load the object from the specified path.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
path (str): The path to load the object from.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Any: The data loaded from the file.
|
|
88
|
+
"""
|
|
89
|
+
if not path.endswith(self.extension):
|
|
90
|
+
path = path + self.extension
|
|
91
|
+
|
|
92
|
+
data = None
|
|
93
|
+
with open(path, "rb") as f:
|
|
94
|
+
data = pickle.load(f)
|
|
95
|
+
|
|
96
|
+
return data
|