flowyml 1.1.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.
- flowyml/__init__.py +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Conditional execution for dynamic pipelines."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from functools import wraps
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Condition:
|
|
9
|
+
"""Condition for conditional step execution.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
```python
|
|
13
|
+
from flowyml import step, Condition
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@step
|
|
17
|
+
def check_quality(data):
|
|
18
|
+
return {"score": 0.95, "needs_cleaning": False}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@step
|
|
22
|
+
@Condition(lambda result: result["needs_cleaning"])
|
|
23
|
+
def clean_data(data, quality_result):
|
|
24
|
+
return cleaned_data
|
|
25
|
+
```
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, condition_func: Callable[[Any], bool]):
|
|
29
|
+
"""Initialize condition.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
condition_func: Function that takes step inputs and returns bool
|
|
33
|
+
"""
|
|
34
|
+
self.condition_func = condition_func
|
|
35
|
+
|
|
36
|
+
def should_execute(self, *args, **kwargs) -> bool:
|
|
37
|
+
"""Check if condition is met.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
*args: Positional arguments
|
|
41
|
+
**kwargs: Keyword arguments
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
True if step should execute
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
# Try to evaluate with first argument if available
|
|
48
|
+
if args:
|
|
49
|
+
return bool(self.condition_func(args[0]))
|
|
50
|
+
elif kwargs:
|
|
51
|
+
# Try with all kwargs
|
|
52
|
+
return bool(self.condition_func(kwargs))
|
|
53
|
+
return False
|
|
54
|
+
except Exception:
|
|
55
|
+
# If condition evaluation fails, don't execute
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def __call__(self, func: Callable) -> Callable:
|
|
59
|
+
"""Decorator to add conditional execution.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
func: Function to wrap
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Wrapped function
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
@wraps(func)
|
|
69
|
+
def wrapper(*args, **kwargs):
|
|
70
|
+
if self.should_execute(*args, **kwargs):
|
|
71
|
+
return func(*args, **kwargs)
|
|
72
|
+
else:
|
|
73
|
+
# Return None or a special marker for skipped execution
|
|
74
|
+
return SkippedExecution(func.__name__)
|
|
75
|
+
|
|
76
|
+
# Mark function as conditional
|
|
77
|
+
wrapper._is_conditional = True
|
|
78
|
+
wrapper._condition = self
|
|
79
|
+
|
|
80
|
+
return wrapper
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SkippedExecution:
|
|
84
|
+
"""Marker for skipped conditional execution."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, step_name: str):
|
|
87
|
+
self.step_name = step_name
|
|
88
|
+
self.skipped = True
|
|
89
|
+
|
|
90
|
+
def __repr__(self):
|
|
91
|
+
return f"<SkippedExecution: {self.step_name}>"
|
|
92
|
+
|
|
93
|
+
def __bool__(self):
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def condition(condition_func: Callable[[Any], bool]) -> Condition:
|
|
98
|
+
"""Create a condition decorator.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
condition_func: Function that evaluates to bool
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Condition decorator
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
```python
|
|
108
|
+
@step
|
|
109
|
+
@condition(lambda x: x["quality"] > 0.9)
|
|
110
|
+
def train_complex_model(data):
|
|
111
|
+
return complex_model
|
|
112
|
+
```
|
|
113
|
+
"""
|
|
114
|
+
return Condition(condition_func)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ConditionalBranch:
|
|
118
|
+
"""Branch execution based on condition.
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
```python
|
|
122
|
+
from flowyml import ConditionalBranch
|
|
123
|
+
|
|
124
|
+
branch = ConditionalBranch()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@branch.if_condition(lambda x: x > 10)
|
|
128
|
+
def process_large(data):
|
|
129
|
+
return process_with_large_model(data)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@branch.else_condition()
|
|
133
|
+
def process_small(data):
|
|
134
|
+
return process_with_small_model(data)
|
|
135
|
+
```
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self):
|
|
139
|
+
self.if_func: Callable | None = None
|
|
140
|
+
self.if_condition_func: Callable | None = None
|
|
141
|
+
self.else_func: Callable | None = None
|
|
142
|
+
self.elif_branches: list[tuple[Callable, Callable]] = []
|
|
143
|
+
|
|
144
|
+
def if_condition(self, condition_func: Callable[[Any], bool]) -> Callable:
|
|
145
|
+
"""Define if branch.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
condition_func: Condition function
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Decorator
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def decorator(func: Callable) -> Callable:
|
|
155
|
+
self.if_func = func
|
|
156
|
+
self.if_condition_func = condition_func
|
|
157
|
+
return func
|
|
158
|
+
|
|
159
|
+
return decorator
|
|
160
|
+
|
|
161
|
+
def elif_condition(self, condition_func: Callable[[Any], bool]) -> Callable:
|
|
162
|
+
"""Define elif branch.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
condition_func: Condition function
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Decorator
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def decorator(func: Callable) -> Callable:
|
|
172
|
+
self.elif_branches.append((condition_func, func))
|
|
173
|
+
return func
|
|
174
|
+
|
|
175
|
+
return decorator
|
|
176
|
+
|
|
177
|
+
def else_condition(self) -> Callable:
|
|
178
|
+
"""Define else branch.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Decorator
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def decorator(func: Callable) -> Callable:
|
|
185
|
+
self.else_func = func
|
|
186
|
+
return func
|
|
187
|
+
|
|
188
|
+
return decorator
|
|
189
|
+
|
|
190
|
+
def execute(self, *args, **kwargs) -> Any:
|
|
191
|
+
"""Execute appropriate branch based on conditions.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
*args: Positional arguments
|
|
195
|
+
**kwargs: Keyword arguments
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Result from executed branch
|
|
199
|
+
"""
|
|
200
|
+
# Try if condition
|
|
201
|
+
if self.if_func and self.if_condition_func:
|
|
202
|
+
try:
|
|
203
|
+
if self.if_condition_func(*args, **kwargs):
|
|
204
|
+
return self.if_func(*args, **kwargs)
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
# Try elif conditions
|
|
209
|
+
for condition_func, func in self.elif_branches:
|
|
210
|
+
try:
|
|
211
|
+
if condition_func(*args, **kwargs):
|
|
212
|
+
return func(*args, **kwargs)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
# Execute else
|
|
217
|
+
if self.else_func:
|
|
218
|
+
return self.else_func(*args, **kwargs)
|
|
219
|
+
|
|
220
|
+
# No branch executed
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class Switch:
|
|
225
|
+
"""Switch-case style conditional execution.
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
```python
|
|
229
|
+
from flowyml import Switch
|
|
230
|
+
|
|
231
|
+
switch = Switch(lambda x: x["type"])
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@switch.case("image")
|
|
235
|
+
def process_image(data):
|
|
236
|
+
return process_image_data(data)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@switch.case("text")
|
|
240
|
+
def process_text(data):
|
|
241
|
+
return process_text_data(data)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@switch.default()
|
|
245
|
+
def process_other(data):
|
|
246
|
+
return process_generic(data)
|
|
247
|
+
```
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
def __init__(self, selector_func: Callable[[Any], Any]):
|
|
251
|
+
"""Initialize switch.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
selector_func: Function to select case value
|
|
255
|
+
"""
|
|
256
|
+
self.selector_func = selector_func
|
|
257
|
+
self.cases: dict[Any, Callable] = {}
|
|
258
|
+
self.default_func: Callable | None = None
|
|
259
|
+
|
|
260
|
+
def case(self, value: Any) -> Callable:
|
|
261
|
+
"""Define a case branch.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
value: Value to match
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Decorator
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def decorator(func: Callable) -> Callable:
|
|
271
|
+
self.cases[value] = func
|
|
272
|
+
return func
|
|
273
|
+
|
|
274
|
+
return decorator
|
|
275
|
+
|
|
276
|
+
def default(self) -> Callable:
|
|
277
|
+
"""Define default branch.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Decorator
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
def decorator(func: Callable) -> Callable:
|
|
284
|
+
self.default_func = func
|
|
285
|
+
return func
|
|
286
|
+
|
|
287
|
+
return decorator
|
|
288
|
+
|
|
289
|
+
def execute(self, *args, **kwargs) -> Any:
|
|
290
|
+
"""Execute appropriate case based on selector.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
*args: Positional arguments
|
|
294
|
+
**kwargs: Keyword arguments
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Result from executed case
|
|
298
|
+
"""
|
|
299
|
+
# Get selector value
|
|
300
|
+
try:
|
|
301
|
+
selector_value = self.selector_func(*args, **kwargs)
|
|
302
|
+
except Exception:
|
|
303
|
+
selector_value = None
|
|
304
|
+
|
|
305
|
+
# Find matching case
|
|
306
|
+
if selector_value in self.cases:
|
|
307
|
+
return self.cases[selector_value](*args, **kwargs)
|
|
308
|
+
|
|
309
|
+
# Execute default
|
|
310
|
+
if self.default_func:
|
|
311
|
+
return self.default_func(*args, **kwargs)
|
|
312
|
+
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def when(condition_func: Callable[[Any], bool]) -> Callable:
|
|
317
|
+
"""Simple conditional execution helper.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
condition_func: Condition to evaluate
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Decorator that executes function only if condition is true
|
|
324
|
+
|
|
325
|
+
Example:
|
|
326
|
+
```python
|
|
327
|
+
@step
|
|
328
|
+
@when(lambda data: len(data) > 1000)
|
|
329
|
+
def process_large_dataset(data):
|
|
330
|
+
return expensive_processing(data)
|
|
331
|
+
```
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
def decorator(func: Callable) -> Callable:
|
|
335
|
+
@wraps(func)
|
|
336
|
+
def wrapper(*args, **kwargs):
|
|
337
|
+
if condition_func(*args, **kwargs):
|
|
338
|
+
return func(*args, **kwargs)
|
|
339
|
+
return SkippedExecution(func.__name__)
|
|
340
|
+
|
|
341
|
+
return wrapper
|
|
342
|
+
|
|
343
|
+
return decorator
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def unless(condition_func: Callable[[Any], bool]) -> Callable:
|
|
347
|
+
"""Execute unless condition is true.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
condition_func: Condition to evaluate
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Decorator that executes function only if condition is false
|
|
354
|
+
|
|
355
|
+
Example:
|
|
356
|
+
```python
|
|
357
|
+
@step
|
|
358
|
+
@unless(lambda data: data["cached"])
|
|
359
|
+
def compute_features(data):
|
|
360
|
+
return expensive_computation(data)
|
|
361
|
+
```
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
def decorator(func: Callable) -> Callable:
|
|
365
|
+
@wraps(func)
|
|
366
|
+
def wrapper(*args, **kwargs):
|
|
367
|
+
if not condition_func(*args, **kwargs):
|
|
368
|
+
return func(*args, **kwargs)
|
|
369
|
+
return SkippedExecution(func.__name__)
|
|
370
|
+
|
|
371
|
+
return wrapper
|
|
372
|
+
|
|
373
|
+
return decorator
|
flowyml/core/context.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Context Management - Automatic parameter injection for ML pipelines."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Context:
|
|
8
|
+
"""Pipeline context with automatic parameter injection.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> ctx = Context(learning_rate=0.001, epochs=10, batch_size=32, device="cuda")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, **kwargs):
|
|
15
|
+
self._params = kwargs
|
|
16
|
+
self._parent = None
|
|
17
|
+
self._metadata = {}
|
|
18
|
+
|
|
19
|
+
def __getattr__(self, name: str) -> Any:
|
|
20
|
+
"""Allow dot notation access to parameters."""
|
|
21
|
+
if name.startswith("_"):
|
|
22
|
+
return super().__getattribute__(name)
|
|
23
|
+
|
|
24
|
+
if name in self._params:
|
|
25
|
+
return self._params[name]
|
|
26
|
+
|
|
27
|
+
if self._parent and name in self._parent._params:
|
|
28
|
+
return self._parent._params[name]
|
|
29
|
+
|
|
30
|
+
raise AttributeError(f"Context has no parameter '{name}'")
|
|
31
|
+
|
|
32
|
+
def __getitem__(self, key: str) -> Any:
|
|
33
|
+
"""Allow dict-style access to parameters."""
|
|
34
|
+
if key in self._params:
|
|
35
|
+
return self._params[key]
|
|
36
|
+
|
|
37
|
+
if self._parent and key in self._parent._params:
|
|
38
|
+
return self._parent._params[key]
|
|
39
|
+
|
|
40
|
+
raise KeyError(f"Context has no parameter '{key}'")
|
|
41
|
+
|
|
42
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
43
|
+
"""Get parameter with default value."""
|
|
44
|
+
try:
|
|
45
|
+
return self[key]
|
|
46
|
+
except KeyError:
|
|
47
|
+
return default
|
|
48
|
+
|
|
49
|
+
def keys(self) -> set[str]:
|
|
50
|
+
"""Return all parameter keys."""
|
|
51
|
+
keys = set(self._params.keys())
|
|
52
|
+
if self._parent:
|
|
53
|
+
keys.update(self._parent.keys())
|
|
54
|
+
return keys
|
|
55
|
+
|
|
56
|
+
def items(self) -> list[tuple]:
|
|
57
|
+
"""Return all parameter items."""
|
|
58
|
+
result = {}
|
|
59
|
+
if self._parent:
|
|
60
|
+
result.update(dict(self._parent.items()))
|
|
61
|
+
result.update(self._params)
|
|
62
|
+
return list(result.items())
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> dict[str, Any]:
|
|
65
|
+
"""Convert context to dictionary."""
|
|
66
|
+
result = {}
|
|
67
|
+
if self._parent:
|
|
68
|
+
result.update(self._parent.to_dict())
|
|
69
|
+
result.update(self._params)
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
def update(self, data: dict[str, Any]) -> None:
|
|
73
|
+
"""Update context with new data.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
data: Dictionary of key-value pairs to add to context
|
|
77
|
+
"""
|
|
78
|
+
self._params.update(data)
|
|
79
|
+
|
|
80
|
+
def inherit(self, **overrides) -> "Context":
|
|
81
|
+
"""Create child context with inheritance."""
|
|
82
|
+
child = Context(**overrides)
|
|
83
|
+
child._parent = self
|
|
84
|
+
return child
|
|
85
|
+
|
|
86
|
+
def inject_params(self, func: callable) -> dict[str, Any]:
|
|
87
|
+
"""Automatically inject parameters based on function signature.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
func: Function to analyze and inject parameters for
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Dictionary of parameters to inject
|
|
94
|
+
"""
|
|
95
|
+
sig = inspect.signature(func)
|
|
96
|
+
injected = {}
|
|
97
|
+
|
|
98
|
+
for param_name, param in sig.parameters.items():
|
|
99
|
+
# Skip self, cls, args, kwargs
|
|
100
|
+
if param_name in ("self", "cls"):
|
|
101
|
+
continue
|
|
102
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# Check if parameter exists in context
|
|
106
|
+
if param_name in self.keys():
|
|
107
|
+
injected[param_name] = self[param_name]
|
|
108
|
+
|
|
109
|
+
return injected
|
|
110
|
+
|
|
111
|
+
def validate_for_step(self, step_func: callable, exclude: list[str] = None) -> list[str]:
|
|
112
|
+
"""Validate that all required parameters are available.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
step_func: Step function to validate
|
|
116
|
+
exclude: List of parameter names to exclude from validation (e.g. inputs)
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List of missing required parameters
|
|
120
|
+
"""
|
|
121
|
+
sig = inspect.signature(step_func)
|
|
122
|
+
missing = []
|
|
123
|
+
exclude = exclude or []
|
|
124
|
+
|
|
125
|
+
for param_name, param in sig.parameters.items():
|
|
126
|
+
# Skip optional parameters
|
|
127
|
+
if param_name in ("self", "cls"):
|
|
128
|
+
continue
|
|
129
|
+
if param_name in exclude:
|
|
130
|
+
continue
|
|
131
|
+
if param.default != inspect.Parameter.empty:
|
|
132
|
+
continue
|
|
133
|
+
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# Check if required param is missing
|
|
137
|
+
if param_name not in self.keys():
|
|
138
|
+
missing.append(param_name)
|
|
139
|
+
|
|
140
|
+
return missing
|
|
141
|
+
|
|
142
|
+
def __repr__(self) -> str:
|
|
143
|
+
params_str = ", ".join(f"{k}={v}" for k, v in list(self._params.items())[:5])
|
|
144
|
+
if len(self._params) > 5:
|
|
145
|
+
params_str += "..."
|
|
146
|
+
return f"Context({params_str})"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def context(**kwargs) -> Context:
|
|
150
|
+
"""Create a new context with parameters.
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> ctx = context(learning_rate=0.001, epochs=10, batch_size=32)
|
|
154
|
+
"""
|
|
155
|
+
return Context(**kwargs)
|