runnable 0.37.0__py3-none-any.whl → 1.0.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.
Potentially problematic release.
This version of runnable might be problematic. Click here for more details.
- runnable/cli.py +156 -0
- runnable/gantt.py +1141 -0
- runnable/graph.py +1 -1
- runnable/parameters.py +106 -49
- runnable-1.0.0.dist-info/METADATA +122 -0
- {runnable-0.37.0.dist-info → runnable-1.0.0.dist-info}/RECORD +9 -8
- runnable-0.37.0.dist-info/METADATA +0 -264
- {runnable-0.37.0.dist-info → runnable-1.0.0.dist-info}/WHEEL +0 -0
- {runnable-0.37.0.dist-info → runnable-1.0.0.dist-info}/entry_points.txt +0 -0
- {runnable-0.37.0.dist-info → runnable-1.0.0.dist-info}/licenses/LICENSE +0 -0
runnable/graph.py
CHANGED
|
@@ -329,7 +329,7 @@ def create_graph(dag_config: Dict[str, Any], internal_branch_name: str = "") ->
|
|
|
329
329
|
Returns:
|
|
330
330
|
Graph: The created graph object
|
|
331
331
|
"""
|
|
332
|
-
description: str = dag_config.get("description", None)
|
|
332
|
+
description: str | None = dag_config.get("description", None)
|
|
333
333
|
start_at: str = cast(
|
|
334
334
|
str, dag_config.get("start_at")
|
|
335
335
|
) # Let the start_at be relative to the graph
|
runnable/parameters.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import argparse
|
|
1
2
|
import inspect
|
|
2
3
|
import json
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
5
|
-
from typing import Any, Dict, Type
|
|
6
|
+
from typing import Any, Dict, Type, get_origin
|
|
6
7
|
|
|
7
|
-
import pydantic
|
|
8
8
|
from pydantic import BaseModel, ConfigDict
|
|
9
9
|
from typing_extensions import Callable
|
|
10
10
|
|
|
@@ -48,6 +48,25 @@ def get_user_set_parameters(remove: bool = False) -> Dict[str, JsonParameter]:
|
|
|
48
48
|
return parameters
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def return_json_parameters(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
52
|
+
"""
|
|
53
|
+
Returns the parameters as a JSON serializable dictionary.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
params (dict): The parameters to serialize.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
dict: The JSON serializable dictionary.
|
|
60
|
+
"""
|
|
61
|
+
return_params = {}
|
|
62
|
+
for key, value in params.items():
|
|
63
|
+
if isinstance(value, ObjectParameter):
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
return_params[key] = value.get_value()
|
|
67
|
+
return return_params
|
|
68
|
+
|
|
69
|
+
|
|
51
70
|
def filter_arguments_for_func(
|
|
52
71
|
func: Callable[..., Any],
|
|
53
72
|
params: Dict[str, Any],
|
|
@@ -55,8 +74,14 @@ def filter_arguments_for_func(
|
|
|
55
74
|
) -> Dict[str, Any]:
|
|
56
75
|
"""
|
|
57
76
|
Inspects the function to be called as part of the pipeline to find the arguments of the function.
|
|
58
|
-
Matches the function arguments to the parameters available either by
|
|
77
|
+
Matches the function arguments to the parameters available either by static parameters or by up stream steps.
|
|
59
78
|
|
|
79
|
+
The function "func" signature could be:
|
|
80
|
+
- def my_function(arg1: int, arg2: str, arg3: float):
|
|
81
|
+
- def my_function(arg1: int, arg2: str, arg3: float, **kwargs):
|
|
82
|
+
in this case, we would need to send in remaining keyword arguments as a dictionary.
|
|
83
|
+
- def my_function(arg1: int, arg2: str, arg3: float, args: argparse.Namespace):
|
|
84
|
+
In this case, we need to send the rest of the parameters as attributes of the args object.
|
|
60
85
|
|
|
61
86
|
Args:
|
|
62
87
|
func (Callable): The function to inspect
|
|
@@ -72,63 +97,95 @@ def filter_arguments_for_func(
|
|
|
72
97
|
params[key] = JsonParameter(kind="json", value=v)
|
|
73
98
|
|
|
74
99
|
bound_args = {}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# If **kwargs is present in the function signature, we send back everything
|
|
78
|
-
for name, value in function_args.items():
|
|
79
|
-
if value.kind != inspect.Parameter.VAR_KEYWORD:
|
|
80
|
-
continue
|
|
81
|
-
# Found VAR_KEYWORD, we send back everything as found
|
|
82
|
-
for key, value in params.items():
|
|
83
|
-
bound_args[key] = params[key].get_value()
|
|
84
|
-
|
|
85
|
-
return bound_args
|
|
100
|
+
var_keyword_param = None
|
|
101
|
+
namespace_param = None
|
|
86
102
|
|
|
87
|
-
#
|
|
103
|
+
# First pass: Handle regular parameters and identify special parameters
|
|
88
104
|
for name, value in function_args.items():
|
|
89
105
|
# Ignore any *args
|
|
90
106
|
if value.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
91
107
|
logger.warning(f"Ignoring parameter {name} as it is VAR_POSITIONAL")
|
|
92
108
|
continue
|
|
93
109
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
# No default value is given in the function signature. error as parameter is required.
|
|
98
|
-
raise ValueError(
|
|
99
|
-
f"Parameter {name} is required for {func.__name__} but not provided"
|
|
100
|
-
)
|
|
101
|
-
# default value is given in the function signature, nothing further to do.
|
|
110
|
+
# Check for **kwargs parameter, we need to send in all the unnamed values in this as a dict
|
|
111
|
+
if value.kind == inspect.Parameter.VAR_KEYWORD:
|
|
112
|
+
var_keyword_param = name
|
|
102
113
|
continue
|
|
103
114
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
pydantic._internal._model_construction.ModelMetaclass,
|
|
109
|
-
] and not isinstance(param_value, ObjectParameter):
|
|
110
|
-
# Even if the annotation is a pydantic model, it can be passed as an object parameter
|
|
111
|
-
# We try to cast it as a pydantic model if asked
|
|
112
|
-
named_param = params[name].get_value()
|
|
113
|
-
|
|
114
|
-
if not isinstance(named_param, dict):
|
|
115
|
-
# A case where the parameter is a one attribute model
|
|
116
|
-
named_param = {name: named_param}
|
|
117
|
-
|
|
118
|
-
bound_model = bind_args_for_pydantic_model(named_param, value.annotation)
|
|
119
|
-
bound_args[name] = bound_model
|
|
115
|
+
# Check for argparse.Namespace parameter, we need to send in all the unnamed values in this as a namespace
|
|
116
|
+
if value.annotation == argparse.Namespace:
|
|
117
|
+
namespace_param = name
|
|
118
|
+
continue
|
|
120
119
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
# Handle regular parameters
|
|
121
|
+
if name not in params:
|
|
122
|
+
if value.default != inspect.Parameter.empty:
|
|
123
|
+
# Default value is given in the function signature, we can use it
|
|
124
|
+
bound_args[name] = value.default
|
|
125
|
+
else:
|
|
126
|
+
# This is a required parameter that's missing - error immediately
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"Function {func.__name__} has required parameter '{name}' that is not present in the parameters"
|
|
129
|
+
)
|
|
124
130
|
else:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
# We have a parameter of this name, lets bind it
|
|
132
|
+
param_value = params[name]
|
|
133
|
+
|
|
134
|
+
if (issubclass(value.annotation, BaseModel)) and not isinstance(
|
|
135
|
+
param_value, ObjectParameter
|
|
136
|
+
):
|
|
137
|
+
# Even if the annotation is a pydantic model, it can be passed as an object parameter
|
|
138
|
+
# We try to cast it as a pydantic model if asked
|
|
139
|
+
named_param = params[name].get_value()
|
|
140
|
+
|
|
141
|
+
if not isinstance(named_param, dict):
|
|
142
|
+
# A case where the parameter is a one attribute model
|
|
143
|
+
named_param = {name: named_param}
|
|
144
|
+
|
|
145
|
+
bound_model = bind_args_for_pydantic_model(
|
|
146
|
+
named_param, value.annotation
|
|
147
|
+
)
|
|
148
|
+
bound_args[name] = bound_model
|
|
149
|
+
|
|
150
|
+
elif value.annotation is not inspect.Parameter.empty and callable(
|
|
151
|
+
value.annotation
|
|
152
|
+
):
|
|
153
|
+
# Cast it if its a primitive type. Ensure the type matches the annotation.
|
|
154
|
+
try:
|
|
155
|
+
# Handle typing generics like Dict[str, int], List[str] by using their origin
|
|
156
|
+
origin = get_origin(value.annotation)
|
|
157
|
+
if origin is not None:
|
|
158
|
+
# For generics like Dict[str, int], use dict() instead of Dict[str, int]()
|
|
159
|
+
bound_args[name] = origin(params[name].get_value())
|
|
160
|
+
else:
|
|
161
|
+
# Regular callable types like int, str, float, etc.
|
|
162
|
+
bound_args[name] = value.annotation(params[name].get_value())
|
|
163
|
+
except (ValueError, TypeError) as e:
|
|
164
|
+
annotation_name = getattr(
|
|
165
|
+
value.annotation, "__name__", str(value.annotation)
|
|
166
|
+
)
|
|
167
|
+
raise ValueError(
|
|
168
|
+
f"Cannot cast parameter '{name}' to {annotation_name}: {e}"
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
# We do not know type of parameter, we send the value as found
|
|
172
|
+
bound_args[name] = params[name].get_value()
|
|
173
|
+
|
|
174
|
+
# Find extra parameters (parameters in params but not consumed by regular function parameters)
|
|
175
|
+
consumed_param_names = set(bound_args.keys())
|
|
176
|
+
extra_params = {k: v for k, v in params.items() if k not in consumed_param_names}
|
|
177
|
+
|
|
178
|
+
# Second pass: Handle **kwargs and argparse.Namespace parameters
|
|
179
|
+
if var_keyword_param is not None:
|
|
180
|
+
# Function accepts **kwargs - add all extra parameters directly to bound_args
|
|
181
|
+
for param_name, param_value in extra_params.items():
|
|
182
|
+
bound_args[param_name] = param_value.get_value()
|
|
183
|
+
elif namespace_param is not None:
|
|
184
|
+
# Function accepts argparse.Namespace - create namespace with extra parameters
|
|
185
|
+
args_namespace = argparse.Namespace()
|
|
186
|
+
for param_name, param_value in extra_params.items():
|
|
187
|
+
setattr(args_namespace, param_name, param_value.get_value())
|
|
188
|
+
bound_args[namespace_param] = args_namespace
|
|
132
189
|
|
|
133
190
|
return bound_args
|
|
134
191
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: runnable
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: "Vammi, Vijay" <vijay.vammi@astrazeneca.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: cloudpathlib>=0.20.0
|
|
9
|
+
Requires-Dist: dill>=0.3.9
|
|
10
|
+
Requires-Dist: pydantic>=2.10.3
|
|
11
|
+
Requires-Dist: python-dotenv>=1.0.1
|
|
12
|
+
Requires-Dist: rich>=13.9.4
|
|
13
|
+
Requires-Dist: ruamel-yaml>=0.18.6
|
|
14
|
+
Requires-Dist: setuptools>=75.6.0
|
|
15
|
+
Requires-Dist: stevedore>=5.4.0
|
|
16
|
+
Requires-Dist: typer>=0.17.3
|
|
17
|
+
Provides-Extra: docker
|
|
18
|
+
Requires-Dist: docker>=7.1.0; extra == 'docker'
|
|
19
|
+
Provides-Extra: examples
|
|
20
|
+
Requires-Dist: pandas>=2.2.3; extra == 'examples'
|
|
21
|
+
Provides-Extra: examples-torch
|
|
22
|
+
Requires-Dist: torch>=2.7.1; extra == 'examples-torch'
|
|
23
|
+
Provides-Extra: k8s
|
|
24
|
+
Requires-Dist: kubernetes>=31.0.0; extra == 'k8s'
|
|
25
|
+
Provides-Extra: notebook
|
|
26
|
+
Requires-Dist: ploomber-engine>=0.0.33; extra == 'notebook'
|
|
27
|
+
Provides-Extra: s3
|
|
28
|
+
Requires-Dist: cloudpathlib[s3]; extra == 's3'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Runnable
|
|
32
|
+
|
|
33
|
+
<img style="float: right;" alt="Runnable" src="docs/assets/sport.png" width="100" height="100">
|
|
34
|
+
|
|
35
|
+
**Transform any Python function into a portable, trackable pipeline in seconds.**
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<a href="https://pypi.org/project/runnable/"><img alt="python:" src="https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10-blue.svg"></a>
|
|
39
|
+
<a href="https://pypi.org/project/runnable/"><img alt="Pypi" src="https://badge.fury.io/py/runnable.svg"></a>
|
|
40
|
+
<a href="https://github.com/AstraZeneca/runnable/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/license-Apache%202.0-blue.svg"></a>
|
|
41
|
+
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
|
|
42
|
+
<a href="https://github.com/python/mypy"><img alt="MyPy Checked" src="https://www.mypy-lang.org/static/mypy_badge.svg"></a>
|
|
43
|
+
<a href="https://github.com/AstraZeneca/runnable/actions/workflows/release.yaml"><img alt="Tests:" src="https://github.com/AstraZeneca/runnable/actions/workflows/release.yaml/badge.svg">
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 🚀 30-Second Transformation
|
|
49
|
+
|
|
50
|
+
**Your existing function (unchanged!):**
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
def analyze_sales():
|
|
54
|
+
total_revenue = 50000
|
|
55
|
+
best_product = "widgets"
|
|
56
|
+
return total_revenue, best_product
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Make it runnable everywhere (2 lines):**
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from runnable import PythonJob
|
|
63
|
+
PythonJob(function=analyze_sales).execute()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**🎉 Success!** Your function now runs the same on laptop, containers, and Kubernetes with automatic tracking and reproducibility.
|
|
67
|
+
|
|
68
|
+
## 🔗 Chain Functions Without Glue Code
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
def load_customer_data():
|
|
72
|
+
return {"count": 1500, "segments": ["premium", "standard"]}
|
|
73
|
+
|
|
74
|
+
def analyze_segments(customer_data): # Name matches = automatic connection
|
|
75
|
+
return {"premium_pct": 30, "growth_potential": "high"}
|
|
76
|
+
|
|
77
|
+
# What Runnable needs (same logic, no glue):
|
|
78
|
+
from runnable import Pipeline, PythonTask
|
|
79
|
+
Pipeline(steps=[
|
|
80
|
+
PythonTask(function=load_customer_data, returns=["customer_data"]),
|
|
81
|
+
PythonTask(function=analyze_segments, returns=["analysis"])
|
|
82
|
+
]).execute()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Same pipeline runs unchanged on laptop, containers, and Kubernetes.**
|
|
86
|
+
|
|
87
|
+
## ⚡ Installation
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install runnable
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 📊 Why Choose Runnable?
|
|
94
|
+
|
|
95
|
+
- **🎯 Easy to adopt**: Your code remains as-is, no decorators or imposed structure
|
|
96
|
+
- **🏗️ Bring your infrastructure**: Works with your platforms, not a replacement
|
|
97
|
+
- **📝 Reproducibility**: Automatic tracking without additional code
|
|
98
|
+
- **🔁 Retry failures**: Debug anywhere, retry from failure points
|
|
99
|
+
- **🧪 Testing**: Mock/patch pipeline steps, test functions normally
|
|
100
|
+
- **💔 Move on**: Easy removal - just delete runnable files, your code stays
|
|
101
|
+
|
|
102
|
+
## 📖 Documentation
|
|
103
|
+
|
|
104
|
+
**[Complete Documentation →](https://astrazeneca.github.io/runnable/)**
|
|
105
|
+
|
|
106
|
+
## 🔀 Pipeline Types
|
|
107
|
+
|
|
108
|
+
### Linear Pipelines
|
|
109
|
+
Simple sequential execution of Python functions, notebooks, or shell scripts.
|
|
110
|
+
|
|
111
|
+
### Parallel Branches
|
|
112
|
+
Execute multiple branches simultaneously for improved performance.
|
|
113
|
+
|
|
114
|
+
### Map Patterns
|
|
115
|
+
Execute pipelines over iterable parameters for batch processing.
|
|
116
|
+
|
|
117
|
+
### Arbitrary Nesting
|
|
118
|
+
Combine parallel, map, and sequential patterns as needed.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
**Ready to get started?** Check out our [30-second demo](https://astrazeneca.github.io/runnable/) for immediate results!
|
|
@@ -49,24 +49,25 @@ extensions/secrets/dotenv.py,sha256=nADHXI6KJ_LUYOIe5EbtYH-21OBebSNVr0Pjb1GlZ7w,
|
|
|
49
49
|
extensions/secrets/pyproject.toml,sha256=mLJNImNcBlbLKHh-0ugVWT9V83R4RibyyYDtBCSqVF4,282
|
|
50
50
|
runnable/__init__.py,sha256=MN9x2jmQb2eOr-rap1DXLzNSC926U-aad_YwENzG52w,509
|
|
51
51
|
runnable/catalog.py,sha256=6l0tT0jwHi40aE6fhQMgYtYe_-2As-bRKztAKiFvy3o,3842
|
|
52
|
-
runnable/cli.py,sha256=
|
|
52
|
+
runnable/cli.py,sha256=q6-5TnrOpqJmGQ8VOfm-nBT1g2UAFo-9znqNqECyZvU,12987
|
|
53
53
|
runnable/context.py,sha256=mLpq5rtMsPawjnaN9Woq7HWZ1FAppeudZtYMT5vf6Fo,17594
|
|
54
54
|
runnable/datastore.py,sha256=2pYg4i1JRMzw_CUUIsPOWt7wYPiGBamfo-CPVAkEH54,32375
|
|
55
55
|
runnable/defaults.py,sha256=4UYuShnjEyWP529UlFnubvkBpOcczKIdE4jEOhPBwl4,3076
|
|
56
56
|
runnable/entrypoints.py,sha256=46prgr3_FYtBMlRbUXIDSpgZUBgaxcdJAekXhgEIj7M,6578
|
|
57
57
|
runnable/exceptions.py,sha256=t5tSlYqe_EjU5liXu32yLLh_yrnXeFL93BuXfmQzV98,3268
|
|
58
58
|
runnable/executor.py,sha256=CwzHkeGVpocACZLzfFS94TzKeiaPLv4NtXtvT3eoocY,15222
|
|
59
|
-
runnable/
|
|
59
|
+
runnable/gantt.py,sha256=hdFovfvnxDK3tAQOl6OLAiaxLBAE-wjPyszPVk5Ogds,40726
|
|
60
|
+
runnable/graph.py,sha256=ukJo_sqtBRD_ZX7ULbd2GnvpToAwGHcAowXPcqKjC4Q,16543
|
|
60
61
|
runnable/names.py,sha256=A9ldUyULXuWjJ1MoXihHqlg-xeTVX-oWYTO5Ah0trmo,8128
|
|
61
62
|
runnable/nodes.py,sha256=JHBxJib7SSQXY51bLHBXUvb0DlNSLNvyqz3JNEDLt8c,16926
|
|
62
|
-
runnable/parameters.py,sha256=
|
|
63
|
+
runnable/parameters.py,sha256=zxP_KnSoGFpo7fwKE7zlrQ7CWRvA30SK_8TeMipNzcU,8131
|
|
63
64
|
runnable/pickler.py,sha256=ydJ_eti_U1F4l-YacFp7BWm6g5vTn04UXye25S1HVok,2684
|
|
64
65
|
runnable/sdk.py,sha256=blLBWzXV2x7jxKQXWpjmeJ9k22jt5CKBQBqQpnt4agk,32587
|
|
65
66
|
runnable/secrets.py,sha256=4L_dBFxTgr8r_hHUD6RlZEtqaOHDRsFG5PXO5wlvMI0,2324
|
|
66
67
|
runnable/tasks.py,sha256=7yuoeG4ZqfxFUmN4mPS4i6kbQmzEpAwbPQweAUWY-ic,31366
|
|
67
68
|
runnable/utils.py,sha256=amHW3KR_NGTDysGHcSafhh5WJUX7GPBSxqdPyzAIhao,11350
|
|
68
|
-
runnable-0.
|
|
69
|
-
runnable-0.
|
|
70
|
-
runnable-0.
|
|
71
|
-
runnable-0.
|
|
72
|
-
runnable-0.
|
|
69
|
+
runnable-1.0.0.dist-info/METADATA,sha256=RL4wbnhywrdotJhuSGwaIBRJDglUKAbuWEe004G5BWQ,4297
|
|
70
|
+
runnable-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
71
|
+
runnable-1.0.0.dist-info/entry_points.txt,sha256=KkxihZ0LLEiwvFl7RquyqZ0tp2fJDIs7DgzHYDlmc3U,2018
|
|
72
|
+
runnable-1.0.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
73
|
+
runnable-1.0.0.dist-info/RECORD,,
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: runnable
|
|
3
|
-
Version: 0.37.0
|
|
4
|
-
Summary: Add your description here
|
|
5
|
-
Author-email: "Vammi, Vijay" <vijay.vammi@astrazeneca.com>
|
|
6
|
-
License-File: LICENSE
|
|
7
|
-
Requires-Python: >=3.10
|
|
8
|
-
Requires-Dist: click-plugins>=1.1.1
|
|
9
|
-
Requires-Dist: click<=8.1.3
|
|
10
|
-
Requires-Dist: cloudpathlib>=0.20.0
|
|
11
|
-
Requires-Dist: dill>=0.3.9
|
|
12
|
-
Requires-Dist: pydantic>=2.10.3
|
|
13
|
-
Requires-Dist: python-dotenv>=1.0.1
|
|
14
|
-
Requires-Dist: rich>=13.9.4
|
|
15
|
-
Requires-Dist: ruamel-yaml>=0.18.6
|
|
16
|
-
Requires-Dist: setuptools>=75.6.0
|
|
17
|
-
Requires-Dist: stevedore>=5.4.0
|
|
18
|
-
Requires-Dist: typer>=0.15.1
|
|
19
|
-
Provides-Extra: docker
|
|
20
|
-
Requires-Dist: docker>=7.1.0; extra == 'docker'
|
|
21
|
-
Provides-Extra: examples
|
|
22
|
-
Requires-Dist: pandas>=2.2.3; extra == 'examples'
|
|
23
|
-
Provides-Extra: k8s
|
|
24
|
-
Requires-Dist: kubernetes>=31.0.0; extra == 'k8s'
|
|
25
|
-
Provides-Extra: notebook
|
|
26
|
-
Requires-Dist: ploomber-engine>=0.0.33; extra == 'notebook'
|
|
27
|
-
Provides-Extra: s3
|
|
28
|
-
Requires-Dist: cloudpathlib[s3]; extra == 's3'
|
|
29
|
-
Description-Content-Type: text/markdown
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
</p>
|
|
37
|
-
<hr style="border:2px dotted orange">
|
|
38
|
-
|
|
39
|
-
<p align="center">
|
|
40
|
-
<a href="https://pypi.org/project/runnable/"><img alt="python:" src="https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10-blue.svg"></a>
|
|
41
|
-
<a href="https://pypi.org/project/runnable/"><img alt="Pypi" src="https://badge.fury.io/py/runnable.svg"></a>
|
|
42
|
-
<a href="https://github.com/vijayvammi/runnable/blob/main/LICENSE"><img alt"License" src="https://img.shields.io/badge/license-Apache%202.0-blue.svg"></a>
|
|
43
|
-
<a href="https://github.com/psf/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
|
|
44
|
-
<a href="https://github.com/python/mypy"><img alt="MyPy Checked" src="https://www.mypy-lang.org/static/mypy_badge.svg"></a>
|
|
45
|
-
<a href="https://github.com/vijayvammi/runnable/actions/workflows/release.yaml"><img alt="Tests:" src="https://github.com/vijayvammi/runnable/actions/workflows/release.yaml/badge.svg">
|
|
46
|
-
</p>
|
|
47
|
-
<hr style="border:2px dotted orange">
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
[Please check here for complete documentation](https://astrazeneca.github.io/runnable/)
|
|
51
|
-
|
|
52
|
-
## Example
|
|
53
|
-
|
|
54
|
-
The below data science flavored code is a well-known
|
|
55
|
-
[iris example from scikit-learn](https://scikit-learn.org/stable/auto_examples/linear_model/plot_iris_logistic.html).
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```python
|
|
59
|
-
"""
|
|
60
|
-
Example of Logistic regression using scikit-learn
|
|
61
|
-
https://scikit-learn.org/stable/auto_examples/linear_model/plot_iris_logistic.html
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
import matplotlib.pyplot as plt
|
|
65
|
-
import numpy as np
|
|
66
|
-
from sklearn import datasets
|
|
67
|
-
from sklearn.inspection import DecisionBoundaryDisplay
|
|
68
|
-
from sklearn.linear_model import LogisticRegression
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def load_data():
|
|
72
|
-
# import some data to play with
|
|
73
|
-
iris = datasets.load_iris()
|
|
74
|
-
X = iris.data[:, :2] # we only take the first two features.
|
|
75
|
-
Y = iris.target
|
|
76
|
-
|
|
77
|
-
return X, Y
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def model_fit(X: np.ndarray, Y: np.ndarray, C: float = 1e5):
|
|
81
|
-
logreg = LogisticRegression(C=C)
|
|
82
|
-
logreg.fit(X, Y)
|
|
83
|
-
|
|
84
|
-
return logreg
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def generate_plots(X: np.ndarray, Y: np.ndarray, logreg: LogisticRegression):
|
|
88
|
-
_, ax = plt.subplots(figsize=(4, 3))
|
|
89
|
-
DecisionBoundaryDisplay.from_estimator(
|
|
90
|
-
logreg,
|
|
91
|
-
X,
|
|
92
|
-
cmap=plt.cm.Paired,
|
|
93
|
-
ax=ax,
|
|
94
|
-
response_method="predict",
|
|
95
|
-
plot_method="pcolormesh",
|
|
96
|
-
shading="auto",
|
|
97
|
-
xlabel="Sepal length",
|
|
98
|
-
ylabel="Sepal width",
|
|
99
|
-
eps=0.5,
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
# Plot also the training points
|
|
103
|
-
plt.scatter(X[:, 0], X[:, 1], c=Y, edgecolors="k", cmap=plt.cm.Paired)
|
|
104
|
-
|
|
105
|
-
plt.xticks(())
|
|
106
|
-
plt.yticks(())
|
|
107
|
-
|
|
108
|
-
plt.savefig("iris_logistic.png")
|
|
109
|
-
|
|
110
|
-
# TODO: What is the right value?
|
|
111
|
-
return 0.6
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
## Without any orchestration
|
|
115
|
-
def main():
|
|
116
|
-
X, Y = load_data()
|
|
117
|
-
logreg = model_fit(X, Y, C=1.0)
|
|
118
|
-
generate_plots(X, Y, logreg)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
## With runnable orchestration
|
|
122
|
-
def runnable_pipeline():
|
|
123
|
-
# The below code can be anywhere
|
|
124
|
-
from runnable import Catalog, Pipeline, PythonTask, metric, pickled
|
|
125
|
-
|
|
126
|
-
# X, Y = load_data()
|
|
127
|
-
load_data_task = PythonTask(
|
|
128
|
-
function=load_data,
|
|
129
|
-
name="load_data",
|
|
130
|
-
returns=[pickled("X"), pickled("Y")], # (1)
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
# logreg = model_fit(X, Y, C=1.0)
|
|
134
|
-
model_fit_task = PythonTask(
|
|
135
|
-
function=model_fit,
|
|
136
|
-
name="model_fit",
|
|
137
|
-
returns=[pickled("logreg")],
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
# generate_plots(X, Y, logreg)
|
|
141
|
-
generate_plots_task = PythonTask(
|
|
142
|
-
function=generate_plots,
|
|
143
|
-
name="generate_plots",
|
|
144
|
-
terminate_with_success=True,
|
|
145
|
-
catalog=Catalog(put=["iris_logistic.png"]), # (2)
|
|
146
|
-
returns=[metric("score")],
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
pipeline = Pipeline(
|
|
150
|
-
steps=[load_data_task, model_fit_task, generate_plots_task],
|
|
151
|
-
) # (4)
|
|
152
|
-
|
|
153
|
-
pipeline.execute()
|
|
154
|
-
|
|
155
|
-
return pipeline
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if __name__ == "__main__":
|
|
159
|
-
# main()
|
|
160
|
-
runnable_pipeline()
|
|
161
|
-
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
1. Return two serialized objects X and Y.
|
|
166
|
-
2. Store the file `iris_logistic.png` for future reference.
|
|
167
|
-
3. Define the sequence of tasks.
|
|
168
|
-
4. Define a pipeline with the tasks
|
|
169
|
-
|
|
170
|
-
The difference between native driver and runnable orchestration:
|
|
171
|
-
|
|
172
|
-
!!! tip inline end "Notebooks and Shell scripts"
|
|
173
|
-
|
|
174
|
-
You can execute notebooks and shell scripts too!!
|
|
175
|
-
|
|
176
|
-
They can be written just as you would want them, *plain old notebooks and scripts*.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
<div class="annotate" markdown>
|
|
182
|
-
|
|
183
|
-
```diff
|
|
184
|
-
|
|
185
|
-
- X, Y = load_data()
|
|
186
|
-
+load_data_task = PythonTask(
|
|
187
|
-
+ function=load_data,
|
|
188
|
-
+ name="load_data",
|
|
189
|
-
+ returns=[pickled("X"), pickled("Y")], (1)
|
|
190
|
-
+ )
|
|
191
|
-
|
|
192
|
-
-logreg = model_fit(X, Y, C=1.0)
|
|
193
|
-
+model_fit_task = PythonTask(
|
|
194
|
-
+ function=model_fit,
|
|
195
|
-
+ name="model_fit",
|
|
196
|
-
+ returns=[pickled("logreg")],
|
|
197
|
-
+ )
|
|
198
|
-
|
|
199
|
-
-generate_plots(X, Y, logreg)
|
|
200
|
-
+generate_plots_task = PythonTask(
|
|
201
|
-
+ function=generate_plots,
|
|
202
|
-
+ name="generate_plots",
|
|
203
|
-
+ terminate_with_success=True,
|
|
204
|
-
+ catalog=Catalog(put=["iris_logistic.png"]), (2)
|
|
205
|
-
+ )
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
+pipeline = Pipeline(
|
|
209
|
-
+ steps=[load_data_task, model_fit_task, generate_plots_task], (3)
|
|
210
|
-
|
|
211
|
-
```
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
---
|
|
216
|
-
|
|
217
|
-
- [x] ```Domain``` code remains completely independent of ```driver``` code.
|
|
218
|
-
- [x] The ```driver``` function has an equivalent and intuitive runnable expression
|
|
219
|
-
- [x] Reproducible by default, runnable stores metadata about code/data/config for every execution.
|
|
220
|
-
- [x] The pipeline is `runnable` in any environment.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
## Documentation
|
|
224
|
-
|
|
225
|
-
[More details about the project and how to use it available here](https://astrazeneca.github.io/runnable/).
|
|
226
|
-
|
|
227
|
-
<hr style="border:2px dotted orange">
|
|
228
|
-
|
|
229
|
-
## Installation
|
|
230
|
-
|
|
231
|
-
The minimum python version that runnable supports is 3.8
|
|
232
|
-
|
|
233
|
-
```shell
|
|
234
|
-
pip install runnable
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
Please look at the [installation guide](https://astrazeneca.github.io/runnable-core/usage)
|
|
238
|
-
for more information.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
## Pipelines can be:
|
|
242
|
-
|
|
243
|
-
### Linear
|
|
244
|
-
|
|
245
|
-
A simple linear pipeline with tasks either
|
|
246
|
-
[python functions](https://astrazeneca.github.io/runnable-core/concepts/task/#python_functions),
|
|
247
|
-
[notebooks](https://astrazeneca.github.io/runnable-core/concepts/task/#notebooks), or [shell scripts](https://astrazeneca.github.io/runnable-core/concepts/task/#shell)
|
|
248
|
-
|
|
249
|
-
[](https://mermaid.live/edit#pako:eNpl0bFuwyAQBuBXQVdZTqTESpxMDJ0ytkszhgwnOCcoNo4OaFVZfvcSx20tGSQ4fn0wHB3o1hBIyLJOWGeDFJ3Iq7r90lfkkA9HHfmTUpnX1hFyLvrHzDLl_qB4-1BOOZGGD3TfSikvTDSNFqdj2sT2vBTr9euQlXNWjqycsN2c7UZWFMUE7udwP0L3y6JenNKiyfvz8t8_b-gavT9QJYY0PcDtjeTLptrAChriBq1JzeoeWkG4UkMKZCoN8k2Bcn1yGEN7_HYaZOBIK4h3g4EOFi-MDcgKa59SMja0_P7s_vAJ_Q_YOH6o)
|
|
250
|
-
|
|
251
|
-
### [Parallel branches](https://astrazeneca.github.io/runnable-core/concepts/parallel)
|
|
252
|
-
|
|
253
|
-
Execute branches in parallel
|
|
254
|
-
|
|
255
|
-
[](https://mermaid.live/edit#pako:eNp9k01rwzAMhv-K8S4ZtJCzDzuMLmWwwkh2KMQ7eImShiZ2sB1KKf3vs52PpsWNT7LySHqlyBeciRwwwUUtTtmBSY2-YsopR8MpQUfAdCdBBekWNBpvv6-EkFICzGAtWcUTDW3wYy20M7lr5QGBK2j-anBAkH4M1z6grnjpy17xAiTwDII07jj6HK8-VnVZBspITnpjztyoVkLLJOy3Qfrdm6gQEu2370Io7WLORo84PbRoA_oOl9BBg4UHbHR58UkMWq_fxjrOnhLRx1nH0SgkjlBjh7ekxNKGc0NelDLknhePI8qf7MVNr_31nm1wwNTeM2Ao6pmf-3y3Mp7WlqA7twOnXfKs17zt-6azmim1gQL1A0NKS3EE8hKZE4Yezm3chIVFiFe4AdmwKjdv7mIjKNYHaIBiYsycySPFlF8NxzotkjPPMNGygxXu2pxp2FSslKzBpGC1Ml7IKy3krn_E7i1f_wEayTcn)
|
|
256
|
-
|
|
257
|
-
### [loops or map](https://astrazeneca.github.io/runnable-core/concepts/map)
|
|
258
|
-
|
|
259
|
-
Execute a pipeline over an iterable parameter.
|
|
260
|
-
|
|
261
|
-
[](https://mermaid.live/edit#pako:eNqVlF1rwjAUhv9KyG4qKNR-3AS2m8nuBgN3Z0Sy5tQG20SSdE7E_76kVVEr2CY3Ied9Tx6Sk3PAmeKACc5LtcsKpi36nlGZFbXciHwfLN79CuWiBLMcEULWGkBSaeosA2OCxbxdXMd89Get2bZASsLiSyuvQE2mJZXIjW27t2rOmQZ3Gp9rD6UjatWnwy7q6zPPukd50WTydmemEiS_QbQ79RwxGoQY9UaMuojRA8TCXexzyHgQZNwbMu5Cxl3IXNX6OWMyiDHpzZh0GZMHjOK3xz2mgxjT3oxplzG9MPp5_nVOhwJjteDwOg3HyFj3L1dCcvh7DUc-iftX18n6Waet1xX8cG908vpKHO6OW7cvkeHm5GR2b3drdvaSGTODHLW37mxabYC8fLgRhlfxpjNdwmEets-Dx7gCXTHBXQc8-D2KbQEVUEzckjO9oZjKo9Ox2qr5XmaYWF3DGNdbzizMBHOVVWGSs9K4XeDCKv3ZttSmsx7_AYa341E)
|
|
262
|
-
|
|
263
|
-
### [Arbitrary nesting](https://astrazeneca.github.io/runnable-core/concepts/nesting/)
|
|
264
|
-
Any nesting of parallel within map and so on.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|