axl-workflows 0.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.
- axl/__init__.py +47 -0
- axl/cli.py +322 -0
- axl/compiler/__init__.py +11 -0
- axl/core/__init__.py +10 -0
- axl/core/decorators.py +158 -0
- axl/core/workflow.py +120 -0
- axl/io/__init__.py +27 -0
- axl/io/handlers/__init__.py +9 -0
- axl/io/handlers/cloudpickle.py +69 -0
- axl/io/handlers/pickle.py +53 -0
- axl/io/protocol.py +51 -0
- axl/io/registry.py +64 -0
- axl/io/storage.py +212 -0
- axl/ir/__init__.py +29 -0
- axl/ir/builder.py +198 -0
- axl/ir/nodes.py +144 -0
- axl/ir/outputs.py +117 -0
- axl/ir/workflow.py +184 -0
- axl/logging.py +131 -0
- axl/runtime/__init__.py +10 -0
- axl/runtime/local.py +353 -0
- axl_workflows-0.1.0.dist-info/METADATA +288 -0
- axl_workflows-0.1.0.dist-info/RECORD +26 -0
- axl_workflows-0.1.0.dist-info/WHEEL +4 -0
- axl_workflows-0.1.0.dist-info/entry_points.txt +3 -0
- axl_workflows-0.1.0.dist-info/licenses/LICENSE +201 -0
axl/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AXL Workflows - Lightweight framework for building data and ML workflows.
|
|
3
|
+
|
|
4
|
+
Write once → run anywhere (Dagster locally or Kubeflow in production).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
__author__ = "Pedro Spinosa"
|
|
9
|
+
|
|
10
|
+
# Core imports
|
|
11
|
+
from .core import Workflow, step, workflow
|
|
12
|
+
|
|
13
|
+
# IO imports
|
|
14
|
+
from .io import (
|
|
15
|
+
CloudpickleIOHandler,
|
|
16
|
+
InMemoryStorage,
|
|
17
|
+
IOHandler,
|
|
18
|
+
LocalFileStorage,
|
|
19
|
+
PickleIOHandler,
|
|
20
|
+
StorageBackend,
|
|
21
|
+
create_storage_from_path,
|
|
22
|
+
registry,
|
|
23
|
+
storage_registry,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Logging imports
|
|
27
|
+
from .logging import WorkflowLogger
|
|
28
|
+
|
|
29
|
+
# Runtime imports
|
|
30
|
+
from .runtime import LocalRuntime
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"workflow",
|
|
34
|
+
"step",
|
|
35
|
+
"Workflow",
|
|
36
|
+
"IOHandler",
|
|
37
|
+
"PickleIOHandler",
|
|
38
|
+
"CloudpickleIOHandler",
|
|
39
|
+
"registry",
|
|
40
|
+
"StorageBackend",
|
|
41
|
+
"LocalFileStorage",
|
|
42
|
+
"InMemoryStorage",
|
|
43
|
+
"storage_registry",
|
|
44
|
+
"create_storage_from_path",
|
|
45
|
+
"LocalRuntime",
|
|
46
|
+
"WorkflowLogger",
|
|
47
|
+
]
|
axl/cli.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for AXL Workflows.
|
|
3
|
+
|
|
4
|
+
This module provides the command-line interface for the axl tool.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
import yaml
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
|
|
18
|
+
from . import __version__
|
|
19
|
+
from .ir import build_ir, get_workflow_info
|
|
20
|
+
from .runtime import LocalRuntime
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
name="axl",
|
|
24
|
+
help="AXL Workflows - Lightweight framework for building data and ML workflows",
|
|
25
|
+
add_completion=False,
|
|
26
|
+
)
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.callback()
|
|
31
|
+
def main(
|
|
32
|
+
version: bool = typer.Option(
|
|
33
|
+
None,
|
|
34
|
+
"--version",
|
|
35
|
+
"-v",
|
|
36
|
+
help="Show version and exit",
|
|
37
|
+
callback=lambda v: typer.echo(f"axl {__version__}") if v else None,
|
|
38
|
+
is_eager=True,
|
|
39
|
+
),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""
|
|
42
|
+
AXL Workflows - Write once → run anywhere (Dagster locally or Kubeflow in production).
|
|
43
|
+
"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command()
|
|
48
|
+
def version() -> None:
|
|
49
|
+
"""Show version information."""
|
|
50
|
+
console.print(Panel(f"AXL Workflows v{__version__}", title="Version"))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_parameters(params_file: str | None) -> dict[str, Any]:
|
|
54
|
+
"""
|
|
55
|
+
Load parameters from YAML file.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
params_file: Path to YAML parameters file
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary of parameters
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
typer.Exit: If file not found or invalid YAML
|
|
65
|
+
"""
|
|
66
|
+
if not params_file:
|
|
67
|
+
return {}
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
params_path = Path(params_file)
|
|
71
|
+
if not params_path.exists():
|
|
72
|
+
raise FileNotFoundError(f"Parameters file not found: {params_path}")
|
|
73
|
+
|
|
74
|
+
with open(params_path) as f:
|
|
75
|
+
params = yaml.safe_load(f)
|
|
76
|
+
|
|
77
|
+
if not isinstance(params, dict):
|
|
78
|
+
raise ValueError("Parameters file must contain a YAML dictionary")
|
|
79
|
+
|
|
80
|
+
console.print(
|
|
81
|
+
f"[green]Loaded {len(params)} parameters from {params_file}[/green]"
|
|
82
|
+
)
|
|
83
|
+
return params
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
console.print(f"[red]Error loading parameters: {e}[/red]")
|
|
87
|
+
raise typer.Exit(1) from None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def load_workflow_class(module_path: str) -> type:
|
|
91
|
+
"""
|
|
92
|
+
Load workflow class from module path.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
module_path: Module path in format 'module:Class' or 'path/to/file.py:Class'
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Workflow class
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
typer.Exit: If module or class not found
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
if ":" not in module_path:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"Module path must be in format 'module:Class' or 'path/to/file.py:Class'"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
module_spec, class_name = module_path.split(":", 1)
|
|
110
|
+
|
|
111
|
+
# Handle file paths
|
|
112
|
+
if module_spec.endswith(".py"):
|
|
113
|
+
file_path = Path(module_spec)
|
|
114
|
+
if not file_path.exists():
|
|
115
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
116
|
+
|
|
117
|
+
# Add parent directory to sys.path
|
|
118
|
+
sys.path.insert(0, str(file_path.parent))
|
|
119
|
+
module_spec = file_path.stem
|
|
120
|
+
|
|
121
|
+
# Import module
|
|
122
|
+
module = importlib.import_module(module_spec)
|
|
123
|
+
|
|
124
|
+
# Get class
|
|
125
|
+
if not hasattr(module, class_name):
|
|
126
|
+
raise AttributeError(
|
|
127
|
+
f"Class '{class_name}' not found in module '{module_spec}'"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
workflow_class = getattr(module, class_name)
|
|
131
|
+
|
|
132
|
+
# Validate it's a workflow class
|
|
133
|
+
if not hasattr(workflow_class, "_is_workflow"):
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"Class '{class_name}' is not a workflow class (missing @workflow decorator)"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return workflow_class # type: ignore[no-any-return]
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
console.print(f"[red]Error loading workflow: {e}[/red]")
|
|
142
|
+
raise typer.Exit(1) from None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.command()
|
|
146
|
+
def validate(
|
|
147
|
+
module: str = typer.Argument(
|
|
148
|
+
...,
|
|
149
|
+
help="Module path to workflow class (e.g., 'myflow:MyWorkflow')",
|
|
150
|
+
),
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Validate a workflow definition."""
|
|
153
|
+
try:
|
|
154
|
+
console.print(f"[yellow]Validating workflow: {module}[/yellow]")
|
|
155
|
+
|
|
156
|
+
# Load workflow class
|
|
157
|
+
workflow_class = load_workflow_class(module)
|
|
158
|
+
|
|
159
|
+
# Get basic info
|
|
160
|
+
info = get_workflow_info(workflow_class)
|
|
161
|
+
|
|
162
|
+
# Display workflow info
|
|
163
|
+
table = Table(title="Workflow Information")
|
|
164
|
+
table.add_column("Property", style="cyan")
|
|
165
|
+
table.add_column("Value", style="green")
|
|
166
|
+
|
|
167
|
+
table.add_row("Name", info["name"])
|
|
168
|
+
table.add_row("Image", info["image"])
|
|
169
|
+
table.add_row("IO Handler", info["io_handler"])
|
|
170
|
+
table.add_row("Steps", str(info["step_count"]))
|
|
171
|
+
|
|
172
|
+
console.print(table)
|
|
173
|
+
|
|
174
|
+
# Validate workflow
|
|
175
|
+
console.print("\n[yellow]Building IR and validating...[/yellow]")
|
|
176
|
+
ir_workflow = build_ir(workflow_class)
|
|
177
|
+
|
|
178
|
+
# Display validation results
|
|
179
|
+
console.print("[green]✅ Workflow is valid![/green]")
|
|
180
|
+
|
|
181
|
+
# Show step details
|
|
182
|
+
step_table = Table(title="Steps")
|
|
183
|
+
step_table.add_column("Step", style="cyan")
|
|
184
|
+
step_table.add_column("Inputs", style="blue")
|
|
185
|
+
step_table.add_column("IO Handler", style="green")
|
|
186
|
+
|
|
187
|
+
for node in ir_workflow.nodes:
|
|
188
|
+
inputs = ", ".join(node.inputs) if node.inputs else "None"
|
|
189
|
+
io_handler = node.get_io_handler() or "default"
|
|
190
|
+
step_table.add_row(node.name, inputs, io_handler)
|
|
191
|
+
|
|
192
|
+
console.print(step_table)
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
console.print(f"[red]❌ Validation failed: {e}[/red]")
|
|
196
|
+
raise typer.Exit(1) from None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.command()
|
|
200
|
+
def compile(
|
|
201
|
+
module: str = typer.Argument(
|
|
202
|
+
...,
|
|
203
|
+
help="Module path to workflow class (e.g., 'myflow:MyWorkflow')",
|
|
204
|
+
),
|
|
205
|
+
target: str = typer.Option(
|
|
206
|
+
"argo",
|
|
207
|
+
"--target",
|
|
208
|
+
"-t",
|
|
209
|
+
help="Target backend (argo, dagster)",
|
|
210
|
+
),
|
|
211
|
+
out: str = typer.Option(
|
|
212
|
+
None,
|
|
213
|
+
"--out",
|
|
214
|
+
"-o",
|
|
215
|
+
help="Output file path",
|
|
216
|
+
),
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Compile a workflow to target backend."""
|
|
219
|
+
# TODO: Implement workflow compilation
|
|
220
|
+
console.print(f"[yellow]Compiling {module} to {target}[/yellow]")
|
|
221
|
+
if out:
|
|
222
|
+
console.print(f"[yellow]Output: {out}[/yellow]")
|
|
223
|
+
console.print("[red]Not implemented yet[/red]")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@app.command()
|
|
227
|
+
def run(
|
|
228
|
+
backend: str = typer.Argument(
|
|
229
|
+
"local",
|
|
230
|
+
help="Backend to run on (local, argo, dagster)",
|
|
231
|
+
),
|
|
232
|
+
module: str = typer.Option(
|
|
233
|
+
None,
|
|
234
|
+
"--module",
|
|
235
|
+
"-m",
|
|
236
|
+
help="Module path to workflow class (e.g., 'examples/iris_example.py:IrisKNN')",
|
|
237
|
+
),
|
|
238
|
+
storage_backend: str = typer.Option(
|
|
239
|
+
"memory",
|
|
240
|
+
"--storage",
|
|
241
|
+
help="Storage backend for artifacts (memory|local)",
|
|
242
|
+
),
|
|
243
|
+
workspace: str = typer.Option(
|
|
244
|
+
"axl_workspace",
|
|
245
|
+
"--workspace",
|
|
246
|
+
help="Workspace directory for local storage backend",
|
|
247
|
+
),
|
|
248
|
+
params: str = typer.Option(
|
|
249
|
+
None,
|
|
250
|
+
"--params",
|
|
251
|
+
"-p",
|
|
252
|
+
help="Parameters file (YAML) to pass to workflow constructor",
|
|
253
|
+
),
|
|
254
|
+
) -> None:
|
|
255
|
+
"""Run a workflow on the specified backend.
|
|
256
|
+
|
|
257
|
+
Currently implemented: local backend using LocalRuntime.
|
|
258
|
+
"""
|
|
259
|
+
try:
|
|
260
|
+
console.print(f"[yellow]Running workflow on {backend}[/yellow]")
|
|
261
|
+
|
|
262
|
+
if backend != "local":
|
|
263
|
+
console.print("[red]Only 'local' backend is implemented for now[/red]")
|
|
264
|
+
raise typer.Exit(1)
|
|
265
|
+
|
|
266
|
+
if not module:
|
|
267
|
+
console.print("[red]--module is required for local execution[/red]")
|
|
268
|
+
raise typer.Exit(1)
|
|
269
|
+
|
|
270
|
+
# Load parameters if provided
|
|
271
|
+
workflow_params = load_parameters(params)
|
|
272
|
+
|
|
273
|
+
# Load workflow class
|
|
274
|
+
workflow_class = load_workflow_class(module)
|
|
275
|
+
|
|
276
|
+
# Build IR
|
|
277
|
+
ir_workflow = build_ir(workflow_class)
|
|
278
|
+
|
|
279
|
+
# Instantiate workflow with parameters
|
|
280
|
+
wf_instance = workflow_class(**workflow_params)
|
|
281
|
+
|
|
282
|
+
# Execute locally
|
|
283
|
+
console.print(
|
|
284
|
+
f"[yellow]Executing locally (storage={storage_backend}, workspace='{workspace}')[/yellow]"
|
|
285
|
+
)
|
|
286
|
+
runtime = LocalRuntime(
|
|
287
|
+
workspace_path=workspace, storage_backend=storage_backend
|
|
288
|
+
)
|
|
289
|
+
runtime.execute_workflow(ir_workflow, wf_instance)
|
|
290
|
+
|
|
291
|
+
console.print(Panel("Local run completed", title="Run"))
|
|
292
|
+
|
|
293
|
+
except typer.Exit:
|
|
294
|
+
raise
|
|
295
|
+
except Exception as e:
|
|
296
|
+
console.print(f"[red]Run failed: {e}[/red]")
|
|
297
|
+
raise typer.Exit(1) from None
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@app.command()
|
|
301
|
+
def render(
|
|
302
|
+
module: str = typer.Argument(
|
|
303
|
+
...,
|
|
304
|
+
help="Module path to workflow class",
|
|
305
|
+
),
|
|
306
|
+
out: str = typer.Option(
|
|
307
|
+
None,
|
|
308
|
+
"--out",
|
|
309
|
+
"-o",
|
|
310
|
+
help="Output file path (e.g., dag.png)",
|
|
311
|
+
),
|
|
312
|
+
) -> None:
|
|
313
|
+
"""Render workflow DAG as a graph."""
|
|
314
|
+
# TODO: Implement DAG rendering
|
|
315
|
+
console.print(f"[yellow]Rendering DAG for {module}[/yellow]")
|
|
316
|
+
if out:
|
|
317
|
+
console.print(f"[yellow]Output: {out}[/yellow]")
|
|
318
|
+
console.print("[red]Not implemented yet[/red]")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
app()
|
axl/compiler/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compilers for AXL Workflows.
|
|
3
|
+
|
|
4
|
+
This module contains backend-specific compilers that convert IR to target formats.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# TODO: Implement compiler modules
|
|
8
|
+
# from .argo import ArgoCompiler
|
|
9
|
+
# from .dagster import DagsterCompiler
|
|
10
|
+
|
|
11
|
+
__all__: list[str] = []
|
axl/core/__init__.py
ADDED
axl/core/decorators.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decorators for AXL Workflows.
|
|
3
|
+
|
|
4
|
+
This module contains the @workflow and @step decorators for defining workflows.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from functools import wraps
|
|
11
|
+
from typing import Any, Protocol, TypedDict, TypeVar, cast, runtime_checkable
|
|
12
|
+
|
|
13
|
+
from ..ir import validate_step_args
|
|
14
|
+
from .workflow import Workflow
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T", bound=type[Workflow])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StepConfig(TypedDict, total=False):
|
|
20
|
+
"""Typed configuration attached to step functions at decoration time."""
|
|
21
|
+
|
|
22
|
+
io_handler: str
|
|
23
|
+
input_mode: str | dict[str, str]
|
|
24
|
+
resources: dict[str, Any]
|
|
25
|
+
retries: int
|
|
26
|
+
env: dict[str, str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@runtime_checkable
|
|
30
|
+
class StepFunction(Protocol):
|
|
31
|
+
"""Protocol for functions decorated with @step."""
|
|
32
|
+
|
|
33
|
+
_is_step: bool
|
|
34
|
+
_step_config: StepConfig
|
|
35
|
+
_original_func: Callable[..., Any]
|
|
36
|
+
|
|
37
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def workflow(
|
|
41
|
+
*,
|
|
42
|
+
name: str | None = None,
|
|
43
|
+
image: str | None = None,
|
|
44
|
+
io_handler: str | None = None,
|
|
45
|
+
) -> Callable[[T], T]:
|
|
46
|
+
"""
|
|
47
|
+
Decorator for defining workflows with configuration overrides.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
name: Workflow name (optional, defaults to class name)
|
|
51
|
+
image: Docker image override
|
|
52
|
+
io_handler: IO handler override
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Decorated workflow class
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
@workflow(name="my-workflow", image="custom:latest")
|
|
59
|
+
class MyWorkflow(Workflow):
|
|
60
|
+
def graph(self):
|
|
61
|
+
return self.step1()
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def decorator(cls: T) -> T:
|
|
65
|
+
# Validate that the class inherits from Workflow
|
|
66
|
+
if not issubclass(cls, Workflow):
|
|
67
|
+
raise TypeError(f"Class {cls.__name__} must inherit from Workflow")
|
|
68
|
+
|
|
69
|
+
# Store original __init__ method
|
|
70
|
+
original_init = cls.__init__
|
|
71
|
+
|
|
72
|
+
def new_init(self: Workflow, *args: Any, **kwargs: Any) -> None:
|
|
73
|
+
# Apply decorator overrides to kwargs
|
|
74
|
+
if image is not None:
|
|
75
|
+
kwargs["image"] = image
|
|
76
|
+
if io_handler is not None:
|
|
77
|
+
kwargs["io_handler"] = io_handler
|
|
78
|
+
|
|
79
|
+
# Call original __init__ with updated kwargs
|
|
80
|
+
original_init(self, *args, **kwargs)
|
|
81
|
+
|
|
82
|
+
# Set workflow name (prefer decorator override)
|
|
83
|
+
self.name = name or cls.__name__
|
|
84
|
+
|
|
85
|
+
# Replace the __init__ method
|
|
86
|
+
cls.__init__ = new_init # type: ignore[method-assign]
|
|
87
|
+
|
|
88
|
+
# Add workflow metadata on the class (dynamic attrs by design)
|
|
89
|
+
cls._is_workflow = True # type: ignore[attr-defined]
|
|
90
|
+
cls._workflow_name = name or cls.__name__ # type: ignore[attr-defined]
|
|
91
|
+
|
|
92
|
+
return cls
|
|
93
|
+
|
|
94
|
+
return decorator
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def step(
|
|
98
|
+
*,
|
|
99
|
+
io_handler: str | None = None,
|
|
100
|
+
input_mode: str | dict[str, str] | None = None,
|
|
101
|
+
resources: dict[str, Any] | None = None,
|
|
102
|
+
retries: int | None = None,
|
|
103
|
+
env: dict[str, str] | None = None,
|
|
104
|
+
) -> Callable[[Callable[..., Any]], StepFunction]:
|
|
105
|
+
"""
|
|
106
|
+
Decorator for defining workflow steps with configuration.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
io_handler: IO handler override for this step
|
|
110
|
+
input_mode: Input mode override ("object", "path", or {arg: mode})
|
|
111
|
+
resources: Resource requirements (CPU, memory, etc.)
|
|
112
|
+
retries: Number of retries for this step
|
|
113
|
+
env: Environment variables for this step
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Decorated step function
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
@step(io_handler="parquet", retries=3)
|
|
120
|
+
def preprocess(self, data):
|
|
121
|
+
return processed_data
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def decorator(func: Callable[..., Any]) -> StepFunction:
|
|
125
|
+
# Build step configuration (only include provided keys)
|
|
126
|
+
step_cfg: StepConfig = {}
|
|
127
|
+
if io_handler is not None:
|
|
128
|
+
step_cfg["io_handler"] = io_handler
|
|
129
|
+
if input_mode is not None:
|
|
130
|
+
step_cfg["input_mode"] = input_mode
|
|
131
|
+
if resources is not None:
|
|
132
|
+
step_cfg["resources"] = resources
|
|
133
|
+
if retries is not None:
|
|
134
|
+
step_cfg["retries"] = retries
|
|
135
|
+
if env is not None:
|
|
136
|
+
step_cfg["env"] = env
|
|
137
|
+
|
|
138
|
+
# Attach attributes to the original function (ruff prefers direct assigns)
|
|
139
|
+
cast(Any, func)._is_step = True
|
|
140
|
+
cast(Any, func)._step_config = step_cfg
|
|
141
|
+
|
|
142
|
+
# Create a wrapper that validates arguments
|
|
143
|
+
@wraps(func)
|
|
144
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
145
|
+
# Validate that all arguments are OutputRef instances
|
|
146
|
+
validate_step_args(func.__name__, args, kwargs)
|
|
147
|
+
# Call the original function
|
|
148
|
+
return func(*args, **kwargs)
|
|
149
|
+
|
|
150
|
+
# Copy attributes to wrapper so runtime can read them
|
|
151
|
+
cast(Any, wrapper)._is_step = True
|
|
152
|
+
cast(Any, wrapper)._step_config = step_cfg
|
|
153
|
+
cast(Any, wrapper)._original_func = func
|
|
154
|
+
|
|
155
|
+
# Explicit cast so mypy understands wrapper carries the attributes
|
|
156
|
+
return cast(StepFunction, wrapper)
|
|
157
|
+
|
|
158
|
+
return decorator
|
axl/core/workflow.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Workflow base class for AXL Workflows.
|
|
3
|
+
|
|
4
|
+
This module contains the core Workflow base class with configuration
|
|
5
|
+
and validation capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ..ir import OutputRef
|
|
11
|
+
from ..logging import WorkflowLogger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Workflow:
|
|
15
|
+
"""Base class for workflows with defaults and configuration."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
image: str = "ghcr.io/axl-workflows/runner:latest",
|
|
20
|
+
io_handler: str = "pickle",
|
|
21
|
+
**params: Any,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Initialize workflow with configuration and parameters.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
image: Docker image for workflow execution
|
|
28
|
+
io_handler: IO handler for step outputs (pickle, parquet, numpy, torch)
|
|
29
|
+
**params: Additional parameters passed to the workflow
|
|
30
|
+
"""
|
|
31
|
+
self.image = image
|
|
32
|
+
self.io_handler = io_handler
|
|
33
|
+
self.name: str = "" # Will be set by @workflow decorator
|
|
34
|
+
|
|
35
|
+
# Store parameters
|
|
36
|
+
self.workflow_params = params
|
|
37
|
+
|
|
38
|
+
# Initialize logger for this workflow instance
|
|
39
|
+
self.log = WorkflowLogger(self.name)
|
|
40
|
+
|
|
41
|
+
# Validate configuration
|
|
42
|
+
self._validate_config()
|
|
43
|
+
|
|
44
|
+
def _validate_config(self) -> None:
|
|
45
|
+
"""Validate workflow configuration."""
|
|
46
|
+
if self.io_handler not in ["pickle", "parquet", "numpy", "torch"]:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"io_handler must be one of ['pickle', 'parquet', 'numpy', 'torch'], "
|
|
49
|
+
f"got {self.io_handler}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def configure(self, **kwargs: Any) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Configure workflow settings after initialization.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
**kwargs: Configuration options to update
|
|
58
|
+
"""
|
|
59
|
+
for key, value in kwargs.items():
|
|
60
|
+
if hasattr(self, key):
|
|
61
|
+
setattr(self, key, value)
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError(f"Unknown configuration option: {key}")
|
|
64
|
+
|
|
65
|
+
# Re-validate after configuration changes
|
|
66
|
+
self._validate_config()
|
|
67
|
+
|
|
68
|
+
def get_workflow_config(self) -> dict[str, Any]:
|
|
69
|
+
"""
|
|
70
|
+
Get workflow configuration for IR building.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary with workflow configuration
|
|
74
|
+
"""
|
|
75
|
+
return {
|
|
76
|
+
"image": self.image,
|
|
77
|
+
"io_handler": self.io_handler,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def graph(self) -> Any:
|
|
81
|
+
"""
|
|
82
|
+
Define the workflow graph. Must be implemented by subclasses.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Workflow output (typically the final step result as OutputRef)
|
|
86
|
+
"""
|
|
87
|
+
raise NotImplementedError("graph method must be implemented")
|
|
88
|
+
|
|
89
|
+
def __getattribute__(self, name: str) -> Any:
|
|
90
|
+
"""
|
|
91
|
+
Intercept step method calls to return OutputRef instances.
|
|
92
|
+
|
|
93
|
+
Step method calls always return OutputRef instances for IR building.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
name: Method name to look up
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
OutputRef if it's a step method, otherwise normal attribute
|
|
100
|
+
"""
|
|
101
|
+
# Get the attribute normally first
|
|
102
|
+
attr = object.__getattribute__(self, name)
|
|
103
|
+
|
|
104
|
+
# Check if this is a step method
|
|
105
|
+
if hasattr(attr, "_is_step") and attr._is_step:
|
|
106
|
+
# Create OutputRef with step configuration
|
|
107
|
+
output_ref = OutputRef(name)
|
|
108
|
+
if hasattr(attr, "_step_config"):
|
|
109
|
+
output_ref.metadata["step_config"] = attr._step_config
|
|
110
|
+
return output_ref
|
|
111
|
+
|
|
112
|
+
return attr
|
|
113
|
+
|
|
114
|
+
def __repr__(self) -> str:
|
|
115
|
+
"""String representation of the workflow."""
|
|
116
|
+
return (
|
|
117
|
+
f"{self.__class__.__name__}("
|
|
118
|
+
f"image='{self.image}', "
|
|
119
|
+
f"io_handler='{self.io_handler}')"
|
|
120
|
+
)
|
axl/io/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""IO handlers for AXL Workflows."""
|
|
2
|
+
|
|
3
|
+
from .handlers import CloudpickleIOHandler, PickleIOHandler
|
|
4
|
+
from .protocol import IOHandler
|
|
5
|
+
from .registry import IOHandlerRegistry, registry
|
|
6
|
+
from .storage import (
|
|
7
|
+
InMemoryStorage,
|
|
8
|
+
LocalFileStorage,
|
|
9
|
+
StorageBackend,
|
|
10
|
+
StorageRegistry,
|
|
11
|
+
create_storage_from_path,
|
|
12
|
+
storage_registry,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"IOHandler",
|
|
17
|
+
"PickleIOHandler",
|
|
18
|
+
"CloudpickleIOHandler",
|
|
19
|
+
"IOHandlerRegistry",
|
|
20
|
+
"registry",
|
|
21
|
+
"StorageBackend",
|
|
22
|
+
"LocalFileStorage",
|
|
23
|
+
"InMemoryStorage",
|
|
24
|
+
"StorageRegistry",
|
|
25
|
+
"storage_registry",
|
|
26
|
+
"create_storage_from_path",
|
|
27
|
+
]
|