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/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 command line or by up stream steps.
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
- unassigned_params = set(params.keys())
76
- # Check if VAR_KEYWORD is used, it is we send back everything
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
- # Lets return what is asked for then!!
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
- if name not in params:
95
- # No parameter of this name was provided
96
- if value.default == inspect.Parameter.empty:
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
- param_value = params[name]
105
-
106
- if type(value.annotation) in [
107
- BaseModel,
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
- elif value.annotation in [str, int, float, bool]:
122
- # Cast it if its a primitive type. Ensure the type matches the annotation.
123
- bound_args[name] = value.annotation(params[name].get_value())
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
- bound_args[name] = params[name].get_value()
126
-
127
- unassigned_params.remove(name)
128
-
129
- params = {
130
- key: params[key] for key in unassigned_params
131
- } # remove keys from params if they are assigned
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=CziCKBoL-dklSEo_x-YO1AysrG2eaf2LMQZbcNTeCbM,7283
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/graph.py,sha256=poQz5zcvq89ju_u5sYlunQLPbHnXTaUmjcvstPwvT4U,16536
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=zEehAliVvCOLOnNZ4ExJvSDJM_2PWY0URZ0bmZUgCQA,5289
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.37.0.dist-info/METADATA,sha256=nD9ezWOwkWw3Yi5_NBE_xqjRGKxqf_FnSsYb8P9oqxo,10047
69
- runnable-0.37.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
70
- runnable-0.37.0.dist-info/entry_points.txt,sha256=KkxihZ0LLEiwvFl7RquyqZ0tp2fJDIs7DgzHYDlmc3U,2018
71
- runnable-0.37.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
72
- runnable-0.37.0.dist-info/RECORD,,
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.ink/img/pako:eNpl0bFuwyAQBuBXQVdZTqTESpxMDJ0ytkszhgwnOCcoNo4OaFVZfvcSx20tGSQ4fn0wHB3o1hBIyLJOWGeDFJ3Iq7r90lfkkA9HHfmTUpnX1hFyLvrHzDLl_qB4-1BOOZGGD3TfSikvTDSNFqdj2sT2vBTr9euQlXNWjqycsN2c7UZWFMUE7udwP0L3y6JenNKiyfvz8t8_b-gavT9QJYY0PcDtjeTLptrAChriBq1JzeoeWkG4UkMKZCoN8k2Bcn1yGEN7_HYaZOBIK4h3g4EOFi-MDcgKa59SMja0_P7s_vAJ_Q_YOH6o?type=png)](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.ink/img/pako:eNp9k01rwzAMhv-K8S4ZtJCzDzuMLmWwwkh2KMQ7eImShiZ2sB1KKf3vs52PpsWNT7LySHqlyBeciRwwwUUtTtmBSY2-YsopR8MpQUfAdCdBBekWNBpvv6-EkFICzGAtWcUTDW3wYy20M7lr5QGBK2j-anBAkH4M1z6grnjpy17xAiTwDII07jj6HK8-VnVZBspITnpjztyoVkLLJOy3Qfrdm6gQEu2370Io7WLORo84PbRoA_oOl9BBg4UHbHR58UkMWq_fxjrOnhLRx1nH0SgkjlBjh7ekxNKGc0NelDLknhePI8qf7MVNr_31nm1wwNTeM2Ao6pmf-3y3Mp7WlqA7twOnXfKs17zt-6azmim1gQL1A0NKS3EE8hKZE4Yezm3chIVFiFe4AdmwKjdv7mIjKNYHaIBiYsycySPFlF8NxzotkjPPMNGygxXu2pxp2FSslKzBpGC1Ml7IKy3krn_E7i1f_wEayTcn?type=png)](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.ink/img/pako:eNqVlF1rwjAUhv9KyG4qKNR-3AS2m8nuBgN3Z0Sy5tQG20SSdE7E_76kVVEr2CY3Ied9Tx6Sk3PAmeKACc5LtcsKpi36nlGZFbXciHwfLN79CuWiBLMcEULWGkBSaeosA2OCxbxdXMd89Get2bZASsLiSyuvQE2mJZXIjW27t2rOmQZ3Gp9rD6UjatWnwy7q6zPPukd50WTydmemEiS_QbQ79RwxGoQY9UaMuojRA8TCXexzyHgQZNwbMu5Cxl3IXNX6OWMyiDHpzZh0GZMHjOK3xz2mgxjT3oxplzG9MPp5_nVOhwJjteDwOg3HyFj3L1dCcvh7DUc-iftX18n6Waet1xX8cG908vpKHO6OW7cvkeHm5GR2b3drdvaSGTODHLW37mxabYC8fLgRhlfxpjNdwmEets-Dx7gCXTHBXQc8-D2KbQEVUEzckjO9oZjKo9Ox2qr5XmaYWF3DGNdbzizMBHOVVWGSs9K4XeDCKv3ZttSmsx7_AYa341E?type=png)](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.