taskflow-tr 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.
- task_flow/__init__.py +13 -0
- task_flow/api.py +121 -0
- task_flow/compiler/__init__.py +4 -0
- task_flow/compiler/builder.py +61 -0
- task_flow/compiler/compiler.py +161 -0
- task_flow/ir/__init__.py +3 -0
- task_flow/ir/graph.py +68 -0
- task_flow/printer/__init__.py +3 -0
- task_flow/printer/printer.py +112 -0
- task_flow/runtime/__init__.py +3 -0
- task_flow/runtime/executor.py +140 -0
- taskflow_tr-0.1.0.dist-info/METADATA +345 -0
- taskflow_tr-0.1.0.dist-info/RECORD +16 -0
- taskflow_tr-0.1.0.dist-info/WHEEL +5 -0
- taskflow_tr-0.1.0.dist-info/licenses/LICENSE +21 -0
- taskflow_tr-0.1.0.dist-info/top_level.txt +1 -0
task_flow/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .api import CompiledFunction, TransformedFunction, compile, transform
|
|
2
|
+
from .runtime import Executor, InlineExecutor, ProcessExecutor, ThreadExecutor
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"CompiledFunction",
|
|
6
|
+
"Executor",
|
|
7
|
+
"InlineExecutor",
|
|
8
|
+
"ProcessExecutor",
|
|
9
|
+
"ThreadExecutor",
|
|
10
|
+
"TransformedFunction",
|
|
11
|
+
"compile",
|
|
12
|
+
"transform",
|
|
13
|
+
]
|
task_flow/api.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from typing import Any, Callable, Optional
|
|
3
|
+
|
|
4
|
+
from task_flow.compiler import Compiler
|
|
5
|
+
from task_flow.ir import GraphIR
|
|
6
|
+
from task_flow.printer import get_printer
|
|
7
|
+
from task_flow.runtime import InlineExecutor, ProcessExecutor, ThreadExecutor
|
|
8
|
+
|
|
9
|
+
_MISSING = object()
|
|
10
|
+
_FORMATS = {"dict", "json", "mermaid", "graphviz"}
|
|
11
|
+
_EXECUTORS = {"inline", "thread", "process"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CompiledFunction:
|
|
15
|
+
def __init__(self, python_function: Callable[..., Any], default_format: str = "json"):
|
|
16
|
+
self.python_function = python_function
|
|
17
|
+
self._graph_ir = Compiler(python_function).compile()
|
|
18
|
+
self.default_format = _validate_format(default_format)
|
|
19
|
+
functools.update_wrapper(self, python_function)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def graph_ir(self) -> GraphIR:
|
|
23
|
+
return self._graph_ir
|
|
24
|
+
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
return self.__format__("")
|
|
27
|
+
|
|
28
|
+
def __format__(self, format_spec: str) -> str:
|
|
29
|
+
format_name = self.default_format if format_spec == "" else _validate_format(format_spec)
|
|
30
|
+
rendered = get_printer(format_name).print(self.graph_ir)
|
|
31
|
+
return repr(rendered) if format_name == "dict" else rendered
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TransformedFunction:
|
|
35
|
+
def __init__(self, compiled: CompiledFunction, executor_name: str, workers: Optional[int]):
|
|
36
|
+
self.compiled = compiled
|
|
37
|
+
self.executor_name = executor_name
|
|
38
|
+
self.workers = workers
|
|
39
|
+
functools.update_wrapper(self, compiled.python_function)
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def graph_ir(self) -> GraphIR:
|
|
43
|
+
return self.compiled.graph_ir
|
|
44
|
+
|
|
45
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
46
|
+
executor = _create_executor(self.executor_name, self.workers)
|
|
47
|
+
with executor:
|
|
48
|
+
return executor.run(self.compiled, args=args, kwargs=kwargs)
|
|
49
|
+
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
return str(self.compiled)
|
|
52
|
+
|
|
53
|
+
def __format__(self, format_spec: str) -> str:
|
|
54
|
+
return self.compiled.__format__(format_spec)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _validate_format(format_name: str) -> str:
|
|
58
|
+
if format_name not in _FORMATS:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"unknown graph format %r; expected one of: dict, json, mermaid, graphviz" % format_name
|
|
61
|
+
)
|
|
62
|
+
return format_name
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compile(function: Any = _MISSING, *, format: Any = _MISSING) -> Any:
|
|
66
|
+
if function is _MISSING:
|
|
67
|
+
if format is _MISSING:
|
|
68
|
+
raise TypeError("compile() requires format when called with parentheses")
|
|
69
|
+
format_name = _validate_format(format)
|
|
70
|
+
|
|
71
|
+
def decorator(target: Callable[..., Any]) -> CompiledFunction:
|
|
72
|
+
return CompiledFunction(target, default_format=format_name)
|
|
73
|
+
|
|
74
|
+
return decorator
|
|
75
|
+
if format is not _MISSING:
|
|
76
|
+
raise TypeError("use compile(format=...) as a decorator")
|
|
77
|
+
if not callable(function):
|
|
78
|
+
raise TypeError("compile expects a Python function")
|
|
79
|
+
return CompiledFunction(function, default_format="json")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def transform(
|
|
83
|
+
function: Any = _MISSING, *, executor: Any = _MISSING, workers: Optional[int] = None
|
|
84
|
+
) -> Any:
|
|
85
|
+
if function is _MISSING:
|
|
86
|
+
if executor is _MISSING:
|
|
87
|
+
raise TypeError("transform() requires executor when called with parentheses")
|
|
88
|
+
executor_name = _validate_executor(executor, workers)
|
|
89
|
+
|
|
90
|
+
def decorator(target: Callable[..., Any]) -> TransformedFunction:
|
|
91
|
+
return TransformedFunction(CompiledFunction(target), executor_name, workers)
|
|
92
|
+
|
|
93
|
+
return decorator
|
|
94
|
+
if executor is not _MISSING:
|
|
95
|
+
raise TypeError("use transform(executor=...) as a decorator")
|
|
96
|
+
if workers is not None:
|
|
97
|
+
raise TypeError("workers requires an explicit executor")
|
|
98
|
+
if not callable(function):
|
|
99
|
+
raise TypeError("transform expects a Python function")
|
|
100
|
+
return TransformedFunction(CompiledFunction(function), "inline", None)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _validate_executor(executor_name: str, workers: Optional[int]) -> str:
|
|
104
|
+
if executor_name not in _EXECUTORS:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"unknown executor %r; expected one of: inline, thread, process" % executor_name
|
|
107
|
+
)
|
|
108
|
+
if executor_name == "inline":
|
|
109
|
+
if workers is not None:
|
|
110
|
+
raise ValueError("inline executor does not accept workers")
|
|
111
|
+
elif not isinstance(workers, int) or isinstance(workers, bool) or workers <= 0:
|
|
112
|
+
raise ValueError("thread and process executors require positive integer workers")
|
|
113
|
+
return executor_name
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _create_executor(executor_name: str, workers: Optional[int]):
|
|
117
|
+
if executor_name == "inline":
|
|
118
|
+
return InlineExecutor()
|
|
119
|
+
if executor_name == "thread":
|
|
120
|
+
return ThreadExecutor(thread_num=workers) # type: ignore[arg-type]
|
|
121
|
+
return ProcessExecutor(process_num=workers) # type: ignore[arg-type]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Any, Callable, Tuple
|
|
4
|
+
|
|
5
|
+
from task_flow.ir import GraphIR, NodeIR
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GraphBuilder:
|
|
9
|
+
def __init__(self, name: str):
|
|
10
|
+
self.name = name
|
|
11
|
+
self._nodes = [] # type: List[NodeIR]
|
|
12
|
+
self._inputs = [] # type: List[Tuple[str, str]]
|
|
13
|
+
self._outputs = () # type: Tuple[str, ...]
|
|
14
|
+
self._output_kind = "none"
|
|
15
|
+
self._counts = defaultdict(int) # type: DefaultDict[str, int]
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _slug(value: str) -> str:
|
|
19
|
+
value = re.sub(r"[^0-9A-Za-z_.]+", "_", value).strip("_")
|
|
20
|
+
return value or "anonymous"
|
|
21
|
+
|
|
22
|
+
def _next_id(self, kind: str, name: str) -> str:
|
|
23
|
+
base = "%s.%s" % (kind, self._slug(name))
|
|
24
|
+
self._counts[base] += 1
|
|
25
|
+
occurrence = self._counts[base]
|
|
26
|
+
return base if occurrence == 1 else "%s.%s" % (base, occurrence)
|
|
27
|
+
|
|
28
|
+
def add_input(self, name: str) -> str:
|
|
29
|
+
node_id = self._next_id("input", name)
|
|
30
|
+
self._nodes.append(NodeIR(node_id, "input", name, None, ()))
|
|
31
|
+
self._inputs.append((name, node_id))
|
|
32
|
+
return node_id
|
|
33
|
+
|
|
34
|
+
def add_constant(self, value: Any) -> str:
|
|
35
|
+
node_id = self._next_id("constant", type(value).__name__)
|
|
36
|
+
self._nodes.append(NodeIR(node_id, "constant", repr(value), None, (), value=value))
|
|
37
|
+
return node_id
|
|
38
|
+
|
|
39
|
+
def add_call(
|
|
40
|
+
self, operation: Callable[..., Any], dependencies: Tuple[str, ...], name: str = ""
|
|
41
|
+
) -> str:
|
|
42
|
+
operation_name = name or getattr(operation, "__qualname__", operation.__class__.__name__)
|
|
43
|
+
module = getattr(operation, "__module__", "")
|
|
44
|
+
display_name = "%s.%s" % (module, operation_name) if module else operation_name
|
|
45
|
+
node_id = self._next_id("call", display_name)
|
|
46
|
+
self._nodes.append(NodeIR(node_id, "call", display_name, operation, dependencies))
|
|
47
|
+
return node_id
|
|
48
|
+
|
|
49
|
+
def set_outputs(self, outputs: Tuple[str, ...], output_kind: str) -> None:
|
|
50
|
+
self._outputs = outputs
|
|
51
|
+
self._output_kind = output_kind
|
|
52
|
+
|
|
53
|
+
def build(self) -> GraphIR:
|
|
54
|
+
return GraphIR(
|
|
55
|
+
"1.0",
|
|
56
|
+
self.name,
|
|
57
|
+
tuple(self._nodes),
|
|
58
|
+
tuple(self._inputs),
|
|
59
|
+
self._outputs,
|
|
60
|
+
self._output_kind,
|
|
61
|
+
)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import inspect
|
|
3
|
+
import operator
|
|
4
|
+
import textwrap
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
from task_flow.ir import GraphIR
|
|
8
|
+
|
|
9
|
+
from .builder import GraphBuilder
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnsupportedSyntaxError(ValueError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_BINARY_OPERATIONS = {
|
|
17
|
+
ast.Add: operator.add,
|
|
18
|
+
ast.Sub: operator.sub,
|
|
19
|
+
ast.Mult: operator.mul,
|
|
20
|
+
ast.Div: operator.truediv,
|
|
21
|
+
ast.FloorDiv: operator.floordiv,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Compiler(ast.NodeVisitor):
|
|
26
|
+
def __init__(self, function: Callable[..., Any]):
|
|
27
|
+
self.function = function
|
|
28
|
+
self.builder = GraphBuilder(function.__name__)
|
|
29
|
+
self.visible = {} # type: Dict[str, str]
|
|
30
|
+
self.environment = dict(function.__globals__)
|
|
31
|
+
closure = inspect.getclosurevars(function)
|
|
32
|
+
self.environment.update(closure.globals)
|
|
33
|
+
self.environment.update(closure.nonlocals)
|
|
34
|
+
self.environment.update(closure.builtins)
|
|
35
|
+
self._returned = False
|
|
36
|
+
|
|
37
|
+
def compile(self) -> GraphIR:
|
|
38
|
+
try:
|
|
39
|
+
source = textwrap.dedent(inspect.getsource(self.function))
|
|
40
|
+
except (OSError, TypeError) as exc:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
"cannot read source for function %s" % self.function.__qualname__
|
|
43
|
+
) from exc
|
|
44
|
+
module = ast.parse(source)
|
|
45
|
+
function_node = next(
|
|
46
|
+
(node for node in module.body if isinstance(node, ast.FunctionDef)), None
|
|
47
|
+
)
|
|
48
|
+
if function_node is None:
|
|
49
|
+
raise UnsupportedSyntaxError("only regular Python functions are supported")
|
|
50
|
+
self.visit(function_node)
|
|
51
|
+
if not self._returned:
|
|
52
|
+
self.builder.set_outputs((), "none")
|
|
53
|
+
return self.builder.build()
|
|
54
|
+
|
|
55
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
56
|
+
if node.args.posonlyargs or node.args.kwonlyargs or node.args.vararg or node.args.kwarg:
|
|
57
|
+
raise UnsupportedSyntaxError(
|
|
58
|
+
"positional-only, keyword-only and variadic parameters are not supported"
|
|
59
|
+
)
|
|
60
|
+
if node.args.defaults or node.args.kw_defaults:
|
|
61
|
+
raise UnsupportedSyntaxError("default parameters are not supported")
|
|
62
|
+
for argument in node.args.args:
|
|
63
|
+
self.visible[argument.arg] = self.builder.add_input(argument.arg)
|
|
64
|
+
for statement in node.body:
|
|
65
|
+
self.visit(statement)
|
|
66
|
+
if self._returned:
|
|
67
|
+
break
|
|
68
|
+
|
|
69
|
+
def generic_visit(self, node: ast.AST) -> None:
|
|
70
|
+
raise UnsupportedSyntaxError("unsupported syntax: %s" % type(node).__name__)
|
|
71
|
+
|
|
72
|
+
def visit_Assign(self, node: ast.Assign) -> None:
|
|
73
|
+
if len(node.targets) != 1:
|
|
74
|
+
raise UnsupportedSyntaxError("chained assignment is not supported")
|
|
75
|
+
target = node.targets[0]
|
|
76
|
+
if isinstance(target, (ast.Tuple, ast.List)):
|
|
77
|
+
if not isinstance(node.value, type(target)):
|
|
78
|
+
raise UnsupportedSyntaxError("unpacking requires a matching literal")
|
|
79
|
+
if len(target.elts) != len(node.value.elts):
|
|
80
|
+
raise UnsupportedSyntaxError("unpacking target size mismatch")
|
|
81
|
+
for target_item, value_item in zip(target.elts, node.value.elts):
|
|
82
|
+
self._assign_name(target_item, self._compile_expression(value_item))
|
|
83
|
+
return
|
|
84
|
+
self._assign_name(target, self._compile_expression(node.value))
|
|
85
|
+
|
|
86
|
+
def _assign_name(self, target: ast.AST, node_id: str) -> None:
|
|
87
|
+
if not isinstance(target, ast.Name):
|
|
88
|
+
raise UnsupportedSyntaxError("only name assignment is supported")
|
|
89
|
+
self.visible[target.id] = node_id
|
|
90
|
+
|
|
91
|
+
def visit_Expr(self, node: ast.Expr) -> None:
|
|
92
|
+
self._compile_expression(node.value)
|
|
93
|
+
|
|
94
|
+
def visit_Return(self, node: ast.Return) -> None:
|
|
95
|
+
if self._returned:
|
|
96
|
+
raise UnsupportedSyntaxError("multiple return statements are not supported")
|
|
97
|
+
self._returned = True
|
|
98
|
+
if node.value is None or (
|
|
99
|
+
isinstance(node.value, ast.Constant) and node.value.value is None
|
|
100
|
+
):
|
|
101
|
+
self.builder.set_outputs((), "none")
|
|
102
|
+
elif isinstance(node.value, ast.Tuple):
|
|
103
|
+
self.builder.set_outputs(
|
|
104
|
+
tuple(self._compile_expression(item) for item in node.value.elts), "tuple"
|
|
105
|
+
)
|
|
106
|
+
elif isinstance(node.value, ast.List):
|
|
107
|
+
self.builder.set_outputs(
|
|
108
|
+
tuple(self._compile_expression(item) for item in node.value.elts), "list"
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
self.builder.set_outputs((self._compile_expression(node.value),), "single")
|
|
112
|
+
|
|
113
|
+
def _compile_expression(self, node: ast.AST) -> str:
|
|
114
|
+
if isinstance(node, ast.Name):
|
|
115
|
+
if node.id not in self.visible:
|
|
116
|
+
raise UnsupportedSyntaxError("unknown local name: %s" % node.id)
|
|
117
|
+
return self.visible[node.id]
|
|
118
|
+
if isinstance(node, ast.Constant):
|
|
119
|
+
return self.builder.add_constant(node.value)
|
|
120
|
+
if isinstance(node, ast.BinOp):
|
|
121
|
+
operation = _BINARY_OPERATIONS.get(type(node.op))
|
|
122
|
+
if operation is None:
|
|
123
|
+
raise UnsupportedSyntaxError(
|
|
124
|
+
"unsupported binary operation: %s" % type(node.op).__name__
|
|
125
|
+
)
|
|
126
|
+
return self.builder.add_call(
|
|
127
|
+
operation,
|
|
128
|
+
(self._compile_expression(node.left), self._compile_expression(node.right)),
|
|
129
|
+
)
|
|
130
|
+
if isinstance(node, ast.Call):
|
|
131
|
+
return self._compile_call(node)
|
|
132
|
+
raise UnsupportedSyntaxError("unsupported expression: %s" % type(node).__name__)
|
|
133
|
+
|
|
134
|
+
def _compile_call(self, node: ast.Call) -> str:
|
|
135
|
+
if node.keywords:
|
|
136
|
+
raise UnsupportedSyntaxError("keyword call arguments are not supported")
|
|
137
|
+
function = self._resolve_callable(node.func)
|
|
138
|
+
return self.builder.add_call(
|
|
139
|
+
function, tuple(self._compile_expression(item) for item in node.args)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _resolve_callable(self, node: ast.AST) -> Callable[..., Any]:
|
|
143
|
+
value = (
|
|
144
|
+
self.environment.get(node.id)
|
|
145
|
+
if isinstance(node, ast.Name)
|
|
146
|
+
else self._resolve_value(node)
|
|
147
|
+
if isinstance(node, ast.Attribute)
|
|
148
|
+
else None
|
|
149
|
+
)
|
|
150
|
+
if not callable(value):
|
|
151
|
+
raise UnsupportedSyntaxError("cannot resolve callable from source")
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
def _resolve_value(self, node: ast.AST) -> Any:
|
|
155
|
+
if isinstance(node, ast.Name):
|
|
156
|
+
if node.id not in self.environment:
|
|
157
|
+
raise UnsupportedSyntaxError("unknown global name: %s" % node.id)
|
|
158
|
+
return self.environment[node.id]
|
|
159
|
+
if isinstance(node, ast.Attribute):
|
|
160
|
+
return getattr(self._resolve_value(node.value), node.attr)
|
|
161
|
+
raise UnsupportedSyntaxError("unsupported callable reference")
|
task_flow/ir/__init__.py
ADDED
task_flow/ir/graph.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Callable, Dict, Iterator, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class NodeIR:
|
|
7
|
+
id: str
|
|
8
|
+
kind: str
|
|
9
|
+
name: str
|
|
10
|
+
operation: Optional[Callable[..., Any]]
|
|
11
|
+
dependencies: Tuple[str, ...]
|
|
12
|
+
value: Any = None
|
|
13
|
+
|
|
14
|
+
def __post_init__(self) -> None:
|
|
15
|
+
if self.kind not in {"input", "constant", "call"}:
|
|
16
|
+
raise ValueError("unknown node kind: %s" % self.kind)
|
|
17
|
+
if self.kind == "call" and self.operation is None:
|
|
18
|
+
raise ValueError("call node %s requires an operation" % self.id)
|
|
19
|
+
if self.kind != "call" and self.operation is not None:
|
|
20
|
+
raise ValueError("%s node %s cannot define an operation" % (self.kind, self.id))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class GraphIR:
|
|
25
|
+
ir_version: str
|
|
26
|
+
name: str
|
|
27
|
+
nodes: Tuple[NodeIR, ...]
|
|
28
|
+
inputs: Tuple[Tuple[str, str], ...]
|
|
29
|
+
outputs: Tuple[str, ...]
|
|
30
|
+
output_kind: str
|
|
31
|
+
|
|
32
|
+
def __post_init__(self) -> None:
|
|
33
|
+
if self.ir_version != "1.0":
|
|
34
|
+
raise ValueError("unsupported GraphIR version: %s" % self.ir_version)
|
|
35
|
+
if self.output_kind not in {"none", "single", "tuple", "list"}:
|
|
36
|
+
raise ValueError("unknown output kind: %s" % self.output_kind)
|
|
37
|
+
node_map = self.node_map
|
|
38
|
+
if len(node_map) != len(self.nodes):
|
|
39
|
+
raise ValueError("GraphIR node IDs must be unique")
|
|
40
|
+
for node in self.nodes:
|
|
41
|
+
for dependency in node.dependencies:
|
|
42
|
+
if dependency not in node_map:
|
|
43
|
+
raise ValueError("node %s depends on missing node %s" % (node.id, dependency))
|
|
44
|
+
input_ids = []
|
|
45
|
+
input_names = []
|
|
46
|
+
for name, node_id in self.inputs:
|
|
47
|
+
if name in input_names:
|
|
48
|
+
raise ValueError("duplicate input name: %s" % name)
|
|
49
|
+
if node_id in input_ids:
|
|
50
|
+
raise ValueError("duplicate input node: %s" % node_id)
|
|
51
|
+
if node_id not in node_map or node_map[node_id].kind != "input":
|
|
52
|
+
raise ValueError("invalid input node: %s" % node_id)
|
|
53
|
+
input_names.append(name)
|
|
54
|
+
input_ids.append(node_id)
|
|
55
|
+
for output in self.outputs:
|
|
56
|
+
if output not in node_map:
|
|
57
|
+
raise ValueError("invalid output node: %s" % output)
|
|
58
|
+
if self.output_kind == "single" and len(self.outputs) != 1:
|
|
59
|
+
raise ValueError("single output requires one node")
|
|
60
|
+
if self.output_kind == "none" and self.outputs:
|
|
61
|
+
raise ValueError("none output cannot contain output nodes")
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def node_map(self) -> Dict[str, NodeIR]:
|
|
65
|
+
return {node.id: node for node in self.nodes}
|
|
66
|
+
|
|
67
|
+
def __iter__(self) -> Iterator[NodeIR]:
|
|
68
|
+
return iter(self.nodes)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from task_flow.ir import GraphIR, NodeIR
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _operation_name(node: NodeIR) -> Any:
|
|
9
|
+
if node.operation is None:
|
|
10
|
+
return None
|
|
11
|
+
module = getattr(node.operation, "__module__", "")
|
|
12
|
+
name = getattr(node.operation, "__qualname__", node.operation.__class__.__name__)
|
|
13
|
+
return "%s.%s" % (module, name) if module else name
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Printer(ABC):
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def print(self, graph: GraphIR) -> Any:
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DictPrinter(Printer):
|
|
23
|
+
def print(self, graph: GraphIR) -> Dict[str, Any]:
|
|
24
|
+
nodes = []
|
|
25
|
+
for node in graph.nodes:
|
|
26
|
+
item = {
|
|
27
|
+
"id": node.id,
|
|
28
|
+
"kind": node.kind,
|
|
29
|
+
"name": node.name,
|
|
30
|
+
"dependencies": list(node.dependencies),
|
|
31
|
+
}
|
|
32
|
+
operation = _operation_name(node)
|
|
33
|
+
if operation is not None:
|
|
34
|
+
item["operation"] = operation
|
|
35
|
+
if node.kind == "constant":
|
|
36
|
+
item["value"] = node.value
|
|
37
|
+
nodes.append(item)
|
|
38
|
+
return {
|
|
39
|
+
"ir_version": graph.ir_version,
|
|
40
|
+
"name": graph.name,
|
|
41
|
+
"inputs": [{"name": name, "node": node_id} for name, node_id in graph.inputs],
|
|
42
|
+
"outputs": list(graph.outputs),
|
|
43
|
+
"output_kind": graph.output_kind,
|
|
44
|
+
"nodes": nodes,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class JsonPrinter(Printer):
|
|
49
|
+
def print(self, graph: GraphIR) -> str:
|
|
50
|
+
return json.dumps(DictPrinter().print(graph), indent=2, ensure_ascii=False, default=repr)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _mermaid_text(value: str) -> str:
|
|
54
|
+
return value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", " ")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class MermaidPrinter(Printer):
|
|
58
|
+
def print(self, graph: GraphIR) -> str:
|
|
59
|
+
lines = ["flowchart TD"]
|
|
60
|
+
for index, node in enumerate(graph.nodes):
|
|
61
|
+
alias = "n%s" % index
|
|
62
|
+
shape = (
|
|
63
|
+
'(["%s"])'
|
|
64
|
+
if node.kind == "input"
|
|
65
|
+
else '{{"%s"}}'
|
|
66
|
+
if node.kind == "constant"
|
|
67
|
+
else '["%s"]'
|
|
68
|
+
)
|
|
69
|
+
lines.append(" %s%s" % (alias, shape % _mermaid_text(node.name)))
|
|
70
|
+
aliases = {node.id: "n%s" % index for index, node in enumerate(graph.nodes)}
|
|
71
|
+
for node in graph.nodes:
|
|
72
|
+
for dependency in node.dependencies:
|
|
73
|
+
lines.append(" %s --> %s" % (aliases[dependency], aliases[node.id]))
|
|
74
|
+
return "\n".join(lines)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _dot_text(value: str) -> str:
|
|
78
|
+
return value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class GraphvizPrinter(Printer):
|
|
82
|
+
def print(self, graph: GraphIR) -> str:
|
|
83
|
+
lines = ['digraph "%s" {' % _dot_text(graph.name)]
|
|
84
|
+
aliases = {node.id: "n%s" % index for index, node in enumerate(graph.nodes)}
|
|
85
|
+
shapes = {"input": "oval", "constant": "diamond", "call": "box"}
|
|
86
|
+
for node in graph.nodes:
|
|
87
|
+
lines.append(
|
|
88
|
+
' %s [label="%s", shape=%s];'
|
|
89
|
+
% (aliases[node.id], _dot_text(node.name), shapes[node.kind])
|
|
90
|
+
)
|
|
91
|
+
for node in graph.nodes:
|
|
92
|
+
for dependency in node.dependencies:
|
|
93
|
+
lines.append(" %s -> %s;" % (aliases[dependency], aliases[node.id]))
|
|
94
|
+
lines.append("}")
|
|
95
|
+
return "\n".join(lines)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_PRINTERS = {
|
|
99
|
+
"dict": DictPrinter,
|
|
100
|
+
"json": JsonPrinter,
|
|
101
|
+
"mermaid": MermaidPrinter,
|
|
102
|
+
"graphviz": GraphvizPrinter,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_printer(format_name: str) -> Printer:
|
|
107
|
+
try:
|
|
108
|
+
return _PRINTERS[format_name]()
|
|
109
|
+
except KeyError as exc:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
"unknown graph format %r; expected one of: %s" % (format_name, ", ".join(_PRINTERS))
|
|
112
|
+
) from exc
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import multiprocessing
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from concurrent.futures import (
|
|
4
|
+
FIRST_COMPLETED,
|
|
5
|
+
Future,
|
|
6
|
+
ProcessPoolExecutor,
|
|
7
|
+
ThreadPoolExecutor,
|
|
8
|
+
wait,
|
|
9
|
+
)
|
|
10
|
+
from typing import Any, Dict, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from task_flow.ir import GraphIR, NodeIR
|
|
13
|
+
|
|
14
|
+
__all__ = ["Executor", "InlineExecutor", "ProcessExecutor", "ThreadExecutor"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Executor(ABC):
|
|
18
|
+
def __enter__(self) -> "Executor":
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
|
22
|
+
self.close()
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def close(self) -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def run(
|
|
29
|
+
self, function: Any, args: Tuple[Any, ...] = (), kwargs: Optional[Dict[str, Any]] = None
|
|
30
|
+
) -> Any:
|
|
31
|
+
if not hasattr(function, "graph_ir") or not hasattr(function, "python_function"):
|
|
32
|
+
raise TypeError("Executor.run expects a CompiledFunction")
|
|
33
|
+
kwargs = {} if kwargs is None else dict(kwargs)
|
|
34
|
+
bound = __import__("inspect").signature(function.python_function).bind(*args, **kwargs)
|
|
35
|
+
graph = function.graph_ir # type: GraphIR
|
|
36
|
+
values = {} # type: Dict[str, Any]
|
|
37
|
+
for name, node_id in graph.inputs:
|
|
38
|
+
values[node_id] = bound.arguments[name]
|
|
39
|
+
for node in graph.nodes:
|
|
40
|
+
if node.kind == "constant":
|
|
41
|
+
values[node.id] = node.value
|
|
42
|
+
|
|
43
|
+
calls = [node for node in graph.nodes if node.kind == "call"]
|
|
44
|
+
waiting = {
|
|
45
|
+
node.id: sum(dependency not in values for dependency in node.dependencies)
|
|
46
|
+
for node in calls
|
|
47
|
+
}
|
|
48
|
+
children = {} # type: Dict[str, list]
|
|
49
|
+
for node in calls:
|
|
50
|
+
for dependency in node.dependencies:
|
|
51
|
+
children.setdefault(dependency, []).append(node.id)
|
|
52
|
+
node_map = graph.node_map
|
|
53
|
+
pending = {} # type: Dict[Future, NodeIR]
|
|
54
|
+
|
|
55
|
+
def submit(node: NodeIR) -> None:
|
|
56
|
+
inputs = tuple(values[dependency] for dependency in node.dependencies)
|
|
57
|
+
pending[self._submit(node, inputs)] = node
|
|
58
|
+
|
|
59
|
+
for node in calls:
|
|
60
|
+
if waiting[node.id] == 0:
|
|
61
|
+
submit(node)
|
|
62
|
+
|
|
63
|
+
completed = 0
|
|
64
|
+
while pending:
|
|
65
|
+
future = self._wait_any(tuple(pending))
|
|
66
|
+
node = pending.pop(future)
|
|
67
|
+
values[node.id] = future.result()
|
|
68
|
+
completed += 1
|
|
69
|
+
for child_id in children.get(node.id, ()):
|
|
70
|
+
waiting[child_id] -= 1
|
|
71
|
+
if waiting[child_id] == 0:
|
|
72
|
+
submit(node_map[child_id])
|
|
73
|
+
|
|
74
|
+
if completed != len(calls):
|
|
75
|
+
blocked = sorted(node.id for node in calls if node.id not in values)
|
|
76
|
+
raise ValueError("GraphIR contains a cycle or unresolved dependencies: %s" % blocked)
|
|
77
|
+
return self._build_result(graph, values)
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _build_result(graph: GraphIR, values: Dict[str, Any]) -> Any:
|
|
81
|
+
results = tuple(values[node_id] for node_id in graph.outputs)
|
|
82
|
+
if graph.output_kind == "none":
|
|
83
|
+
return None
|
|
84
|
+
if graph.output_kind == "single":
|
|
85
|
+
return results[0]
|
|
86
|
+
if graph.output_kind == "list":
|
|
87
|
+
return list(results)
|
|
88
|
+
return results
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def _submit(self, node: NodeIR, inputs: Tuple[Any, ...]) -> Future:
|
|
92
|
+
raise NotImplementedError
|
|
93
|
+
|
|
94
|
+
@abstractmethod
|
|
95
|
+
def _wait_any(self, futures: Tuple[Future, ...]) -> Future:
|
|
96
|
+
raise NotImplementedError
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class InlineExecutor(Executor):
|
|
100
|
+
def _submit(self, node: NodeIR, inputs: Tuple[Any, ...]) -> Future:
|
|
101
|
+
future = Future()
|
|
102
|
+
try:
|
|
103
|
+
future.set_result(node.operation(*inputs)) # type: ignore[misc]
|
|
104
|
+
except BaseException as exc:
|
|
105
|
+
future.set_exception(exc)
|
|
106
|
+
return future
|
|
107
|
+
|
|
108
|
+
def _wait_any(self, futures: Tuple[Future, ...]) -> Future:
|
|
109
|
+
return futures[0]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _PoolExecutor(Executor):
|
|
113
|
+
pool = None
|
|
114
|
+
|
|
115
|
+
def _submit(self, node: NodeIR, inputs: Tuple[Any, ...]) -> Future:
|
|
116
|
+
return self.pool.submit(node.operation, *inputs)
|
|
117
|
+
|
|
118
|
+
def _wait_any(self, futures: Tuple[Future, ...]) -> Future:
|
|
119
|
+
done, _ = wait(futures, return_when=FIRST_COMPLETED)
|
|
120
|
+
return next(iter(done))
|
|
121
|
+
|
|
122
|
+
def close(self) -> None:
|
|
123
|
+
if self.pool is not None:
|
|
124
|
+
self.pool.shutdown(wait=True)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ThreadExecutor(_PoolExecutor):
|
|
128
|
+
def __init__(self, thread_num: int):
|
|
129
|
+
if thread_num <= 0:
|
|
130
|
+
raise ValueError("thread_num must be positive")
|
|
131
|
+
self.pool = ThreadPoolExecutor(max_workers=thread_num)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ProcessExecutor(_PoolExecutor):
|
|
135
|
+
def __init__(self, process_num: int):
|
|
136
|
+
if process_num <= 0:
|
|
137
|
+
raise ValueError("process_num must be positive")
|
|
138
|
+
methods = multiprocessing.get_all_start_methods()
|
|
139
|
+
context = multiprocessing.get_context("fork") if "fork" in methods else None
|
|
140
|
+
self.pool = ProcessPoolExecutor(max_workers=process_num, mp_context=context)
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: taskflow-tr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Compile Python functions into GraphIR DAGs and execute them inline, with threads, or with processes.
|
|
5
|
+
Author-email: gaoxinge <gaoxx5@gmail.com>
|
|
6
|
+
Maintainer-email: gaoxinge <gaoxx5@gmail.com>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 gaoxinge
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
Project-URL: Homepage, https://github.com/gaoxinge/task_flow
|
|
30
|
+
Project-URL: Repository, https://github.com/gaoxinge/task_flow
|
|
31
|
+
Project-URL: Issues, https://github.com/gaoxinge/task_flow/issues
|
|
32
|
+
Project-URL: Documentation, https://github.com/gaoxinge/task_flow#readme
|
|
33
|
+
Keywords: dag,graph-ir,workflow,concurrency,threading,multiprocessing
|
|
34
|
+
Classifier: Development Status :: 3 - Alpha
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
46
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
47
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
48
|
+
Requires-Python: >=3.8
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
License-File: LICENSE
|
|
51
|
+
Provides-Extra: examples
|
|
52
|
+
Requires-Dist: flask>=2; extra == "examples"
|
|
53
|
+
Requires-Dist: grpcio>=1.48; extra == "examples"
|
|
54
|
+
Requires-Dist: grpcio-tools>=1.48; extra == "examples"
|
|
55
|
+
Requires-Dist: protobuf>=3.20; extra == "examples"
|
|
56
|
+
Requires-Dist: requests>=2; extra == "examples"
|
|
57
|
+
Dynamic: license-file
|
|
58
|
+
|
|
59
|
+
# taskflow-tr
|
|
60
|
+
|
|
61
|
+
taskflow-tr 将一小部分 Python 函数编译成静态 `GraphIR`,可打印计算图,也可使用串行、线程池或进程池执行。PyPI 发行名是 `taskflow-tr`,Python 导入名是 `task_flow`。
|
|
62
|
+
|
|
63
|
+
## 环境与安装
|
|
64
|
+
|
|
65
|
+
项目最低支持 Python 3.8。通过 PyPI 安装核心包:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install taskflow-tr
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
如需运行 README 中的 HTTP 和 gRPC 集成示例:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install "taskflow-tr[examples]"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
项目开发环境使用 [uv](https://docs.astral.sh/uv/) 管理:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uv sync --all-groups
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 直接执行函数
|
|
84
|
+
|
|
85
|
+
`@transform` 默认使用 `InlineExecutor`:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from task_flow import transform
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@transform
|
|
92
|
+
def add(a, b):
|
|
93
|
+
return a + b
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
assert add(2, b=1) == 3
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
显式使用线程池或进程池时必须指定工作数量:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
@transform(executor="thread", workers=4)
|
|
103
|
+
def threaded_add(a, b):
|
|
104
|
+
return a + b
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@transform(executor="process", workers=4)
|
|
108
|
+
def process_add(a, b):
|
|
109
|
+
return a + b
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`@transform()` 是非法形式;带括号时必须显式传入 `executor`。
|
|
113
|
+
|
|
114
|
+
## 编译和打印 GraphIR
|
|
115
|
+
|
|
116
|
+
`@compile` 只编译函数,不绑定执行策略。`print()` 默认输出 JSON:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from task_flow import compile
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@compile
|
|
123
|
+
def add(a, b):
|
|
124
|
+
return a + b
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
print(add)
|
|
128
|
+
print(format(add, "dict"))
|
|
129
|
+
print(format(add, "mermaid"))
|
|
130
|
+
print(format(add, "graphviz"))
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
也可以在装饰时改变默认打印格式:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
@compile(format="mermaid")
|
|
137
|
+
def workflow(a, b):
|
|
138
|
+
return a + b
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
print(workflow)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
支持 `dict`、`json`、`mermaid` 和 `graphviz` 四种格式。Graphviz 格式返回 DOT 源码,不要求 Runtime 安装 Graphviz。
|
|
145
|
+
|
|
146
|
+
`@compile()` 是非法形式;带括号时必须显式传入 `format`。
|
|
147
|
+
|
|
148
|
+
## 显式选择 Executor
|
|
149
|
+
|
|
150
|
+
编译结果可以被不同 Executor 重复执行,无需重新编译:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from task_flow import InlineExecutor, ProcessExecutor, ThreadExecutor
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
with InlineExecutor() as executor:
|
|
157
|
+
assert executor.run(add, args=(2,), kwargs={"b": 1}) == 3
|
|
158
|
+
|
|
159
|
+
with ThreadExecutor(thread_num=4) as executor:
|
|
160
|
+
assert executor.run(add, args=(2, 1)) == 3
|
|
161
|
+
|
|
162
|
+
with ProcessExecutor(process_num=4) as executor:
|
|
163
|
+
assert executor.run(add, args=(2, 1)) == 3
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
线程执行适合 I/O 或会释放 GIL 的扩展调用;进程执行适合 CPU 密集型 Python 代码。进程池中的调用函数与参数必须能被 `pickle` 序列化,应优先使用模块顶层命名函数,避免 lambda 和局部函数。
|
|
167
|
+
|
|
168
|
+
## 完整示例
|
|
169
|
+
|
|
170
|
+
### 并行计算与 Graphviz 输出
|
|
171
|
+
|
|
172
|
+
下面的计算图包含四个相互独立的运算节点,线程 Executor 可以并发执行它们:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
import time
|
|
176
|
+
from operator import add, floordiv, mul, sub
|
|
177
|
+
|
|
178
|
+
from task_flow import compile, transform
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def delayed(operation, x, y):
|
|
182
|
+
time.sleep(0.1)
|
|
183
|
+
return operation(x, y)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def add0(x, y):
|
|
187
|
+
return delayed(add, x, y)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def sub0(x, y):
|
|
191
|
+
return delayed(sub, x, y)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def mul0(x, y):
|
|
195
|
+
return delayed(mul, x, y)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def div0(x, y):
|
|
199
|
+
return delayed(floordiv, x, y)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@transform(executor="thread", workers=4)
|
|
203
|
+
def compute_graph(a, b):
|
|
204
|
+
result_add = add0(a, b)
|
|
205
|
+
result_sub = sub0(a, b)
|
|
206
|
+
result_mul = mul0(a, b)
|
|
207
|
+
result_div = div0(a, b)
|
|
208
|
+
return result_add, result_sub, result_mul, result_div
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
assert compute_graph(2, 1) == (3, 1, 2, 2)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@compile(format="graphviz")
|
|
215
|
+
def printable_graph(a, b):
|
|
216
|
+
result_add = a + b
|
|
217
|
+
result_sub = a - b
|
|
218
|
+
result_mul = a * b
|
|
219
|
+
result_div = a // b
|
|
220
|
+
return result_add, result_sub, result_mul, result_div
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
print(printable_graph) # 输出 DOT 源码
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 集成 HTTP 服务
|
|
227
|
+
|
|
228
|
+
HTTP 和 gRPC 只是 `TransformedFunction` 的调用入口,调度逻辑仍由 taskflow-tr 管理。安装示例依赖:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
uv sync --extra examples
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
使用 Flask 暴露计算接口:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from flask import Flask, jsonify, request
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
app = Flask(__name__)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@app.post("/compute")
|
|
244
|
+
def compute():
|
|
245
|
+
inputs = request.get_json()
|
|
246
|
+
x, y, z, w = compute_graph(inputs["x"], inputs["y"])
|
|
247
|
+
return jsonify({"x": x, "y": y, "z": z, "w": w})
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
app.run(host="0.0.0.0", port=8000)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
请求示例:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
import requests
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
response = requests.post(
|
|
261
|
+
"http://127.0.0.1:8000/compute",
|
|
262
|
+
json={"x": 2, "y": 1},
|
|
263
|
+
timeout=30,
|
|
264
|
+
)
|
|
265
|
+
assert response.json() == {"x": 3, "y": 1, "z": 2, "w": 2}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 集成 gRPC 服务
|
|
269
|
+
|
|
270
|
+
先定义 `compute.proto`:
|
|
271
|
+
|
|
272
|
+
```proto
|
|
273
|
+
syntax = "proto3";
|
|
274
|
+
package example;
|
|
275
|
+
|
|
276
|
+
message Inputs {
|
|
277
|
+
int32 x = 1;
|
|
278
|
+
int32 y = 2;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
message Outputs {
|
|
282
|
+
int32 x = 1;
|
|
283
|
+
int32 y = 2;
|
|
284
|
+
int32 z = 3;
|
|
285
|
+
int32 w = 4;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
service App {
|
|
289
|
+
rpc Compute (Inputs) returns (Outputs);
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
生成 Python 代码:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
uv run python -m grpc_tools.protoc \
|
|
297
|
+
-I. \
|
|
298
|
+
--python_out=. \
|
|
299
|
+
--grpc_python_out=. \
|
|
300
|
+
compute.proto
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
在生成的服务接口中调用同一个 `compute_graph`:
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from concurrent import futures
|
|
307
|
+
|
|
308
|
+
import grpc
|
|
309
|
+
import compute_pb2
|
|
310
|
+
import compute_pb2_grpc
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class App(compute_pb2_grpc.AppServicer):
|
|
314
|
+
def Compute(self, inputs, context):
|
|
315
|
+
x, y, z, w = compute_graph(inputs.x, inputs.y)
|
|
316
|
+
return compute_pb2.Outputs(x=x, y=y, z=z, w=w)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
server = grpc.server(futures.ThreadPoolExecutor(max_workers=3))
|
|
320
|
+
compute_pb2_grpc.add_AppServicer_to_server(App(), server)
|
|
321
|
+
server.add_insecure_port("0.0.0.0:8000")
|
|
322
|
+
server.start()
|
|
323
|
+
server.wait_for_termination()
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## V1 编译范围
|
|
327
|
+
|
|
328
|
+
V1 支持普通位置或关键字参数、常量、简单赋值、普通函数调用、`+`、`-`、`*`、`/`、`//`,以及 `None`、单值、tuple 和 list 返回。条件、循环、递归、可变参数、默认参数和关键字调用参数尚未纳入 V1。
|
|
329
|
+
|
|
330
|
+
总体流程:
|
|
331
|
+
|
|
332
|
+
```text
|
|
333
|
+
Python Source -> Python AST -> GraphIR -> Executor -> Result
|
|
334
|
+
|
|
|
335
|
+
+-> Printer -> dict / JSON / Mermaid / Graphviz
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
V1 不包含 Planner、ExecutablePlanIR、Dask、Ray 或 MPI 后端。设计说明见 [`docs/v1/dev.md`](docs/v1/dev.md)。
|
|
339
|
+
|
|
340
|
+
## 开发
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
uv run pytest
|
|
344
|
+
uv run python -m benchmarks.benchmark_executor
|
|
345
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
task_flow/__init__.py,sha256=jw7xigbwdpbVjGB8RP-EYPJSsVU0KUxzNgFLjbiMcFk,335
|
|
2
|
+
task_flow/api.py,sha256=Zv7wfgqEcfOZWJWumORSGd43w3AzjYIBCdKZ30zqfUA,4642
|
|
3
|
+
task_flow/compiler/__init__.py,sha256=g2_LuWHt5rqbKaaVbDO2iJKQ5fD6EcgrWbfXJeQ6pgU,155
|
|
4
|
+
task_flow/compiler/builder.py,sha256=ULLO6Tgu08yeRwGxda08qQqOcDW4zp6guzhctKdsPjI,2249
|
|
5
|
+
task_flow/compiler/compiler.py,sha256=BrM6225_365fSrTTmepnloRT7geyF9kY8ztXTM7ChCw,6652
|
|
6
|
+
task_flow/ir/__init__.py,sha256=hHuEgw5BDUGO9iso2p0C_0MATIbd2uZLyFIFCaznO0U,68
|
|
7
|
+
task_flow/ir/graph.py,sha256=kWA0CWvLp0zb5oCl2Luojttx80YGong-XBmRZ4_eF8o,2721
|
|
8
|
+
task_flow/printer/__init__.py,sha256=DyyN0Chjp6Fphh_wgSivXbUlSA0m5roCSmI75VXrPj8,186
|
|
9
|
+
task_flow/printer/printer.py,sha256=Xs6hFdbYo-WqaUU6YARGprwepnS4iF9cv-RjmTx6Wh8,3712
|
|
10
|
+
task_flow/runtime/__init__.py,sha256=ANHuKxQ0rcTvCNxOh50Cws0lKAIKVVMYdjTUcZRRRA4,159
|
|
11
|
+
task_flow/runtime/executor.py,sha256=9W96zKAmnq9Eedp3XKhgdH6CKTLRTQNLOnW4jW1ic-4,4876
|
|
12
|
+
taskflow_tr-0.1.0.dist-info/licenses/LICENSE,sha256=0-vpht0C3dJgVh7KnbT1wjz--5Yd_jznV74lQuSthEQ,1065
|
|
13
|
+
taskflow_tr-0.1.0.dist-info/METADATA,sha256=6WQb4-DTHmx2jRXm6ufEXwyNEJ2kFPWvYXBBZIbJBSE,8963
|
|
14
|
+
taskflow_tr-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
15
|
+
taskflow_tr-0.1.0.dist-info/top_level.txt,sha256=oMr2tOAseQ0qj3RIu9FTFz9Chwk5q_mq6Q0T8WHEFu0,10
|
|
16
|
+
taskflow_tr-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gaoxinge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
task_flow
|