flowweave-lite 1.0.0__tar.gz
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.
- flowweave_lite-1.0.0/LICENSE +24 -0
- flowweave_lite-1.0.0/PKG-INFO +26 -0
- flowweave_lite-1.0.0/README.md +12 -0
- flowweave_lite-1.0.0/flowweave/__init__.py +6 -0
- flowweave_lite-1.0.0/flowweave/base.py +90 -0
- flowweave_lite-1.0.0/flowweave/cli.py +84 -0
- flowweave_lite-1.0.0/flowweave/flowweave.py +420 -0
- flowweave_lite-1.0.0/flowweave/message.py +101 -0
- flowweave_lite-1.0.0/flowweave/print_lock.py +3 -0
- flowweave_lite-1.0.0/flowweave/schema/flow.json +72 -0
- flowweave_lite-1.0.0/flowweave/schema/op_code.json +20 -0
- flowweave_lite-1.0.0/flowweave_lite.egg-info/PKG-INFO +26 -0
- flowweave_lite-1.0.0/flowweave_lite.egg-info/SOURCES.txt +17 -0
- flowweave_lite-1.0.0/flowweave_lite.egg-info/dependency_links.txt +1 -0
- flowweave_lite-1.0.0/flowweave_lite.egg-info/entry_points.txt +2 -0
- flowweave_lite-1.0.0/flowweave_lite.egg-info/requires.txt +3 -0
- flowweave_lite-1.0.0/flowweave_lite.egg-info/top_level.txt +1 -0
- flowweave_lite-1.0.0/pyproject.toml +26 -0
- flowweave_lite-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This project depends on third-party open source software.
|
|
2
|
+
Their licenses are governed by their respective authors.
|
|
3
|
+
|
|
4
|
+
MIT License
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2026 syatch
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
in the Software without restriction, including without limitation the rights
|
|
11
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
furnished to do so, subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
copies or substantial portions of the Software.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
SOFTWARE.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowweave-lite
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: YAML-based workflow runner for task orchestration (lite version of flowweave)
|
|
5
|
+
Author: syatch
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: colorama>=0.4.6
|
|
11
|
+
Requires-Dist: jsonschema>=4.25.0
|
|
12
|
+
Requires-Dist: pyyaml<7,>=6
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# FlowWeave
|
|
16
|
+
YAML-based workflow runner for task orchestration
|
|
17
|
+
|
|
18
|
+
This project is in early development.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install FlowWeave using pip:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install flowweave
|
|
26
|
+
```
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Standard library
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
from typing import IO, Optional
|
|
4
|
+
|
|
5
|
+
# Third-party
|
|
6
|
+
from colorama import Fore
|
|
7
|
+
|
|
8
|
+
# Local application / relative imports
|
|
9
|
+
from .print_lock import print_lock
|
|
10
|
+
|
|
11
|
+
class FlowWeaveResult(IntEnum):
|
|
12
|
+
FAIL = 0
|
|
13
|
+
SUCCESS = 1
|
|
14
|
+
IGNORE = 2
|
|
15
|
+
|
|
16
|
+
class TaskData:
|
|
17
|
+
def __init__(self,
|
|
18
|
+
name,
|
|
19
|
+
task_class,
|
|
20
|
+
option: dict,
|
|
21
|
+
stage_name: str,
|
|
22
|
+
flow_part: int,
|
|
23
|
+
flow_all: int,
|
|
24
|
+
do_only: str = None,
|
|
25
|
+
show_log: bool = False):
|
|
26
|
+
self.name = name
|
|
27
|
+
self.task_class = task_class
|
|
28
|
+
self.option = option
|
|
29
|
+
|
|
30
|
+
self.stage_name = stage_name
|
|
31
|
+
self.flow_part = flow_part
|
|
32
|
+
self.flow_all = flow_all
|
|
33
|
+
|
|
34
|
+
self.do_only = do_only
|
|
35
|
+
self.show_log = show_log
|
|
36
|
+
|
|
37
|
+
class FlowWeaveTask:
|
|
38
|
+
def __init__(self, prev_future):
|
|
39
|
+
self.prev_future = prev_future
|
|
40
|
+
self.return_data = None
|
|
41
|
+
|
|
42
|
+
def __call__(self):
|
|
43
|
+
result, return_data = self.run()
|
|
44
|
+
return result, return_data
|
|
45
|
+
|
|
46
|
+
def run(self):
|
|
47
|
+
return FlowWeaveResult.SUCCESS, self.return_data
|
|
48
|
+
|
|
49
|
+
def set_task_data(self, task_data: TaskData) -> None:
|
|
50
|
+
self.task_data = task_data
|
|
51
|
+
|
|
52
|
+
def message(self,
|
|
53
|
+
*args: object,
|
|
54
|
+
sep: str = " ",
|
|
55
|
+
end: str = "\n",
|
|
56
|
+
file: Optional[IO[str]] = None,
|
|
57
|
+
flush: bool = False) -> None:
|
|
58
|
+
if not self.task_data:
|
|
59
|
+
raise Exception("task_data is 'None'")
|
|
60
|
+
|
|
61
|
+
part_num = self.task_data.flow_part
|
|
62
|
+
all_num = self.task_data.flow_all
|
|
63
|
+
stage = self.task_data.stage_name
|
|
64
|
+
task = self.task_data.name
|
|
65
|
+
head = f"[Flow {part_num} / {all_num}] {stage}/{task}"
|
|
66
|
+
|
|
67
|
+
args = [f"{head}: {str(a)}" for a in args]
|
|
68
|
+
|
|
69
|
+
with print_lock:
|
|
70
|
+
print(*args, sep=sep, end=end, file=file, flush=flush)
|
|
71
|
+
|
|
72
|
+
def error(self,
|
|
73
|
+
*args: object,
|
|
74
|
+
sep: str = " ",
|
|
75
|
+
end: str = "\n",
|
|
76
|
+
file: Optional[IO[str]] = None,
|
|
77
|
+
flush: bool = False) -> None:
|
|
78
|
+
if not self.task_data:
|
|
79
|
+
raise Exception("task_data is 'None'")
|
|
80
|
+
|
|
81
|
+
part_num = self.task_data.flow_part
|
|
82
|
+
all_num = self.task_data.flow_all
|
|
83
|
+
stage = self.task_data.stage_name
|
|
84
|
+
task = self.task_data.name
|
|
85
|
+
head = f"[Flow {part_num} / {all_num}] {stage}/{task}"
|
|
86
|
+
|
|
87
|
+
args = [f"{head}: {Fore.RED}{str(a)}" for a in args]
|
|
88
|
+
|
|
89
|
+
with print_lock:
|
|
90
|
+
print(*args, sep=sep, end=end, file=file, flush=flush)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Standard library
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Third-party
|
|
7
|
+
import colorama
|
|
8
|
+
|
|
9
|
+
# Local application / relative imports
|
|
10
|
+
from .flowweave import FlowWeave
|
|
11
|
+
from .base import FlowWeaveResult
|
|
12
|
+
|
|
13
|
+
def get_setting_path(args):
|
|
14
|
+
setting_path = None
|
|
15
|
+
|
|
16
|
+
if args.flow_file:
|
|
17
|
+
setting_path = str(Path("flow") / f"{args.flow_file}.yml")
|
|
18
|
+
|
|
19
|
+
return setting_path
|
|
20
|
+
|
|
21
|
+
def serialize(obj) -> str:
|
|
22
|
+
if isinstance(obj, type):
|
|
23
|
+
return obj.__name__
|
|
24
|
+
raise TypeError(f"Type {type(obj)} not serializable")
|
|
25
|
+
|
|
26
|
+
def show_flow_op(setting_path: str, flow_name: str, info: bool = False) -> None:
|
|
27
|
+
flow_data = FlowWeave.load_and_validate_schema(file=setting_path, schema="flow")
|
|
28
|
+
op_dic = FlowWeave.get_op_dic(flow_data, info=info)
|
|
29
|
+
print_op_dic(op_dic, flow_name)
|
|
30
|
+
|
|
31
|
+
def show_available_op() -> None:
|
|
32
|
+
op_dic = FlowWeave.get_available_op_dic()
|
|
33
|
+
print_op_dic(op_dic)
|
|
34
|
+
|
|
35
|
+
def print_op_dic(op_dic: dict, flow_name: str = "") -> None:
|
|
36
|
+
flow_text = ""
|
|
37
|
+
if "" != flow_name:
|
|
38
|
+
flow_text = f"in {flow_name} "
|
|
39
|
+
print(f"Operation code available {flow_text}(<op> : <class>)")
|
|
40
|
+
print(json.dumps(op_dic, indent=2, default=serialize))
|
|
41
|
+
|
|
42
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
43
|
+
parser = argparse.ArgumentParser(prog="flowweave")
|
|
44
|
+
|
|
45
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
46
|
+
run_parser = subparsers.add_parser("run", help="Run a flow file")
|
|
47
|
+
run_parser.add_argument("flow_file", help="Path to YAML flow file")
|
|
48
|
+
run_parser.add_argument("-l", "--log", help="Show flow log", action='store_true')
|
|
49
|
+
run_parser.add_argument("-p", "--parallel", help="Run flow parallel", action='store_true')
|
|
50
|
+
|
|
51
|
+
info_parser = subparsers.add_parser("info", help="Show info")
|
|
52
|
+
info_parser.add_argument(
|
|
53
|
+
"flow_file",
|
|
54
|
+
nargs="?",
|
|
55
|
+
default=None,
|
|
56
|
+
help="Path to YAML flow file"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return parser
|
|
60
|
+
|
|
61
|
+
def main() -> None:
|
|
62
|
+
result = FlowWeaveResult.SUCCESS
|
|
63
|
+
|
|
64
|
+
colorama.init(autoreset=True)
|
|
65
|
+
|
|
66
|
+
parser = build_parser()
|
|
67
|
+
args = parser.parse_args()
|
|
68
|
+
|
|
69
|
+
setting_path = get_setting_path(args)
|
|
70
|
+
|
|
71
|
+
if args.command == "run":
|
|
72
|
+
if not setting_path:
|
|
73
|
+
parser.error("run requires flow_file")
|
|
74
|
+
results = FlowWeave.run(setting_file=setting_path, parallel=args.parallel, show_log = args.log)
|
|
75
|
+
result = all(x == FlowWeaveResult.SUCCESS for x in results)
|
|
76
|
+
elif args.command == "info":
|
|
77
|
+
if args.flow_file:
|
|
78
|
+
show_flow_op(setting_path, args.flow_file, info=True)
|
|
79
|
+
else:
|
|
80
|
+
show_available_op()
|
|
81
|
+
else:
|
|
82
|
+
parser.print_help()
|
|
83
|
+
|
|
84
|
+
return result
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# Standard library
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
import copy
|
|
4
|
+
from functools import reduce
|
|
5
|
+
import importlib
|
|
6
|
+
from importlib.resources import files
|
|
7
|
+
import inspect
|
|
8
|
+
import itertools
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
# Third-party
|
|
16
|
+
import jsonschema
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
# Local application / relative imports
|
|
20
|
+
from .base import FlowWeaveResult, TaskData, FlowWeaveTask
|
|
21
|
+
from .message import FlowMessage
|
|
22
|
+
|
|
23
|
+
class StageData():
|
|
24
|
+
def __init__(self, name: str, stage_info: dict, default_option: dict, global_option: dict, op_dic: dict, flow_part: int, flow_all: int):
|
|
25
|
+
self.name = name
|
|
26
|
+
self.stage_info = stage_info
|
|
27
|
+
self.default_option = default_option
|
|
28
|
+
self.global_option = global_option
|
|
29
|
+
self.op_dic = op_dic
|
|
30
|
+
|
|
31
|
+
self.flow_part = flow_part
|
|
32
|
+
self.flow_all = flow_all
|
|
33
|
+
|
|
34
|
+
def __str__(self):
|
|
35
|
+
text = "== Stage ==\n"
|
|
36
|
+
text += f"Name: {self.name}\n"
|
|
37
|
+
text += f"Stage Info: {self.stage_info}\n"
|
|
38
|
+
text += f"Default: {self.default_option}\n"
|
|
39
|
+
text += f"Global: {self.global_option}\n"
|
|
40
|
+
text += f"Operation: {self.op_dic}\n"
|
|
41
|
+
text += "==========="
|
|
42
|
+
return text
|
|
43
|
+
|
|
44
|
+
class TaskRunner:
|
|
45
|
+
@staticmethod
|
|
46
|
+
def start(prev_result, task_data: TaskData):
|
|
47
|
+
return_data = None
|
|
48
|
+
try:
|
|
49
|
+
task_instance = task_data.task_class(prev_result)
|
|
50
|
+
except AttributeError:
|
|
51
|
+
raise TypeError(f"Failed to get instance of '{task_data.task_class}'")
|
|
52
|
+
|
|
53
|
+
setattr(task_instance, "task_data", task_data)
|
|
54
|
+
|
|
55
|
+
for key, value in task_data.option.items():
|
|
56
|
+
if hasattr(task_instance, key):
|
|
57
|
+
setattr(task_instance, key, value)
|
|
58
|
+
|
|
59
|
+
run_task = True
|
|
60
|
+
if prev_result is not None:
|
|
61
|
+
if task_data.do_only == "pre_success":
|
|
62
|
+
run_task = prev_result["result"] == FlowWeaveResult.SUCCESS
|
|
63
|
+
elif task_data.do_only == "pre_fail":
|
|
64
|
+
run_task = prev_result["result"] == FlowWeaveResult.FAIL
|
|
65
|
+
|
|
66
|
+
if run_task:
|
|
67
|
+
TaskRunner.message_task_start(prev_result, task_data)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
task_result, return_data = task_instance()
|
|
71
|
+
except Exception as e:
|
|
72
|
+
FlowMessage.error(e)
|
|
73
|
+
task_result = FlowWeaveResult.FAIL
|
|
74
|
+
return_data = None
|
|
75
|
+
|
|
76
|
+
FlowMessage.task_end(task_data, task_result)
|
|
77
|
+
else:
|
|
78
|
+
TaskRunner.message_task_ignore(prev_result, task_data)
|
|
79
|
+
task_result = FlowWeaveResult.IGNORE
|
|
80
|
+
return_data = None
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"name": task_data.name,
|
|
84
|
+
"option": task_data.option,
|
|
85
|
+
"data": return_data,
|
|
86
|
+
"result": task_result,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
def message_task_start(prev_result, task_data: TaskData):
|
|
90
|
+
if prev_result is not None:
|
|
91
|
+
prev_task_name = prev_result.get("name")
|
|
92
|
+
FlowMessage.task_start_link(prev_task_name, task_data)
|
|
93
|
+
else:
|
|
94
|
+
FlowMessage.task_start(task_data)
|
|
95
|
+
|
|
96
|
+
def message_task_ignore(prev_result, task_data: TaskData):
|
|
97
|
+
if prev_result is not None:
|
|
98
|
+
prev_task_name = prev_result.get("name")
|
|
99
|
+
FlowMessage.task_ignore_link(task_data, prev_task_name)
|
|
100
|
+
else:
|
|
101
|
+
FlowMessage.task_ignore(task_data)
|
|
102
|
+
|
|
103
|
+
class FlowWeave():
|
|
104
|
+
def run(setting_file: str, parallel: bool = False, show_log: bool = False) -> list[FlowWeaveResult]:
|
|
105
|
+
if not show_log:
|
|
106
|
+
logging.getLogger("prefect").setLevel(logging.CRITICAL)
|
|
107
|
+
|
|
108
|
+
flow_executor = ThreadPoolExecutor(max_workers=os.cpu_count())
|
|
109
|
+
task_executor = ThreadPoolExecutor(max_workers=min(32, (os.cpu_count() or 1) + 4))
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
flow_data = FlowWeave.load_and_validate_schema(setting_file, "flow")
|
|
113
|
+
|
|
114
|
+
op_dic = FlowWeave.get_op_dic(flow_data)
|
|
115
|
+
|
|
116
|
+
global_option = flow_data.get("global_option")
|
|
117
|
+
comb_list = list()
|
|
118
|
+
if global_option:
|
|
119
|
+
comb_list = FlowWeave._get_global_option_comb(global_option)
|
|
120
|
+
else:
|
|
121
|
+
comb_list = [{}]
|
|
122
|
+
|
|
123
|
+
comb_count = 0
|
|
124
|
+
all_count = len(comb_list)
|
|
125
|
+
futures = []
|
|
126
|
+
results = []
|
|
127
|
+
for comb in comb_list:
|
|
128
|
+
comb_count += 1
|
|
129
|
+
FlowMessage.flow_start(comb_count, all_count)
|
|
130
|
+
FlowMessage.flow_message(comb_count, all_count, comb)
|
|
131
|
+
if parallel:
|
|
132
|
+
futures.append(flow_executor.submit(FlowWeave.run_flow,
|
|
133
|
+
flow_data=flow_data,
|
|
134
|
+
global_cmb=comb,
|
|
135
|
+
op_dic=op_dic,
|
|
136
|
+
part=comb_count,
|
|
137
|
+
all=all_count,
|
|
138
|
+
executor=task_executor,
|
|
139
|
+
show_log=show_log))
|
|
140
|
+
else:
|
|
141
|
+
result = FlowWeave.run_flow(flow_data=flow_data,
|
|
142
|
+
global_cmb=comb,
|
|
143
|
+
op_dic=op_dic,
|
|
144
|
+
part=comb_count,
|
|
145
|
+
all=all_count,
|
|
146
|
+
executor=task_executor,
|
|
147
|
+
show_log=show_log)
|
|
148
|
+
results.append(result)
|
|
149
|
+
FlowMessage.flow_end(comb_count, all_count, result)
|
|
150
|
+
|
|
151
|
+
if parallel:
|
|
152
|
+
comb_count = 0
|
|
153
|
+
for f in futures:
|
|
154
|
+
comb_count += 1
|
|
155
|
+
result = f.result()
|
|
156
|
+
results.append(result)
|
|
157
|
+
FlowMessage.flow_end(comb_count, all_count, result)
|
|
158
|
+
|
|
159
|
+
return results
|
|
160
|
+
finally:
|
|
161
|
+
flow_executor.shutdown(wait=True)
|
|
162
|
+
task_executor.shutdown(wait=True)
|
|
163
|
+
|
|
164
|
+
def load_and_validate_schema(file: str, schema: str) -> dict:
|
|
165
|
+
data = FlowWeave._load_yaml(file)
|
|
166
|
+
schema = FlowWeave._load_schema(schema)
|
|
167
|
+
jsonschema.validate(instance=data, schema=schema)
|
|
168
|
+
|
|
169
|
+
return data
|
|
170
|
+
|
|
171
|
+
def _load_yaml(path: str) -> dict:
|
|
172
|
+
file_path = Path(path)
|
|
173
|
+
if not file_path.exists() or not file_path.is_file():
|
|
174
|
+
raise FileNotFoundError(f"{file_path.resolve()} does not exist or not file")
|
|
175
|
+
|
|
176
|
+
with open(Path(path), "r", encoding="utf-8") as f:
|
|
177
|
+
data = yaml.safe_load(f)
|
|
178
|
+
return data
|
|
179
|
+
|
|
180
|
+
def _load_schema(schema: str) -> dict:
|
|
181
|
+
schema_path = files("flowweave")/ "schema" / f"{schema}.json"
|
|
182
|
+
with schema_path.open("r", encoding="utf-8") as f:
|
|
183
|
+
data = json.load(f)
|
|
184
|
+
|
|
185
|
+
return data
|
|
186
|
+
|
|
187
|
+
def get_op_dic(flow_data: dict, info: bool = False):
|
|
188
|
+
return_dic = dict()
|
|
189
|
+
|
|
190
|
+
op_source = flow_data.get("op_source")
|
|
191
|
+
op_source_list = op_source if isinstance(op_source, list) else [op_source]
|
|
192
|
+
for source in op_source_list:
|
|
193
|
+
source_name = f"task.{source}"
|
|
194
|
+
setting_file = f"{source_name.replace('.', '/')}/op_code.yml"
|
|
195
|
+
return_dic |= FlowWeave._get_op_dic_from_setting_file(setting_file, info=info)
|
|
196
|
+
|
|
197
|
+
return return_dic
|
|
198
|
+
|
|
199
|
+
def get_available_op_dic():
|
|
200
|
+
return_dic = dict()
|
|
201
|
+
|
|
202
|
+
base_path = Path("task")
|
|
203
|
+
avaliable_settings = [str(f) for f in base_path.rglob("op_code.yml")]
|
|
204
|
+
for setting in avaliable_settings:
|
|
205
|
+
place = setting.replace("\\", ".").removeprefix("task.").removesuffix(".op_code.yml")
|
|
206
|
+
return_dic[place] = FlowWeave._get_op_dic_from_setting_file(setting.replace("\\", "/"), info=True)
|
|
207
|
+
|
|
208
|
+
return return_dic
|
|
209
|
+
|
|
210
|
+
def _get_op_dic_from_setting_file(setting_file: str, info: bool = False):
|
|
211
|
+
return_dic = dict()
|
|
212
|
+
|
|
213
|
+
setting = FlowWeave.load_and_validate_schema(setting_file, "op_code")
|
|
214
|
+
source_name = setting_file.removesuffix("/op_code.yml").replace("/", ".")
|
|
215
|
+
|
|
216
|
+
task_root = Path("task").resolve()
|
|
217
|
+
if str(task_root.parent) not in sys.path:
|
|
218
|
+
sys.path.insert(0, str(task_root.parent))
|
|
219
|
+
|
|
220
|
+
op_dic = setting.get("op", {})
|
|
221
|
+
for op, op_info in op_dic.items():
|
|
222
|
+
script_name = op_info.get('script')
|
|
223
|
+
op_class = FlowWeave._get_op_class(source_name, script_name, FlowWeaveTask)
|
|
224
|
+
|
|
225
|
+
return_dic[str(op)] = op_class
|
|
226
|
+
|
|
227
|
+
return return_dic
|
|
228
|
+
|
|
229
|
+
def _get_op_class(source_name: str, script_name: str, base_class):
|
|
230
|
+
module_name = f"{source_name}.{script_name}"
|
|
231
|
+
|
|
232
|
+
if module_name in sys.modules:
|
|
233
|
+
loaded_path = Path(sys.modules[module_name].__file__).resolve()
|
|
234
|
+
spec = importlib.util.find_spec(module_name)
|
|
235
|
+
if not spec or not spec.origin:
|
|
236
|
+
raise RuntimeError(f"Cannot resolve module path for {module_name}")
|
|
237
|
+
|
|
238
|
+
new_path = Path(spec.origin).resolve()
|
|
239
|
+
|
|
240
|
+
if loaded_path != new_path:
|
|
241
|
+
raise RuntimeError(
|
|
242
|
+
f"Module name collision: {module_name}\n"
|
|
243
|
+
f"Already loaded from: {loaded_path}\n"
|
|
244
|
+
f"Trying to load from: {new_path}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
module = importlib.import_module(module_name)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
raise RuntimeError(f"Failed to import {module_name}: {e}")
|
|
251
|
+
|
|
252
|
+
candidates = []
|
|
253
|
+
|
|
254
|
+
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
255
|
+
if obj.__module__ != module.__name__:
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
if issubclass(obj, base_class) and obj is not base_class:
|
|
259
|
+
candidates.append(obj)
|
|
260
|
+
|
|
261
|
+
if len(candidates) == 0:
|
|
262
|
+
raise RuntimeError(
|
|
263
|
+
f"No subclass of {base_class.__name__} found in {module_name}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if len(candidates) > 1:
|
|
267
|
+
names = ", ".join(c.__name__ for c in candidates)
|
|
268
|
+
raise RuntimeError(
|
|
269
|
+
f"Multiple subclasses of {base_class.__name__} found in {module_name}: {names}"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
cls = candidates[0]
|
|
273
|
+
|
|
274
|
+
return cls
|
|
275
|
+
|
|
276
|
+
def _get_global_option_comb(global_option: dict) -> list:
|
|
277
|
+
keys = list(global_option.keys())
|
|
278
|
+
value_lists = []
|
|
279
|
+
|
|
280
|
+
for key in keys:
|
|
281
|
+
inner_dict = global_option[key]
|
|
282
|
+
value_lists.append([dict(zip(inner_dict.keys(), v))
|
|
283
|
+
for v in itertools.product(*inner_dict.values())])
|
|
284
|
+
|
|
285
|
+
all_combinations = []
|
|
286
|
+
for combo in itertools.product(*value_lists):
|
|
287
|
+
combined = dict(zip(keys, combo))
|
|
288
|
+
all_combinations.append(combined)
|
|
289
|
+
|
|
290
|
+
return all_combinations
|
|
291
|
+
|
|
292
|
+
def run_flow(flow_data: dict, global_cmb: dict, op_dic: dict, part: int, all: int, executor: ThreadPoolExecutor, show_log: bool = False) -> FlowWeaveResult:
|
|
293
|
+
flow_result = FlowWeaveResult.SUCCESS
|
|
294
|
+
|
|
295
|
+
if show_log:
|
|
296
|
+
text = "= Flow =\n"
|
|
297
|
+
text += f"Stage: {flow_data.get('flow')}\n"
|
|
298
|
+
text += "========"
|
|
299
|
+
FlowMessage.flow_print(text)
|
|
300
|
+
|
|
301
|
+
default_option = flow_data.get("default_option", {})
|
|
302
|
+
|
|
303
|
+
stage_list = flow_data.get("flow")
|
|
304
|
+
for stage in stage_list:
|
|
305
|
+
FlowMessage.stage_start(stage, part, all)
|
|
306
|
+
|
|
307
|
+
stage_info = flow_data.get("stage", {}).get(stage)
|
|
308
|
+
stage_global_option = FlowWeave._get_stage_global_option(global_cmb, stage)
|
|
309
|
+
|
|
310
|
+
stage_data = StageData(stage, stage_info, default_option, stage_global_option, op_dic, part, all)
|
|
311
|
+
if show_log:
|
|
312
|
+
FlowMessage.flow_print(str(stage_data))
|
|
313
|
+
|
|
314
|
+
result = FlowWeave._run_stage(stage_data, executor, show_log)
|
|
315
|
+
if FlowWeaveResult.FAIL == result:
|
|
316
|
+
flow_result = FlowWeaveResult.FAIL
|
|
317
|
+
|
|
318
|
+
FlowMessage.stage_end(stage, part, all, result)
|
|
319
|
+
|
|
320
|
+
return flow_result
|
|
321
|
+
|
|
322
|
+
def _get_stage_global_option(global_cmb: dict, stage: str) -> dict:
|
|
323
|
+
stage_global_option = dict()
|
|
324
|
+
|
|
325
|
+
for stages, option in global_cmb.items():
|
|
326
|
+
stage_list = [x.strip() for x in stages.split(",")]
|
|
327
|
+
if stage in stage_list:
|
|
328
|
+
stage_global_option |= option
|
|
329
|
+
|
|
330
|
+
return stage_global_option
|
|
331
|
+
|
|
332
|
+
def _run_stage(stage_data: StageData, executor: ThreadPoolExecutor, show_log: bool = False):
|
|
333
|
+
stage_result = FlowWeaveResult.SUCCESS
|
|
334
|
+
|
|
335
|
+
all_futures = []
|
|
336
|
+
|
|
337
|
+
for task_name, task_dic in stage_data.stage_info.items():
|
|
338
|
+
part = task_dic.get("chain", {}).get("part", "head")
|
|
339
|
+
if "head" == part:
|
|
340
|
+
all_futures.extend(
|
|
341
|
+
FlowWeave._run_task(stage_data, task_name, executor, None, None, show_log)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
for f in all_futures:
|
|
345
|
+
try:
|
|
346
|
+
result = f.result()
|
|
347
|
+
if FlowWeaveResult.FAIL == result.get("result"):
|
|
348
|
+
stage_result = FlowWeaveResult.FAIL
|
|
349
|
+
except Exception as e:
|
|
350
|
+
FlowMessage.error(e)
|
|
351
|
+
stage_result = FlowWeaveResult.FAIL
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
return stage_result
|
|
355
|
+
|
|
356
|
+
def _deep_merge(a: dict, b: dict) -> dict:
|
|
357
|
+
result = copy.deepcopy(a)
|
|
358
|
+
for k, v in b.items():
|
|
359
|
+
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
|
|
360
|
+
result[k] = FlowWeave._deep_merge(result[k], v)
|
|
361
|
+
else:
|
|
362
|
+
result[k] = v
|
|
363
|
+
return result
|
|
364
|
+
|
|
365
|
+
def _deep_merge_many(*dicts):
|
|
366
|
+
return reduce(FlowWeave._deep_merge, dicts)
|
|
367
|
+
|
|
368
|
+
def _submit_task(prev_future, task_data, executor):
|
|
369
|
+
if prev_future is None:
|
|
370
|
+
return executor.submit(TaskRunner.start, None, task_data)
|
|
371
|
+
|
|
372
|
+
def runner():
|
|
373
|
+
prev_result = prev_future.result()
|
|
374
|
+
return TaskRunner.start(prev_result, task_data)
|
|
375
|
+
|
|
376
|
+
return executor.submit(runner)
|
|
377
|
+
|
|
378
|
+
def _run_task(stage_data: dict, task_name: str, executor: ThreadPoolExecutor, prev_future = None, visited = None, show_log: bool = False):
|
|
379
|
+
if visited is None:
|
|
380
|
+
visited = set()
|
|
381
|
+
if task_name in visited:
|
|
382
|
+
raise Exception(f"Cycle detected at task '{task_name}' in {visited}")
|
|
383
|
+
visited.add(task_name)
|
|
384
|
+
try:
|
|
385
|
+
task_dic = stage_data.stage_info.get(task_name)
|
|
386
|
+
|
|
387
|
+
task_module = stage_data.op_dic.get(task_dic.get('op'))
|
|
388
|
+
if not task_module:
|
|
389
|
+
raise Exception(f"module of op '{task_dic.get('op')}' for '{task_name}' not found")
|
|
390
|
+
|
|
391
|
+
default_option = stage_data.default_option or {}
|
|
392
|
+
global_option = stage_data.global_option or {}
|
|
393
|
+
task_option = FlowWeave._deep_merge_many(default_option, global_option, task_dic.get("option", {}))
|
|
394
|
+
|
|
395
|
+
task_data = TaskData(name=task_name,
|
|
396
|
+
task_class=task_module,
|
|
397
|
+
option=task_option,
|
|
398
|
+
stage_name=stage_data.name,
|
|
399
|
+
flow_part=stage_data.flow_part,
|
|
400
|
+
flow_all=stage_data.flow_all,
|
|
401
|
+
do_only=task_dic.get("do_only"),
|
|
402
|
+
show_log=show_log)
|
|
403
|
+
if prev_future is None:
|
|
404
|
+
future = FlowWeave._submit_task(None, task_data, executor)
|
|
405
|
+
else:
|
|
406
|
+
future = FlowWeave._submit_task(prev_future, task_data, executor)
|
|
407
|
+
|
|
408
|
+
links = task_dic.get("chain", {}).get("next", [])
|
|
409
|
+
links = links if isinstance(links, list) else [links]
|
|
410
|
+
|
|
411
|
+
futures = [future]
|
|
412
|
+
for link in links:
|
|
413
|
+
futures.extend(
|
|
414
|
+
FlowWeave._run_task(stage_data, link, executor, future, visited.copy(), show_log)
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
return futures
|
|
418
|
+
|
|
419
|
+
finally:
|
|
420
|
+
visited.remove(task_name)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Standard library
|
|
2
|
+
from typing import IO, Optional
|
|
3
|
+
|
|
4
|
+
# Third-party
|
|
5
|
+
from colorama import Fore
|
|
6
|
+
|
|
7
|
+
# Local application / relative imports
|
|
8
|
+
from .print_lock import print_lock
|
|
9
|
+
from .base import FlowWeaveResult, TaskData
|
|
10
|
+
|
|
11
|
+
class FlowMessage:
|
|
12
|
+
@staticmethod
|
|
13
|
+
def _print(*args: object,
|
|
14
|
+
sep: str = " ",
|
|
15
|
+
end: str = "\n",
|
|
16
|
+
file: Optional[IO[str]] = None,
|
|
17
|
+
flush: bool = False) -> None:
|
|
18
|
+
with print_lock:
|
|
19
|
+
print(*args, sep=sep, end=end, file=file, flush=flush)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def get_result_text(result: FlowWeaveResult) -> str:
|
|
23
|
+
text = ""
|
|
24
|
+
|
|
25
|
+
if FlowWeaveResult.SUCCESS == result:
|
|
26
|
+
text = f"{Fore.GREEN}SUCCESS"
|
|
27
|
+
elif FlowWeaveResult.IGNORE == result:
|
|
28
|
+
text = f"{Fore.CYAN}IGNORE"
|
|
29
|
+
elif FlowWeaveResult.FAIL == result:
|
|
30
|
+
text = f"{Fore.RED}FAIL"
|
|
31
|
+
else:
|
|
32
|
+
text = f"{Fore.MAGENTA}UNKNOWN: {result.name}({result.value})"
|
|
33
|
+
|
|
34
|
+
return text
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def flow_start(part: int, all: int) -> None:
|
|
38
|
+
text = f"{Fore.YELLOW}[Flow {part} / {all}] Start"
|
|
39
|
+
FlowMessage._print(text)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def flow_print(part: int, all: int, text: dict) -> None:
|
|
43
|
+
message = f"[Flow {part} / {all}] {text}"
|
|
44
|
+
FlowMessage._print(message)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def flow_message(part: int, all: int, global_option: dict) -> None:
|
|
48
|
+
text = f"[Flow {part} / {all}] {global_option}"
|
|
49
|
+
FlowMessage._print(text)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def flow_end(part: int, all: int, result: FlowWeaveResult) -> None:
|
|
53
|
+
result_text = FlowMessage.get_result_text(result)
|
|
54
|
+
text = f"{Fore.YELLOW}[Flow {part} / {all}] Finish - {result_text}"
|
|
55
|
+
FlowMessage._print(text)
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def stage_start(stage: str, part: int, all: int) -> None:
|
|
59
|
+
text = f"{Fore.MAGENTA}[Flow {part} / {all}] Start Stage {stage}"
|
|
60
|
+
FlowMessage._print(text)
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def stage_end(stage: str, part: int, all: int, result: FlowWeaveResult) -> None:
|
|
64
|
+
result_text = FlowMessage.get_result_text(result)
|
|
65
|
+
text = f"{Fore.MAGENTA}[Flow {part} / {all}] Finish Stage {stage} - {result_text}"
|
|
66
|
+
FlowMessage._print(text)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def task_start(task_data: TaskData) -> None:
|
|
70
|
+
text = f"{Fore.CYAN}[Flow {task_data.flow_part} / {task_data.flow_all}] Start Task {task_data.stage_name}/{task_data.name}"
|
|
71
|
+
FlowMessage._print(text)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def task_start_link(prev_task: str, task_data: TaskData) -> None:
|
|
75
|
+
text = f"{Fore.CYAN}[Flow {task_data.flow_part} / {task_data.flow_all}] Start Link Task {task_data.stage_name}/{prev_task} -> {task_data.name}"
|
|
76
|
+
FlowMessage._print(text)
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def task_ignore(task_data: TaskData) -> None:
|
|
80
|
+
text = f"{Fore.CYAN}[Flow {task_data.flow_part} / {task_data.flow_all}] Ignore {task_data.name} (do_only : {task_data.do_only})"
|
|
81
|
+
FlowMessage._print(text)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def task_ignore_link(task_data: TaskData, prev_task_name: str) -> None:
|
|
85
|
+
text = f"{Fore.CYAN}[Flow {task_data.flow_part} / {task_data.flow_all}] Ignore {prev_task_name} -> {task_data.name} (do_only : {task_data.do_only})"
|
|
86
|
+
FlowMessage._print(text)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def task_end(task_data: TaskData, result: FlowWeaveResult) -> None:
|
|
90
|
+
result_text = FlowMessage.get_result_text(result)
|
|
91
|
+
text = f"{Fore.CYAN}[Flow {task_data.flow_part} / {task_data.flow_all}] Finish Task {task_data.stage_name}/{task_data.name} - {result_text}"
|
|
92
|
+
FlowMessage._print(text)
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def error(*args: object,
|
|
96
|
+
sep: str = " ",
|
|
97
|
+
end: str = "\n",
|
|
98
|
+
file: Optional[IO[str]] = None,
|
|
99
|
+
flush: bool = False) -> None:
|
|
100
|
+
args = [f"{Fore.RED}{str(a)}" for a in args]
|
|
101
|
+
FlowMessage._print(*args, sep=sep, end=end, file=file, flush=flush)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"flow": {
|
|
5
|
+
"type": ["array", "string"],
|
|
6
|
+
"items": { "type": "string" }
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
"op_source": {
|
|
10
|
+
"type": ["array", "string"],
|
|
11
|
+
"items": { "type": "string" }
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
"global_option": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"items": {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
"default_option": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": true
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
"stage": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"patternProperties": {
|
|
33
|
+
".*": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"patternProperties": {
|
|
36
|
+
".*": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"op": { "type": "string" },
|
|
40
|
+
|
|
41
|
+
"do_only": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["pre_success", "pre_fail"]
|
|
44
|
+
},
|
|
45
|
+
"chain": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {
|
|
48
|
+
"part": { "type": "string" },
|
|
49
|
+
"next": {
|
|
50
|
+
"type": ["array", "string"],
|
|
51
|
+
"items": { "type": "string" }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"additionalProperties": false
|
|
55
|
+
},
|
|
56
|
+
"option": {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"additionalProperties": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"required": ["op"],
|
|
62
|
+
"additionalProperties": false
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"additionalProperties": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"additionalProperties": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"required": ["flow", "op_source", "stage"]
|
|
72
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"op": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"patternProperties": {
|
|
7
|
+
".*": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"script": { "type": "string" }
|
|
11
|
+
},
|
|
12
|
+
"required": ["script"],
|
|
13
|
+
"additionalProperties": false
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"required": ["op"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flowweave-lite
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: YAML-based workflow runner for task orchestration (lite version of flowweave)
|
|
5
|
+
Author: syatch
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: colorama>=0.4.6
|
|
11
|
+
Requires-Dist: jsonschema>=4.25.0
|
|
12
|
+
Requires-Dist: pyyaml<7,>=6
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# FlowWeave
|
|
16
|
+
YAML-based workflow runner for task orchestration
|
|
17
|
+
|
|
18
|
+
This project is in early development.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install FlowWeave using pip:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install flowweave
|
|
26
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
flowweave/__init__.py
|
|
5
|
+
flowweave/base.py
|
|
6
|
+
flowweave/cli.py
|
|
7
|
+
flowweave/flowweave.py
|
|
8
|
+
flowweave/message.py
|
|
9
|
+
flowweave/print_lock.py
|
|
10
|
+
flowweave/schema/flow.json
|
|
11
|
+
flowweave/schema/op_code.json
|
|
12
|
+
flowweave_lite.egg-info/PKG-INFO
|
|
13
|
+
flowweave_lite.egg-info/SOURCES.txt
|
|
14
|
+
flowweave_lite.egg-info/dependency_links.txt
|
|
15
|
+
flowweave_lite.egg-info/entry_points.txt
|
|
16
|
+
flowweave_lite.egg-info/requires.txt
|
|
17
|
+
flowweave_lite.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flowweave
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "flowweave-lite"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "YAML-based workflow runner for task orchestration (lite version of flowweave)"
|
|
5
|
+
authors = [{name = "syatch"}]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"colorama>=0.4.6",
|
|
10
|
+
"jsonschema>=4.25.0",
|
|
11
|
+
"pyyaml>=6,<7"
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
|
|
15
|
+
[tool.setuptools.packages.find]
|
|
16
|
+
include = ["flowweave", "flowweave.*"]
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.package-data]
|
|
19
|
+
"flowweave" = ["schema/*.json"]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
flowweave = "flowweave.cli:main"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["setuptools", "wheel"]
|
|
26
|
+
build-backend = "setuptools.build_meta"
|