runnable 0.37.0__py3-none-any.whl → 0.38.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.
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
6
  from typing import Any, Dict, Type
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,109 @@ 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()
100
+ missing_required_args: list[str] = []
101
+ var_keyword_param = None
102
+ namespace_param = None
84
103
 
85
- return bound_args
86
-
87
- # Lets return what is asked for then!!
104
+ # First pass: Handle regular parameters and identify special parameters
88
105
  for name, value in function_args.items():
89
106
  # Ignore any *args
90
107
  if value.kind == inspect.Parameter.VAR_POSITIONAL:
91
108
  logger.warning(f"Ignoring parameter {name} as it is VAR_POSITIONAL")
92
109
  continue
93
110
 
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.
111
+ # Check for **kwargs parameter
112
+ if value.kind == inspect.Parameter.VAR_KEYWORD:
113
+ var_keyword_param = name
102
114
  continue
103
115
 
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
116
+ # Check for argparse.Namespace parameter
117
+ if value.annotation == argparse.Namespace:
118
+ namespace_param = name
119
+ continue
120
120
 
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())
121
+ # Handle regular parameters
122
+ if name not in params:
123
+ if value.default != inspect.Parameter.empty:
124
+ # Default value is given in the function signature, we can use it
125
+ bound_args[name] = value.default
126
+ else:
127
+ # This is a required parameter that's missing
128
+ missing_required_args.append(name)
124
129
  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
130
+ # We have a parameter of this name, lets bind it
131
+ param_value = params[name]
132
+
133
+ if (
134
+ inspect.isclass(value.annotation)
135
+ and issubclass(value.annotation, BaseModel)
136
+ ) and not isinstance(param_value, ObjectParameter):
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 in [str, int, float, bool] and callable(
151
+ value.annotation
152
+ ):
153
+ # Cast it if its a primitive type. Ensure the type matches the annotation.
154
+ try:
155
+ bound_args[name] = value.annotation(params[name].get_value())
156
+ except (ValueError, TypeError) as e:
157
+ raise ValueError(
158
+ f"Cannot cast parameter '{name}' to {value.annotation.__name__}: {e}"
159
+ )
160
+ else:
161
+ # We do not know type of parameter, we send the value as found
162
+ bound_args[name] = params[name].get_value()
163
+
164
+ # Find extra parameters (parameters in params but not consumed by regular function parameters)
165
+ consumed_param_names = set(bound_args.keys()) | set(missing_required_args)
166
+ extra_params = {k: v for k, v in params.items() if k not in consumed_param_names}
167
+
168
+ # Second pass: Handle **kwargs and argparse.Namespace parameters
169
+ if var_keyword_param is not None:
170
+ # Function accepts **kwargs - add all extra parameters directly to bound_args
171
+ for param_name, param_value in extra_params.items():
172
+ bound_args[param_name] = param_value.get_value()
173
+ elif namespace_param is not None:
174
+ # Function accepts argparse.Namespace - create namespace with extra parameters
175
+ args_namespace = argparse.Namespace()
176
+ for param_name, param_value in extra_params.items():
177
+ setattr(args_namespace, param_name, param_value.get_value())
178
+ bound_args[namespace_param] = args_namespace
179
+ elif extra_params:
180
+ # Function doesn't accept **kwargs or namespace, but we have extra parameters
181
+ # This should only be an error if we also have missing required parameters
182
+ # or if the function truly can't handle the extra parameters
183
+ if missing_required_args:
184
+ # We have both missing required and extra parameters - this is an error
185
+ raise ValueError(
186
+ f"Function {func.__name__} has parameters {missing_required_args} that are not present in the parameters"
187
+ )
188
+ # If we only have extra parameters and no missing required ones, we just ignore the extras
189
+ # This allows for more flexible parameter passing
190
+
191
+ # Check for missing required parameters
192
+ if missing_required_args:
193
+ if var_keyword_param is None and namespace_param is None:
194
+ # No way to handle missing parameters
195
+ raise ValueError(
196
+ f"Function {func.__name__} has parameters {missing_required_args} that are not present in the parameters"
197
+ )
198
+ # If we have **kwargs or namespace, missing parameters might be handled there
199
+ # But if they're truly required (no default), we should still error
200
+ raise ValueError(
201
+ f"Function {func.__name__} has parameters {missing_required_args} that are not present in the parameters"
202
+ )
132
203
 
133
204
  return bound_args
134
205
 
@@ -1,12 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runnable
3
- Version: 0.37.0
3
+ Version: 0.38.0
4
4
  Summary: Add your description here
5
5
  Author-email: "Vammi, Vijay" <vijay.vammi@astrazeneca.com>
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.10
8
- Requires-Dist: click-plugins>=1.1.1
9
- Requires-Dist: click<=8.1.3
10
8
  Requires-Dist: cloudpathlib>=0.20.0
11
9
  Requires-Dist: dill>=0.3.9
12
10
  Requires-Dist: pydantic>=2.10.3
@@ -15,7 +13,7 @@ Requires-Dist: rich>=13.9.4
15
13
  Requires-Dist: ruamel-yaml>=0.18.6
16
14
  Requires-Dist: setuptools>=75.6.0
17
15
  Requires-Dist: stevedore>=5.4.0
18
- Requires-Dist: typer>=0.15.1
16
+ Requires-Dist: typer>=0.17.3
19
17
  Provides-Extra: docker
20
18
  Requires-Dist: docker>=7.1.0; extra == 'docker'
21
19
  Provides-Extra: examples
@@ -56,17 +56,17 @@ 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/graph.py,sha256=ukJo_sqtBRD_ZX7ULbd2GnvpToAwGHcAowXPcqKjC4Q,16543
60
60
  runnable/names.py,sha256=A9ldUyULXuWjJ1MoXihHqlg-xeTVX-oWYTO5Ah0trmo,8128
61
61
  runnable/nodes.py,sha256=JHBxJib7SSQXY51bLHBXUvb0DlNSLNvyqz3JNEDLt8c,16926
62
- runnable/parameters.py,sha256=zEehAliVvCOLOnNZ4ExJvSDJM_2PWY0URZ0bmZUgCQA,5289
62
+ runnable/parameters.py,sha256=HZW0bhAYxgMyvRZzUlwp29MVxmzoFU5ZoVJMJHnOcX8,8734
63
63
  runnable/pickler.py,sha256=ydJ_eti_U1F4l-YacFp7BWm6g5vTn04UXye25S1HVok,2684
64
64
  runnable/sdk.py,sha256=blLBWzXV2x7jxKQXWpjmeJ9k22jt5CKBQBqQpnt4agk,32587
65
65
  runnable/secrets.py,sha256=4L_dBFxTgr8r_hHUD6RlZEtqaOHDRsFG5PXO5wlvMI0,2324
66
66
  runnable/tasks.py,sha256=7yuoeG4ZqfxFUmN4mPS4i6kbQmzEpAwbPQweAUWY-ic,31366
67
67
  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,,
68
+ runnable-0.38.0.dist-info/METADATA,sha256=HbM0-0hNd9E3aI5BFrzlQiEdkxSdxkfLaTwhvAIkmd0,9983
69
+ runnable-0.38.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
70
+ runnable-0.38.0.dist-info/entry_points.txt,sha256=KkxihZ0LLEiwvFl7RquyqZ0tp2fJDIs7DgzHYDlmc3U,2018
71
+ runnable-0.38.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
72
+ runnable-0.38.0.dist-info/RECORD,,