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.
Files changed (72) hide show
  1. extensions/README.md +0 -0
  2. extensions/__init__.py +0 -0
  3. extensions/catalog/README.md +0 -0
  4. extensions/catalog/any_path.py +214 -0
  5. extensions/catalog/file_system.py +52 -0
  6. extensions/catalog/minio.py +72 -0
  7. extensions/catalog/pyproject.toml +14 -0
  8. extensions/catalog/s3.py +11 -0
  9. extensions/job_executor/README.md +0 -0
  10. extensions/job_executor/__init__.py +236 -0
  11. extensions/job_executor/emulate.py +70 -0
  12. extensions/job_executor/k8s.py +553 -0
  13. extensions/job_executor/k8s_job_spec.yaml +37 -0
  14. extensions/job_executor/local.py +35 -0
  15. extensions/job_executor/local_container.py +161 -0
  16. extensions/job_executor/pyproject.toml +16 -0
  17. extensions/nodes/README.md +0 -0
  18. extensions/nodes/__init__.py +0 -0
  19. extensions/nodes/conditional.py +301 -0
  20. extensions/nodes/fail.py +78 -0
  21. extensions/nodes/loop.py +394 -0
  22. extensions/nodes/map.py +477 -0
  23. extensions/nodes/parallel.py +281 -0
  24. extensions/nodes/pyproject.toml +15 -0
  25. extensions/nodes/stub.py +93 -0
  26. extensions/nodes/success.py +78 -0
  27. extensions/nodes/task.py +156 -0
  28. extensions/pipeline_executor/README.md +0 -0
  29. extensions/pipeline_executor/__init__.py +871 -0
  30. extensions/pipeline_executor/argo.py +1266 -0
  31. extensions/pipeline_executor/emulate.py +119 -0
  32. extensions/pipeline_executor/local.py +226 -0
  33. extensions/pipeline_executor/local_container.py +369 -0
  34. extensions/pipeline_executor/mocked.py +159 -0
  35. extensions/pipeline_executor/pyproject.toml +16 -0
  36. extensions/run_log_store/README.md +0 -0
  37. extensions/run_log_store/__init__.py +0 -0
  38. extensions/run_log_store/any_path.py +100 -0
  39. extensions/run_log_store/chunked_fs.py +122 -0
  40. extensions/run_log_store/chunked_minio.py +141 -0
  41. extensions/run_log_store/file_system.py +91 -0
  42. extensions/run_log_store/generic_chunked.py +549 -0
  43. extensions/run_log_store/minio.py +114 -0
  44. extensions/run_log_store/pyproject.toml +15 -0
  45. extensions/secrets/README.md +0 -0
  46. extensions/secrets/dotenv.py +62 -0
  47. extensions/secrets/pyproject.toml +15 -0
  48. runnable/__init__.py +108 -0
  49. runnable/catalog.py +141 -0
  50. runnable/cli.py +484 -0
  51. runnable/context.py +730 -0
  52. runnable/datastore.py +1058 -0
  53. runnable/defaults.py +159 -0
  54. runnable/entrypoints.py +390 -0
  55. runnable/exceptions.py +137 -0
  56. runnable/executor.py +561 -0
  57. runnable/gantt.py +1646 -0
  58. runnable/graph.py +501 -0
  59. runnable/names.py +546 -0
  60. runnable/nodes.py +593 -0
  61. runnable/parameters.py +217 -0
  62. runnable/pickler.py +96 -0
  63. runnable/sdk.py +1277 -0
  64. runnable/secrets.py +92 -0
  65. runnable/tasks.py +1268 -0
  66. runnable/telemetry.py +142 -0
  67. runnable/utils.py +423 -0
  68. runnable-0.50.0.dist-info/METADATA +189 -0
  69. runnable-0.50.0.dist-info/RECORD +72 -0
  70. runnable-0.50.0.dist-info/WHEEL +4 -0
  71. runnable-0.50.0.dist-info/entry_points.txt +53 -0
  72. 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