pipefy 0.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.
- pipefy-0.0.0/PKG-INFO +7 -0
- pipefy-0.0.0/pyproject.toml +26 -0
- pipefy-0.0.0/setup.cfg +4 -0
- pipefy-0.0.0/src/pipefy/__init__.py +8 -0
- pipefy-0.0.0/src/pipefy/context.py +71 -0
- pipefy-0.0.0/src/pipefy/engine/__init__.py +6 -0
- pipefy-0.0.0/src/pipefy/engine/decision_engine.py +116 -0
- pipefy-0.0.0/src/pipefy/engine/middleware_engine.py +42 -0
- pipefy-0.0.0/src/pipefy/engine/node_registry.py +15 -0
- pipefy-0.0.0/src/pipefy/engine/node_runner.py +75 -0
- pipefy-0.0.0/src/pipefy/engine/step_runner.py +90 -0
- pipefy-0.0.0/src/pipefy/error.py +10 -0
- pipefy-0.0.0/src/pipefy/middleware/__init__.py +5 -0
- pipefy-0.0.0/src/pipefy/middleware/error_report.py +31 -0
- pipefy-0.0.0/src/pipefy/middleware/logger.py +47 -0
- pipefy-0.0.0/src/pipefy/middleware/middleware.py +82 -0
- pipefy-0.0.0/src/pipefy/middleware/report_trace.py +81 -0
- pipefy-0.0.0/src/pipefy/middleware/validation.py +66 -0
- pipefy-0.0.0/src/pipefy/node.py +41 -0
- pipefy-0.0.0/src/pipefy/pipeline.py +66 -0
- pipefy-0.0.0/src/pipefy/report.py +268 -0
- pipefy-0.0.0/src/pipefy/sdk/__init__.py +3 -0
- pipefy-0.0.0/src/pipefy/sdk/html/page.py +14 -0
- pipefy-0.0.0/src/pipefy/sdk/html/tag.py +32 -0
- pipefy-0.0.0/src/pipefy/sdk/html/theme.py +83 -0
- pipefy-0.0.0/src/pipefy/sdk/html/themes/default.py +268 -0
- pipefy-0.0.0/src/pipefy/sdk/html/ui.py +135 -0
- pipefy-0.0.0/src/pipefy/sdk/html/ui_base.py +95 -0
- pipefy-0.0.0/src/pipefy/sdk/logger.py +68 -0
- pipefy-0.0.0/src/pipefy/sdk/reference.py +13 -0
- pipefy-0.0.0/src/pipefy/sdk/save.py +9 -0
- pipefy-0.0.0/src/pipefy/test_pipeline.py +137 -0
- pipefy-0.0.0/src/pipefy.egg-info/PKG-INFO +7 -0
- pipefy-0.0.0/src/pipefy.egg-info/SOURCES.txt +35 -0
- pipefy-0.0.0/src/pipefy.egg-info/dependency_links.txt +1 -0
- pipefy-0.0.0/src/pipefy.egg-info/requires.txt +1 -0
- pipefy-0.0.0/src/pipefy.egg-info/top_level.txt +1 -0
pipefy-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pipefy"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "A lightweight pipeline framework with validation, middleware, logging and reports."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"polars",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["setuptools>=68", "wheel"]
|
|
13
|
+
build-backend = "setuptools.build_meta"
|
|
14
|
+
|
|
15
|
+
[tool.setuptools]
|
|
16
|
+
package-dir = {"" = "src"}
|
|
17
|
+
|
|
18
|
+
[tool.setuptools_scm]
|
|
19
|
+
version_scheme = "guess-next-dev"
|
|
20
|
+
local_scheme = "no-local-version"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
where = ["src"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.package-data]
|
|
26
|
+
pipefy = ["py.typed"]
|
pipefy-0.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .context import Context, ErrorContext
|
|
2
|
+
from .node import Node
|
|
3
|
+
from .pipeline import Pipeline
|
|
4
|
+
from .error import ErrorDecision
|
|
5
|
+
from .report import PipelineErrorReport, PipelineNodeReport, PipelineReport, PipelineRetryReport, PipelineStepReport
|
|
6
|
+
from .middleware.middleware import Middleware
|
|
7
|
+
|
|
8
|
+
from .sdk import Logger, saveHTML, saveJson, getReference
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
import polars as pl
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from .sdk import logger
|
|
7
|
+
|
|
8
|
+
from .error import ErrorDecision
|
|
9
|
+
|
|
10
|
+
df_readers: dict[str, Callable[[Path], pl.DataFrame]] = {
|
|
11
|
+
".xlsx": pl.read_excel,
|
|
12
|
+
".xls": pl.read_excel,
|
|
13
|
+
".csv": pl.read_csv,
|
|
14
|
+
".parquet": pl.read_parquet,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class Context:
|
|
18
|
+
def __init__(self, log: logger.Logger):
|
|
19
|
+
self.log: logger.Logger = log
|
|
20
|
+
self.data: dict[str, Any] = {}
|
|
21
|
+
|
|
22
|
+
self.read_keys: set[str] = set()
|
|
23
|
+
self.written_keys: set[str] = set()
|
|
24
|
+
|
|
25
|
+
def begin_tracking(self) -> None:
|
|
26
|
+
self.read_keys = set()
|
|
27
|
+
self.written_keys = set()
|
|
28
|
+
|
|
29
|
+
def get(self, key: str) -> Any:
|
|
30
|
+
self.read_keys.add(key)
|
|
31
|
+
|
|
32
|
+
if key not in self.data:
|
|
33
|
+
raise KeyError(f"Context key not found: {key}")
|
|
34
|
+
|
|
35
|
+
return self.data[key]
|
|
36
|
+
|
|
37
|
+
def set(self, key: str, value: Any) -> None:
|
|
38
|
+
self.written_keys.add(key)
|
|
39
|
+
self.data[key] = value
|
|
40
|
+
|
|
41
|
+
def has(self, key: str) -> bool:
|
|
42
|
+
return key in self.data
|
|
43
|
+
|
|
44
|
+
class ErrorContext:
|
|
45
|
+
def __init__(self, ctx: Context, exc: Exception):
|
|
46
|
+
self.error = exc
|
|
47
|
+
self._ctx = ctx
|
|
48
|
+
|
|
49
|
+
def get(self, key: str):
|
|
50
|
+
return self._ctx.get(key)
|
|
51
|
+
|
|
52
|
+
def set(self, key: str, value):
|
|
53
|
+
return self._ctx.set(key, value)
|
|
54
|
+
|
|
55
|
+
def has(self, key: str):
|
|
56
|
+
return self._ctx.has(key)
|
|
57
|
+
|
|
58
|
+
def abort(self, reason: str | None = None):
|
|
59
|
+
return ErrorDecision("abort", reason or str(self.error))
|
|
60
|
+
|
|
61
|
+
def retry(self, max_retries: int = 1, reason: str | None = None):
|
|
62
|
+
return ErrorDecision("retry", reason or str(self.error), max_retries)
|
|
63
|
+
|
|
64
|
+
def continue_(self, reason: str | None = None):
|
|
65
|
+
return ErrorDecision("continue", reason or str(self.error))
|
|
66
|
+
|
|
67
|
+
def skip(self, reason: str | None = None):
|
|
68
|
+
return ErrorDecision("skip", reason or str(self.error))
|
|
69
|
+
|
|
70
|
+
def is_(self, error_type: type[Exception]) -> bool:
|
|
71
|
+
return isinstance(self.error, error_type)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
from ..context import Context
|
|
4
|
+
from ..error import ErrorDecision
|
|
5
|
+
from ..report import PipelineStepReport, PipelineNodeReport, PipelineRetryReport
|
|
6
|
+
from ..engine.middleware_engine import MiddlewareEngine
|
|
7
|
+
|
|
8
|
+
class DecisionEngine:
|
|
9
|
+
def __init__(self, middleware: MiddlewareEngine):
|
|
10
|
+
self.middleware = middleware
|
|
11
|
+
|
|
12
|
+
def finish_step(
|
|
13
|
+
self,
|
|
14
|
+
step_report: PipelineStepReport,
|
|
15
|
+
node_report: PipelineNodeReport,
|
|
16
|
+
success: bool,
|
|
17
|
+
fixed: bool | None = None,
|
|
18
|
+
) -> bool:
|
|
19
|
+
step_report.success = success
|
|
20
|
+
step_report.fixed = fixed
|
|
21
|
+
|
|
22
|
+
if not success:
|
|
23
|
+
node_report.success = False
|
|
24
|
+
|
|
25
|
+
node_report.steps.append(step_report)
|
|
26
|
+
return success
|
|
27
|
+
|
|
28
|
+
def handle(
|
|
29
|
+
self,
|
|
30
|
+
ctx: Context,
|
|
31
|
+
action: Callable[[Context], None],
|
|
32
|
+
decision: ErrorDecision,
|
|
33
|
+
step_report: PipelineStepReport,
|
|
34
|
+
node_report: PipelineNodeReport,
|
|
35
|
+
) -> bool:
|
|
36
|
+
step_report.decision = decision.action
|
|
37
|
+
step_report.decision_reason = decision.reason
|
|
38
|
+
|
|
39
|
+
handlers = {
|
|
40
|
+
"continue": self._continue,
|
|
41
|
+
"skip": self._skip,
|
|
42
|
+
"abort": self._abort,
|
|
43
|
+
"retry": self._retry,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
handler = handlers.get(decision.action)
|
|
47
|
+
|
|
48
|
+
if handler is None:
|
|
49
|
+
step_report.decision_reason = (
|
|
50
|
+
step_report.decision_reason
|
|
51
|
+
or f"Unknown error decision: {decision.action}"
|
|
52
|
+
)
|
|
53
|
+
return self.finish_step(step_report, node_report, False, False)
|
|
54
|
+
|
|
55
|
+
return handler(ctx, action, decision, step_report, node_report)
|
|
56
|
+
|
|
57
|
+
def _continue(self, ctx, action, decision, step_report, node_report):
|
|
58
|
+
return self.finish_step(step_report, node_report, True, True)
|
|
59
|
+
|
|
60
|
+
def _skip(self, ctx, action, decision, step_report, node_report):
|
|
61
|
+
return self.finish_step(step_report, node_report, True, False)
|
|
62
|
+
|
|
63
|
+
def _abort(self, ctx, action, decision, step_report, node_report):
|
|
64
|
+
return self.finish_step(step_report, node_report, False, False)
|
|
65
|
+
|
|
66
|
+
def _retry(self, ctx, action, decision, step_report, node_report):
|
|
67
|
+
max_retries = decision.max_retries or 1
|
|
68
|
+
|
|
69
|
+
for attempt in range(1, max_retries + 1):
|
|
70
|
+
retry_report = PipelineRetryReport(
|
|
71
|
+
attempt=attempt,
|
|
72
|
+
success=True,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
self.middleware.emit(
|
|
76
|
+
"beforeRetry",
|
|
77
|
+
ctx,
|
|
78
|
+
retry_report,
|
|
79
|
+
attempt,
|
|
80
|
+
max_retries,
|
|
81
|
+
action,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
action(ctx)
|
|
86
|
+
|
|
87
|
+
retry_report.success = True
|
|
88
|
+
step_report.retries.append(retry_report)
|
|
89
|
+
|
|
90
|
+
self.middleware.emit(
|
|
91
|
+
"afterRetry",
|
|
92
|
+
ctx,
|
|
93
|
+
retry_report,
|
|
94
|
+
attempt,
|
|
95
|
+
max_retries,
|
|
96
|
+
action,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return self.finish_step(step_report, node_report, True, True)
|
|
100
|
+
|
|
101
|
+
except Exception as retry_err:
|
|
102
|
+
retry_report.success = False
|
|
103
|
+
|
|
104
|
+
self.middleware.emit(
|
|
105
|
+
"onRetryError",
|
|
106
|
+
ctx,
|
|
107
|
+
retry_report,
|
|
108
|
+
retry_err,
|
|
109
|
+
attempt,
|
|
110
|
+
max_retries,
|
|
111
|
+
action,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
step_report.retries.append(retry_report)
|
|
115
|
+
|
|
116
|
+
return self.finish_step(step_report, node_report, False, False)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from ..middleware.middleware import Middleware, MiddlewareEvent
|
|
2
|
+
|
|
3
|
+
class MiddlewareEngine:
|
|
4
|
+
def __init__(self, *mw: Middleware):
|
|
5
|
+
self.middlewares = mw
|
|
6
|
+
self.listeners: dict[str, list[Middleware]] = self._index_middlewares(self.middlewares)
|
|
7
|
+
|
|
8
|
+
def _index_middlewares(self, middlewares: tuple[Middleware, ...]):
|
|
9
|
+
listeners = {}
|
|
10
|
+
|
|
11
|
+
for mw in middlewares:
|
|
12
|
+
for event in mw.events:
|
|
13
|
+
listeners.setdefault(event, []).append(mw)
|
|
14
|
+
|
|
15
|
+
return listeners
|
|
16
|
+
|
|
17
|
+
def emit(self, hook: MiddlewareEvent, *args) -> None:
|
|
18
|
+
for mw in self._listeners(hook):
|
|
19
|
+
getattr(mw, hook)(*args)
|
|
20
|
+
|
|
21
|
+
def every(self, hook: MiddlewareEvent, *args) -> bool:
|
|
22
|
+
for mw in self._listeners(hook):
|
|
23
|
+
result = getattr(mw, hook)(*args)
|
|
24
|
+
|
|
25
|
+
if result is False:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
def emit_transform(self, hook: MiddlewareEvent, value):
|
|
31
|
+
result = value
|
|
32
|
+
|
|
33
|
+
for mw in self._listeners(hook):
|
|
34
|
+
result = getattr(mw, hook)(result)
|
|
35
|
+
|
|
36
|
+
if result is None:
|
|
37
|
+
raise TypeError(f"{mw.__class__.__name__}.{hook} must return a value")
|
|
38
|
+
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
def _listeners(self, hook: MiddlewareEvent):
|
|
42
|
+
return self.listeners.get(hook, ())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from ..node import Node
|
|
4
|
+
from .middleware_engine import MiddlewareEngine
|
|
5
|
+
|
|
6
|
+
class NodeRegistry:
|
|
7
|
+
def __init__(self, middleware: MiddlewareEngine):
|
|
8
|
+
self.middleware = middleware
|
|
9
|
+
self.nodes: list[Node] = []
|
|
10
|
+
|
|
11
|
+
def push(self, n: Node) -> Self:
|
|
12
|
+
self.nodes.append(
|
|
13
|
+
self.middleware.emit_transform("onPush", n)
|
|
14
|
+
)
|
|
15
|
+
return self
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from ..context import Context
|
|
2
|
+
from ..node import Node
|
|
3
|
+
from ..report import PipelineNodeReport
|
|
4
|
+
from .step_runner import StepRunner
|
|
5
|
+
from .middleware_engine import MiddlewareEngine
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NodeRunner:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
middleware: MiddlewareEngine,
|
|
12
|
+
steps: StepRunner,
|
|
13
|
+
):
|
|
14
|
+
self.middleware = middleware
|
|
15
|
+
self.steps = steps
|
|
16
|
+
|
|
17
|
+
def run(
|
|
18
|
+
self,
|
|
19
|
+
ctx: Context,
|
|
20
|
+
current_node: Node,
|
|
21
|
+
node_report: PipelineNodeReport,
|
|
22
|
+
) -> bool:
|
|
23
|
+
self.middleware.emit("beforeRunNode", ctx, node_report, current_node)
|
|
24
|
+
|
|
25
|
+
prev_keys = set(ctx.data.keys())
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
if not self.middleware.every(
|
|
29
|
+
"validateInputs",
|
|
30
|
+
ctx,
|
|
31
|
+
current_node,
|
|
32
|
+
node_report,
|
|
33
|
+
):
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
if not self.steps.run(
|
|
37
|
+
ctx,
|
|
38
|
+
"onPreRun",
|
|
39
|
+
current_node.onPreRun,
|
|
40
|
+
current_node.onPreRunErr,
|
|
41
|
+
node_report,
|
|
42
|
+
):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
if not self.steps.run(
|
|
46
|
+
ctx,
|
|
47
|
+
"onRun",
|
|
48
|
+
current_node.onRun,
|
|
49
|
+
current_node.onRunErr,
|
|
50
|
+
node_report,
|
|
51
|
+
):
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
if not self.steps.run(
|
|
55
|
+
ctx,
|
|
56
|
+
"onPostRun",
|
|
57
|
+
current_node.onPostRun,
|
|
58
|
+
current_node.onPostRunErr,
|
|
59
|
+
node_report,
|
|
60
|
+
):
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
if not self.middleware.every(
|
|
64
|
+
"validateOutputs",
|
|
65
|
+
ctx,
|
|
66
|
+
current_node,
|
|
67
|
+
node_report,
|
|
68
|
+
prev_keys,
|
|
69
|
+
):
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
finally:
|
|
75
|
+
self.middleware.emit("afterRunNode", ctx, node_report, current_node)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
from ..context import Context, ErrorContext
|
|
4
|
+
from ..error import ErrorDecision
|
|
5
|
+
from ..report import PipelineNodeReport, PipelineStepReport
|
|
6
|
+
from ..sdk import reference
|
|
7
|
+
|
|
8
|
+
from .decision_engine import DecisionEngine
|
|
9
|
+
from .middleware_engine import MiddlewareEngine
|
|
10
|
+
|
|
11
|
+
class StepRunner:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
middleware: MiddlewareEngine,
|
|
15
|
+
decisions: DecisionEngine,
|
|
16
|
+
):
|
|
17
|
+
self.middleware = middleware
|
|
18
|
+
self.decisions = decisions
|
|
19
|
+
|
|
20
|
+
def run(
|
|
21
|
+
self,
|
|
22
|
+
ctx: Context,
|
|
23
|
+
name: str,
|
|
24
|
+
action: Callable[[Context], None],
|
|
25
|
+
error_handler: Callable[[ErrorContext], ErrorDecision],
|
|
26
|
+
node_report: PipelineNodeReport,
|
|
27
|
+
) -> bool:
|
|
28
|
+
step_report = PipelineStepReport(
|
|
29
|
+
name=name,
|
|
30
|
+
reference=reference.getReference(action),
|
|
31
|
+
success=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
self.middleware.emit("beforeRunStep", ctx, step_report, name, action)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
action(ctx)
|
|
38
|
+
|
|
39
|
+
result = self.decisions.finish_step(
|
|
40
|
+
step_report,
|
|
41
|
+
node_report,
|
|
42
|
+
True,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self.middleware.emit("afterRunStep", ctx, step_report, name, action)
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
except Exception as err:
|
|
49
|
+
err_ctx = ErrorContext(ctx, err)
|
|
50
|
+
|
|
51
|
+
self.middleware.emit("onStepError", ctx, step_report, err, error_handler)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
decision = error_handler(err_ctx)
|
|
55
|
+
|
|
56
|
+
if decision is None:
|
|
57
|
+
decision = err_ctx.abort(f"{name} must return an ErrorDecision")
|
|
58
|
+
|
|
59
|
+
except Exception as handler_err:
|
|
60
|
+
self.middleware.emit(
|
|
61
|
+
"onStepError",
|
|
62
|
+
ctx,
|
|
63
|
+
step_report,
|
|
64
|
+
handler_err,
|
|
65
|
+
error_handler,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
step_report.decision = "abort"
|
|
69
|
+
step_report.decision_reason = "error handler failed"
|
|
70
|
+
|
|
71
|
+
result = self.decisions.finish_step(
|
|
72
|
+
step_report,
|
|
73
|
+
node_report,
|
|
74
|
+
False,
|
|
75
|
+
False,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self.middleware.emit("afterRunStep", ctx, step_report, name, action)
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
result = self.decisions.handle(
|
|
82
|
+
ctx=ctx,
|
|
83
|
+
action=action,
|
|
84
|
+
decision=decision,
|
|
85
|
+
step_report=step_report,
|
|
86
|
+
node_report=node_report,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self.middleware.emit("afterRunStep", ctx, step_report, name, action)
|
|
90
|
+
return result
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from ..report import PipelineErrorReport
|
|
2
|
+
from ..sdk import getReference
|
|
3
|
+
from .middleware import Middleware
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
class ErrorReportMiddleware(Middleware):
|
|
7
|
+
events = (
|
|
8
|
+
"onStepError",
|
|
9
|
+
"onRetryError",
|
|
10
|
+
)
|
|
11
|
+
def build_error_report(self, err: Exception) -> PipelineErrorReport:
|
|
12
|
+
cause = err.__cause__ or err.__context__
|
|
13
|
+
|
|
14
|
+
return PipelineErrorReport(
|
|
15
|
+
type=type(err).__name__,
|
|
16
|
+
message=str(err),
|
|
17
|
+
traceback="".join(
|
|
18
|
+
traceback.format_exception(type(err), err, err.__traceback__)
|
|
19
|
+
),
|
|
20
|
+
cause_type=type(cause).__name__ if cause else None,
|
|
21
|
+
cause_message=str(cause) if cause else None,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def onStepError(self, ctx, r, err, error_handler):
|
|
25
|
+
r.success = False
|
|
26
|
+
r.error = self.build_error_report(err)
|
|
27
|
+
r.handler_reference = getReference(error_handler)
|
|
28
|
+
|
|
29
|
+
def onRetryError(self, ctx, r, err, attempt, max_retries, action):
|
|
30
|
+
r.success = False
|
|
31
|
+
r.error = self.build_error_report(err)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from ..sdk import getReference
|
|
2
|
+
from .middleware import Middleware
|
|
3
|
+
|
|
4
|
+
class LoggerMiddleware(Middleware):
|
|
5
|
+
events = (
|
|
6
|
+
"beforeRunPipeline",
|
|
7
|
+
"afterRunPipeline",
|
|
8
|
+
"beforeRunNode",
|
|
9
|
+
"afterRunNode",
|
|
10
|
+
"beforeRunStep",
|
|
11
|
+
"afterRunStep",
|
|
12
|
+
"onStepError",
|
|
13
|
+
"beforeRetry",
|
|
14
|
+
"afterRetry",
|
|
15
|
+
"onRetryError",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def beforeRunPipeline(self, ctx, r):
|
|
19
|
+
ctx.log.info("pipeline", "started")
|
|
20
|
+
|
|
21
|
+
def afterRunPipeline(self, ctx, r):
|
|
22
|
+
ctx.log.info("pipeline", "success" if r.success else "failed")
|
|
23
|
+
|
|
24
|
+
def beforeRunNode(self, ctx, r, n):
|
|
25
|
+
ctx.log.info(n.id, "started")
|
|
26
|
+
|
|
27
|
+
def afterRunNode(self, ctx, r, n):
|
|
28
|
+
ctx.log.info(n.id, "success" if r.success else "failed")
|
|
29
|
+
|
|
30
|
+
def beforeRunStep(self, ctx, r, name, action):
|
|
31
|
+
ctx.log.info(getReference(action), "started")
|
|
32
|
+
|
|
33
|
+
def afterRunStep(self, ctx, r, name, action):
|
|
34
|
+
ctx.log.info(getReference(action), "success" if r.success else "failed")
|
|
35
|
+
|
|
36
|
+
def onStepError(self, ctx, r, err, error_handler):
|
|
37
|
+
ctx.log.error(r.reference, type(err).__name__, str(err))
|
|
38
|
+
|
|
39
|
+
def beforeRetry(self, ctx, r, attempt, max_retries, action):
|
|
40
|
+
ctx.log.info(getReference(action), f"retry {attempt}/{max_retries}")
|
|
41
|
+
|
|
42
|
+
def afterRetry(self, ctx, r, attempt, max_retries, action):
|
|
43
|
+
ctx.log.info(getReference(action), f"retry {attempt}/{max_retries} success")
|
|
44
|
+
|
|
45
|
+
def onRetryError(self, ctx, r, err, attempt, max_retries, action):
|
|
46
|
+
ctx.log.error(getReference(action), f"retry {attempt}/{max_retries} failed:", err)
|
|
47
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Callable, Literal
|
|
3
|
+
|
|
4
|
+
from ..context import Context
|
|
5
|
+
from ..node import Node
|
|
6
|
+
from ..report import PipelineNodeReport, PipelineReport, PipelineRetryReport, PipelineStepReport
|
|
7
|
+
|
|
8
|
+
MiddlewareEvent = Literal[
|
|
9
|
+
"onPush",
|
|
10
|
+
"beforeRunPipeline",
|
|
11
|
+
"afterRunPipeline",
|
|
12
|
+
"beforeRunNode",
|
|
13
|
+
"afterRunNode",
|
|
14
|
+
"beforeRunStep",
|
|
15
|
+
"afterRunStep",
|
|
16
|
+
"onStepError",
|
|
17
|
+
"validateInputs",
|
|
18
|
+
"validateOutputs",
|
|
19
|
+
"beforeRetry",
|
|
20
|
+
"afterRetry",
|
|
21
|
+
"onRetryError",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
class Middleware(ABC):
|
|
25
|
+
events: tuple[MiddlewareEvent, ...] = (
|
|
26
|
+
"onPush",
|
|
27
|
+
"beforeRunPipeline",
|
|
28
|
+
"afterRunPipeline",
|
|
29
|
+
"beforeRunNode",
|
|
30
|
+
"afterRunNode",
|
|
31
|
+
"beforeRunStep",
|
|
32
|
+
"afterRunStep",
|
|
33
|
+
"onStepError",
|
|
34
|
+
"validateInputs",
|
|
35
|
+
"validateOutputs",
|
|
36
|
+
"beforeRetry",
|
|
37
|
+
"afterRetry",
|
|
38
|
+
"onRetryError",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def listens(self, event: MiddlewareEvent) -> bool:
|
|
42
|
+
return event in self.events
|
|
43
|
+
|
|
44
|
+
def onPush(self, n: Node) -> Node:
|
|
45
|
+
return n
|
|
46
|
+
|
|
47
|
+
def beforeRunPipeline(self, ctx: Context, r: PipelineReport) -> None:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def afterRunPipeline(self, ctx: Context, r: PipelineReport) -> None:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def beforeRunNode(self, ctx: Context, r: PipelineNodeReport, n: Node) -> None:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def afterRunNode(self, ctx: Context, r: PipelineNodeReport, n: Node) -> None:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
def beforeRunStep(self, ctx: Context, r: PipelineStepReport, name: str, action: Callable) -> None:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def afterRunStep(self, ctx: Context, r: PipelineStepReport, name: str, action: Callable) -> None:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
def onStepError(self, ctx: Context, r: PipelineStepReport, err: Exception, error_handler: Callable) -> None:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def validateInputs(self, ctx: Context, n: Node, r: PipelineNodeReport) -> bool:
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
def validateOutputs(self, ctx: Context, n: Node, r: PipelineNodeReport, prev_keys: set[str]) -> bool:
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
def beforeRetry(self, ctx: Context, r: PipelineRetryReport, attempt: int, max_retries: int, action: Callable) -> None:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def afterRetry(self, ctx: Context, r: PipelineRetryReport, attempt: int, max_retries: int, action: Callable) -> None:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def onRetryError(self, ctx: Context, r: PipelineRetryReport, err: Exception, attempt: int, max_retries: int, action: Callable) -> None:
|
|
81
|
+
pass
|
|
82
|
+
|