flowweave 2.0.1__tar.gz → 3.0.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowweave
3
- Version: 2.0.1
3
+ Version: 3.0.2
4
4
  Summary: YAML-based workflow runner for task orchestration
5
5
  Author: syatch
6
6
  License: MIT
@@ -16,6 +16,10 @@ Dynamic: license-file
16
16
  # FlowWeave
17
17
  YAML-based workflow runner for task orchestration
18
18
 
19
+ Although this version is more stable, it takes a few seconds to start up.
20
+
21
+ Therefore, [Lite version](https://github.com/syatch/flowweave-lite) is recommended.
22
+
19
23
  This project is in early development.
20
24
 
21
25
  ## Installation
@@ -0,0 +1,16 @@
1
+ # FlowWeave
2
+ YAML-based workflow runner for task orchestration
3
+
4
+ Although this version is more stable, it takes a few seconds to start up.
5
+
6
+ Therefore, [Lite version](https://github.com/syatch/flowweave-lite) is recommended.
7
+
8
+ This project is in early development.
9
+
10
+ ## Installation
11
+
12
+ Install FlowWeave using pip:
13
+
14
+ ```bash
15
+ pip install flowweave
16
+ ```
@@ -0,0 +1,6 @@
1
+ __version__ = "3.0.2"
2
+ __author__ = "syatch"
3
+ __license__ = "MIT"
4
+
5
+ from .flowweave import FlowWeave
6
+ from .base import FlowWeaveResult, FlowWeaveTask
@@ -1,15 +1,32 @@
1
1
  # Standard library
2
2
  from enum import IntEnum
3
+ import importlib
3
4
  from typing import IO, Optional
4
5
 
5
6
  from colorama import Fore
6
7
 
7
- class Result(IntEnum):
8
+ class FlowWeaveResult(IntEnum):
8
9
  FAIL = 0
9
10
  SUCCESS = 1
10
11
  IGNORE = 2
11
12
 
12
13
  class TaskData:
14
+ def __getstate__(self):
15
+ state = self.__dict__.copy()
16
+ state["task_class_path"] = (
17
+ self.task_class.__module__,
18
+ self.task_class.__name__,
19
+ )
20
+ del state["task_class"]
21
+ return state
22
+
23
+ def __setstate__(self, state):
24
+ module_name, class_name = state["task_class_path"]
25
+ module = importlib.import_module(module_name)
26
+ state["task_class"] = getattr(module, class_name)
27
+ del state["task_class_path"]
28
+ self.__dict__.update(state)
29
+
13
30
  def __init__(self,
14
31
  name,
15
32
  task_class,
@@ -30,17 +47,17 @@ class TaskData:
30
47
  self.do_only = do_only
31
48
  self.show_log = show_log
32
49
 
33
- class FlowWeaveTaskRunner:
50
+ class FlowWeaveTask:
34
51
  def __init__(self, prev_future):
35
52
  self.prev_future = prev_future
36
53
  self.return_data = None
37
54
 
38
- def __call__(self) -> Result:
39
- result = self.run()
40
- return result
55
+ def __call__(self):
56
+ result, return_data = self.run()
57
+ return result, return_data
41
58
 
42
- def run(self) -> Result:
43
- return Result.SUCCESS
59
+ def run(self):
60
+ return FlowWeaveResult.SUCCESS, self.return_data
44
61
 
45
62
  def set_task_data(self, task_data: TaskData) -> None:
46
63
  self.task_data = task_data
@@ -8,7 +8,7 @@ import colorama
8
8
 
9
9
  # Local application / relative imports
10
10
  from .flowweave import FlowWeave
11
- from .base import Result
11
+ from .base import FlowWeaveResult
12
12
 
13
13
  def get_setting_path(args):
14
14
  setting_path = None
@@ -59,7 +59,7 @@ def build_parser() -> argparse.ArgumentParser:
59
59
  return parser
60
60
 
61
61
  def main() -> None:
62
- result = Result.SUCCESS
62
+ result = FlowWeaveResult.SUCCESS
63
63
 
64
64
  colorama.init(autoreset=True)
65
65
 
@@ -72,7 +72,7 @@ def main() -> None:
72
72
  if not setting_path:
73
73
  parser.error("run requires flow_file")
74
74
  results = FlowWeave.run(setting_file=setting_path, parallel=args.parallel, show_log = args.log)
75
- result = all(x == Result.SUCCESS for x in results)
75
+ result = all(x == FlowWeaveResult.SUCCESS for x in results)
76
76
  elif args.command == "info":
77
77
  if args.flow_file:
78
78
  show_flow_op(setting_path, args.flow_file, info=True)
@@ -1,11 +1,13 @@
1
1
  # Standard library
2
- import copy
2
+ from functools import reduce
3
3
  import importlib
4
4
  from importlib.resources import files
5
+ import inspect
5
6
  import itertools
6
7
  import json
7
8
  import logging
8
9
  from pathlib import Path
10
+ import pickle
9
11
  import sys
10
12
 
11
13
  # Third-party
@@ -14,7 +16,7 @@ import yaml
14
16
  from prefect import flow, task, get_run_logger
15
17
 
16
18
  # Local application / relative imports
17
- from .base import Result, TaskData
19
+ from .base import FlowWeaveResult, TaskData, FlowWeaveTask
18
20
  from .message import FlowMessage
19
21
 
20
22
  class StageData():
@@ -38,15 +40,17 @@ class StageData():
38
40
  text += "==========="
39
41
  return text
40
42
 
41
- class FlowWeaveTask():
42
- task_class = None
43
-
43
+ class TaskRunner():
44
44
  @task
45
45
  def start(prev_future, task_data: TaskData):
46
+ if hasattr(prev_future, "result"):
47
+ prev_future = prev_future.result()
48
+
49
+ return_data = None
46
50
  try:
47
- task_instance = task_data.task_class.runner(prev_future)
51
+ task_instance = task_data.task_class(prev_future)
48
52
  except AttributeError:
49
- raise TypeError(f"{task_data.task_class} must define runner")
53
+ raise TypeError(f"Failed to get instance of '{task_data.task_class}'")
50
54
 
51
55
  # set task member variables
52
56
  setattr(task_instance, "task_data", task_data)
@@ -60,37 +64,44 @@ class FlowWeaveTask():
60
64
  if task_data.show_log:
61
65
  FlowWeave._print_log(f"Task option {key} not found: ignore")
62
66
  run_task = True
63
- if prev_future:
67
+
68
+ if prev_future is not None:
64
69
  if "pre_success" == task_data.do_only:
65
- run_task = True if (Result.SUCCESS == prev_future.get("result")) else False
70
+ run_task = True if (FlowWeaveResult.SUCCESS == prev_future.get("result")) else False
66
71
  elif "pre_fail" == task_data.do_only:
67
- run_task = True if (Result.FAIL == prev_future.get("result")) else False
72
+ run_task = True if (FlowWeaveResult.FAIL == prev_future.get("result")) else False
68
73
 
69
74
  if run_task:
70
- FlowWeaveTask.message_task_start(prev_future, task_data)
75
+ TaskRunner.message_task_start(prev_future, task_data)
71
76
 
72
77
  try:
73
78
  task_result, return_data = task_instance()
74
79
  except Exception as e:
75
80
  FlowMessage.error(e)
76
- task_result = Result.FAIL
81
+ task_result = FlowWeaveResult.FAIL
77
82
 
78
83
  FlowMessage.task_end(task_data, task_result)
79
84
  else:
80
- FlowWeaveTask.message_task_ignore(prev_future, task_data)
81
- task_result = Result.IGNORE
85
+ TaskRunner.message_task_ignore(prev_future, task_data)
86
+ task_result = FlowWeaveResult.IGNORE
87
+
88
+ try:
89
+ if return_data is not None:
90
+ pickle.dumps(return_data)
91
+ except Exception:
92
+ raise TypeError(f"Task '{task_data.name}' return_data is not serializable")
82
93
 
83
94
  return {"name" : task_data.name, "option" : task_data.option, "data" : return_data, "result" : task_result}
84
95
 
85
96
  def message_task_start(prev_future, task_data: TaskData):
86
- if prev_future:
97
+ if prev_future is not None:
87
98
  prev_task_name = prev_future.get("name")
88
99
  FlowMessage.task_start_link(prev_task_name, task_data)
89
100
  else:
90
101
  FlowMessage.task_start(task_data)
91
102
 
92
103
  def message_task_ignore(prev_future, task_data: TaskData):
93
- if prev_future:
104
+ if prev_future is not None:
94
105
  prev_task_name = prev_future.get("name")
95
106
  FlowMessage.task_ignore_link(task_data, prev_task_name)
96
107
  else:
@@ -98,7 +109,7 @@ class FlowWeaveTask():
98
109
 
99
110
  class FlowWeave():
100
111
  @flow
101
- def run(setting_file: str, parallel: bool = False, show_log: bool = False) -> list[str]:
112
+ def run(setting_file: str, parallel: bool = False, show_log: bool = False) -> list[FlowWeaveResult]:
102
113
  if not show_log:
103
114
  logging.getLogger("prefect").setLevel(logging.CRITICAL)
104
115
 
@@ -177,7 +188,7 @@ class FlowWeave():
177
188
  op_source = flow_data.get("op_source")
178
189
  op_source_list = op_source if isinstance(op_source, list) else [op_source]
179
190
  for source in op_source_list:
180
- source_name = f"task/{source}"
191
+ source_name = f"task.{source}"
181
192
  setting_file = f"{source_name.replace('.', '/')}/op_code.yml"
182
193
  return_dic |= FlowWeave._get_op_dic_from_setting_file(setting_file, info=info)
183
194
 
@@ -207,27 +218,43 @@ class FlowWeave():
207
218
  op_dic = setting.get("op", {})
208
219
  for op, op_info in op_dic.items():
209
220
  script_name = op_info.get('script')
210
- op_class = FlowWeave._get_op_class(source_name, script_name, info)
221
+ op_class = FlowWeave._get_op_class(source_name, script_name, FlowWeaveTask)
211
222
 
212
223
  return_dic[str(op)] = op_class
213
224
 
214
225
  return return_dic
215
226
 
216
- def _get_op_class(source_name: str, script_name: str, info: bool = False):
227
+ def _get_op_class(source_name: str, script_name: str, base_class):
217
228
  module_name = f"{source_name}.{script_name}"
229
+
218
230
  try:
219
231
  module = importlib.import_module(module_name)
220
232
  except Exception as e:
221
233
  raise RuntimeError(f"Failed to import {module_name}: {e}")
222
234
 
223
- if not hasattr(module, "Task"):
224
- raise RuntimeError(f"'Task' class not found in {module_name}")
235
+ candidates = []
225
236
 
226
- return_module = module.Task
227
- if info:
228
- return_module = module.Task.runner
237
+ for _, obj in inspect.getmembers(module, inspect.isclass):
238
+ if obj.__module__ != module.__name__:
239
+ continue
240
+
241
+ if issubclass(obj, base_class) and obj is not base_class:
242
+ candidates.append(obj)
243
+
244
+ if len(candidates) == 0:
245
+ raise RuntimeError(
246
+ f"No subclass of {base_class.__name__} found in {module_name}"
247
+ )
248
+
249
+ if len(candidates) > 1:
250
+ names = ", ".join(c.__name__ for c in candidates)
251
+ raise RuntimeError(
252
+ f"Multiple subclasses of {base_class.__name__} found in {module_name}: {names}"
253
+ )
229
254
 
230
- return return_module
255
+ cls = candidates[0]
256
+
257
+ return cls
231
258
 
232
259
  def _get_global_option_comb(global_option: dict) -> list:
233
260
  keys = list(global_option.keys())
@@ -246,8 +273,8 @@ class FlowWeave():
246
273
  return all_combinations
247
274
 
248
275
  @task
249
- def run_flow(flow_data: dict, global_cmb: dict, op_dic: dict, part: int, all: int, show_log: bool = False) -> list[str]:
250
- flow_result = Result.SUCCESS
276
+ def run_flow(flow_data: dict, global_cmb: dict, op_dic: dict, part: int, all: int, show_log: bool = False) -> FlowWeaveResult:
277
+ flow_result = FlowWeaveResult.SUCCESS
251
278
 
252
279
  if show_log:
253
280
  text = "= Flow =\n"
@@ -255,7 +282,7 @@ class FlowWeave():
255
282
  text += "========"
256
283
  FlowWeave._print_log(text)
257
284
 
258
- default_option = flow_data.get("default_option")
285
+ default_option = flow_data.get("default_option", {})
259
286
 
260
287
  stage_list = flow_data.get("flow")
261
288
  for stage in stage_list:
@@ -269,10 +296,10 @@ class FlowWeave():
269
296
  FlowWeave._print_log(str(stage_data))
270
297
 
271
298
  result = FlowWeave._run_stage(stage_data, show_log)
272
- if Result.FAIL == result:
273
- flow_result = Result.FAIL
299
+ if FlowWeaveResult.FAIL == result:
300
+ flow_result = FlowWeaveResult.FAIL
274
301
 
275
- FlowMessage.stage_end(stage, part, all, flow_result)
302
+ FlowMessage.stage_end(stage, part, all, result)
276
303
 
277
304
  return flow_result
278
305
 
@@ -293,7 +320,7 @@ class FlowWeave():
293
320
  logger.info(f"{text}")
294
321
 
295
322
  def _run_stage(stage_data: StageData, show_log: bool = False):
296
- stage_result = Result.SUCCESS
323
+ stage_result = FlowWeaveResult.SUCCESS
297
324
 
298
325
  all_futures = []
299
326
 
@@ -306,11 +333,23 @@ class FlowWeave():
306
333
 
307
334
  for f in all_futures:
308
335
  result = f.result()
309
- if Result.FAIL == result.get("result"):
310
- stage_result = Result.FAIL
336
+ if FlowWeaveResult.FAIL == result.get("result"):
337
+ stage_result = FlowWeaveResult.FAIL
311
338
 
312
339
  return stage_result
313
340
 
341
+ def _deep_merge(a: dict, b: dict) -> dict:
342
+ result = copy.deepcopy(a)
343
+ for k, v in b.items():
344
+ if k in result and isinstance(result[k], dict) and isinstance(v, dict):
345
+ result[k] = FlowWeave._deep_merge(result[k], v)
346
+ else:
347
+ result[k] = v
348
+ return result
349
+
350
+ def _deep_merge_many(*dicts):
351
+ return reduce(FlowWeave._deep_merge, dicts)
352
+
314
353
  def _run_task(stage_data: dict, task_name: str, prev_future = None, visited = None, show_log: bool = False):
315
354
  if visited is None:
316
355
  visited = set()
@@ -318,40 +357,42 @@ class FlowWeave():
318
357
  raise Exception(f"Cycle detected at task '{task_name}' in {visited}")
319
358
  visited.add(task_name)
320
359
 
321
- task_dic = stage_data.stage_info.get(task_name)
322
-
323
- task_module = stage_data.op_dic.get(task_dic.get('op'))
324
- if not task_module:
325
- raise Exception(f"module of op '{task_dic.get('op')}' for '{task_name}' not found")
326
-
327
- default_option = stage_data.default_option or {}
328
- global_option = stage_data.global_option or {}
329
- task_option = copy.deepcopy(
330
- default_option
331
- | global_option
332
- | task_dic.get("option", {})
333
- )
334
-
335
- task_data = TaskData(name=task_name,
336
- task_class=task_module,
337
- option=task_option,
338
- stage_name=stage_data.name,
339
- flow_part=stage_data.flow_part,
340
- flow_all=stage_data.flow_all,
341
- do_only=task_dic.get("do_only"),
342
- show_log=show_log)
343
- if prev_future is None:
344
- future = task_module.start.submit(None, task_data)
345
- else:
346
- future = task_module.start.submit(prev_future, task_data)
360
+ try:
361
+ task_dic = stage_data.stage_info.get(task_name)
362
+ if task_dic is None:
363
+ raise KeyError(f"Task '{task_name}' not found in stage '{stage_data.name}'")
364
+
365
+ task_module = stage_data.op_dic.get(task_dic.get('op'))
366
+ if not task_module:
367
+ raise Exception(f"module of op '{task_dic.get('op')}' for '{task_name}' not found")
368
+
369
+ default_option = stage_data.default_option or {}
370
+ global_option = stage_data.global_option or {}
371
+ task_option = FlowWeave._deep_merge_many(default_option, global_option, task_dic.get("option", {}))
372
+
373
+ task_data = TaskData(name=task_name,
374
+ task_class=task_module,
375
+ option=task_option,
376
+ stage_name=stage_data.name,
377
+ flow_part=stage_data.flow_part,
378
+ flow_all=stage_data.flow_all,
379
+ do_only=task_dic.get("do_only"),
380
+ show_log=show_log)
381
+ if prev_future is None:
382
+ future = TaskRunner.start.submit(None, task_data)
383
+ else:
384
+ future = TaskRunner.start.submit(prev_future, task_data)
347
385
 
348
- links = task_dic.get("chain", {}).get("next", [])
349
- links = links if isinstance(links, list) else [links]
386
+ links = task_dic.get("chain", {}).get("next", [])
387
+ links = links if isinstance(links, list) else [links]
350
388
 
351
- futures = [future]
352
- for link in links:
353
- futures.extend(
354
- FlowWeave._run_task(stage_data, link, future, visited.copy(), show_log)
355
- )
389
+ futures = [future]
390
+ for link in links:
391
+ futures.extend(
392
+ FlowWeave._run_task(stage_data, link, future, visited.copy(), show_log)
393
+ )
394
+
395
+ return futures
356
396
 
357
- return futures
397
+ finally:
398
+ visited.remove(task_name)
@@ -5,7 +5,7 @@ from typing import IO, Optional
5
5
  from colorama import Fore
6
6
 
7
7
  # Local application / relative imports
8
- from .base import Result, TaskData
8
+ from .base import FlowWeaveResult, TaskData
9
9
 
10
10
  class FlowMessage:
11
11
  @staticmethod
@@ -17,14 +17,14 @@ class FlowMessage:
17
17
  print(*args, sep=sep, end=end, file=file, flush=flush)
18
18
 
19
19
  @staticmethod
20
- def get_result_text(result: Result) -> str:
20
+ def get_result_text(result: FlowWeaveResult) -> str:
21
21
  text = ""
22
22
 
23
- if Result.SUCCESS == result:
23
+ if FlowWeaveResult.SUCCESS == result:
24
24
  text = f"{Fore.GREEN}SUCCESS"
25
- elif Result.IGNORE == result:
25
+ elif FlowWeaveResult.IGNORE == result:
26
26
  text = f"{Fore.CYAN}IGNORE"
27
- elif Result.FAIL == result:
27
+ elif FlowWeaveResult.FAIL == result:
28
28
  text = f"{Fore.RED}FAIL"
29
29
  else:
30
30
  text = f"{Fore.MAGENTA}UNKNOWN: {result.name}({result.value})"
@@ -42,7 +42,7 @@ class FlowMessage:
42
42
  FlowMessage._print(text)
43
43
 
44
44
  @staticmethod
45
- def flow_end(part: int, all: int, result: Result) -> None:
45
+ def flow_end(part: int, all: int, result: FlowWeaveResult) -> None:
46
46
  result_text = FlowMessage.get_result_text(result)
47
47
  text = f"{Fore.YELLOW}[Flow {part} / {all}] Finish - {result_text}"
48
48
  FlowMessage._print(text)
@@ -53,7 +53,7 @@ class FlowMessage:
53
53
  FlowMessage._print(text)
54
54
 
55
55
  @staticmethod
56
- def stage_end(stage: str, part: int, all: int, result: Result) -> None:
56
+ def stage_end(stage: str, part: int, all: int, result: FlowWeaveResult) -> None:
57
57
  result_text = FlowMessage.get_result_text(result)
58
58
  text = f"{Fore.MAGENTA}[Flow {part} / {all}] Finish Stage {stage} - {result_text}"
59
59
  FlowMessage._print(text)
@@ -79,7 +79,7 @@ class FlowMessage:
79
79
  FlowMessage._print(text)
80
80
 
81
81
  @staticmethod
82
- def task_end(task_data: TaskData, result: Result) -> None:
82
+ def task_end(task_data: TaskData, result: FlowWeaveResult) -> None:
83
83
  result_text = FlowMessage.get_result_text(result)
84
84
  text = f"{Fore.CYAN}[Flow {task_data.flow_part} / {task_data.flow_all}] Finish Task {task_data.stage_name}/{task_data.name} - {result_text}"
85
85
  FlowMessage._print(text)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowweave
3
- Version: 2.0.1
3
+ Version: 3.0.2
4
4
  Summary: YAML-based workflow runner for task orchestration
5
5
  Author: syatch
6
6
  License: MIT
@@ -16,6 +16,10 @@ Dynamic: license-file
16
16
  # FlowWeave
17
17
  YAML-based workflow runner for task orchestration
18
18
 
19
+ Although this version is more stable, it takes a few seconds to start up.
20
+
21
+ Therefore, [Lite version](https://github.com/syatch/flowweave-lite) is recommended.
22
+
19
23
  This project is in early development.
20
24
 
21
25
  ## Installation
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flowweave"
3
- version = "2.0.1"
3
+ version = "3.0.2"
4
4
  description = "YAML-based workflow runner for task orchestration"
5
5
  authors = [{name = "syatch"}]
6
6
  readme = "README.md"
flowweave-2.0.1/README.md DELETED
@@ -1,12 +0,0 @@
1
- # FlowWeave
2
- YAML-based workflow runner for task orchestration
3
-
4
- This project is in early development.
5
-
6
- ## Installation
7
-
8
- Install FlowWeave using pip:
9
-
10
- ```bash
11
- pip install flowweave
12
- ```
@@ -1,6 +0,0 @@
1
- __version__ = "2.0.1"
2
- __author__ = "syatch"
3
- __license__ = "MIT"
4
-
5
- from .flowweave import FlowWeave, FlowWeaveTask
6
- from .base import Result, TaskData, FlowWeaveTaskRunner
File without changes
File without changes