stageflow-framework 0.12.0__tar.gz → 0.13.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.
- {stageflow_framework-0.12.0/stageflow_framework.egg-info → stageflow_framework-0.13.0}/PKG-INFO +5 -1
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/README.md +2 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/pyproject.toml +15 -2
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/__init__.py +16 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/_args.py +10 -4
- stageflow_framework-0.13.0/stageflow/builtins/_base.py +16 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/dicts.py +4 -3
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/lists.py +23 -11
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/logic.py +8 -6
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/strings.py +11 -7
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/vars.py +11 -6
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/capabilities.py +5 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/cel.py +9 -4
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/context.py +3 -2
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/debug.py +5 -1
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/base.py +15 -7
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/bindings.py +4 -2
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/branching.py +20 -5
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/entry.py +20 -7
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/map_block.py +34 -16
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/parallel.py +14 -7
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/stage.py +27 -13
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/subpipeline.py +19 -6
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/try_block.py +31 -8
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/payload_schema.py +10 -4
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/pipeline.py +33 -16
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/policy.py +23 -12
- stageflow_framework-0.13.0/stageflow/core/registry.py +62 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/session.py +19 -4
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/spec.py +62 -7
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/stage.py +33 -3
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/typesys.py +43 -17
- stageflow_framework-0.13.0/stageflow/docs/schema.py +43 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/exceptions.py +5 -1
- stageflow_framework-0.13.0/stageflow/i18n.py +370 -0
- stageflow_framework-0.13.0/stageflow/locale/ru/LC_MESSAGES/stageflow.mo +0 -0
- stageflow_framework-0.13.0/stageflow/locale/ru/LC_MESSAGES/stageflow.po +935 -0
- stageflow_framework-0.13.0/stageflow/locale/stageflow.pot +910 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0/stageflow_framework.egg-info}/PKG-INFO +5 -1
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow_framework.egg-info/SOURCES.txt +6 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow_framework.egg-info/requires.txt +3 -0
- stageflow_framework-0.13.0/tests/test_i18n.py +396 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_try_block.py +29 -0
- stageflow_framework-0.12.0/stageflow/core/registry.py +0 -36
- stageflow_framework-0.12.0/stageflow/docs/schema.py +0 -35
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/LICENSE +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/setup.cfg +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/__init__.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/__init__.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/budget.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/event.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/inputs.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/__init__.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/recovery.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/terminal.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/docs/__init__.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/docs/schemas/pipeline.json +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/py.typed +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/testing.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow_framework.egg-info/dependency_links.txt +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow_framework.egg-info/top_level.txt +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_budget.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_budget_meters.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_budget_spending.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_capabilities.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_core_flow.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_cycles.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_debug.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_docs_schema.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_entry_node.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_full_pipeline.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_map.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_packaging.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_parallel.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_payload_validation.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_pipeline_tester.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_policy.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_removed_local_scope.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_session_control.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_session_wait_input.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_snapshot.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_std_stages.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_subpipeline.py +0 -0
- {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_typesystem.py +0 -0
{stageflow_framework-0.12.0/stageflow_framework.egg-info → stageflow_framework-0.13.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stageflow-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: StageFlow: pipeline framework for stages
|
|
5
5
|
Author-email: лень <pzrnqt1vrss@protonmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -29,6 +29,8 @@ Requires-Dist: twine; extra == "dev"
|
|
|
29
29
|
Provides-Extra: docs
|
|
30
30
|
Requires-Dist: mkdocs-material; extra == "docs"
|
|
31
31
|
Requires-Dist: mkdocs-static-i18n; extra == "docs"
|
|
32
|
+
Provides-Extra: i18n
|
|
33
|
+
Requires-Dist: babel; extra == "i18n"
|
|
32
34
|
Dynamic: license-file
|
|
33
35
|
|
|
34
36
|
<div align="center">
|
|
@@ -129,6 +131,7 @@ against it, so a wrong argument name is an error before anything runs.
|
|
|
129
131
|
| **Budgets that hold** | counters and gauges for time, steps, tokens, fan-out and depth; a ceiling a `try` block cannot catch |
|
|
130
132
|
| **Gradual typing** | declare the variables that matter; checked at validation and on every write |
|
|
131
133
|
| **Step debugging** | stop between nodes, read and edit the frame, replay the event stream |
|
|
134
|
+
| **Answers in a language** | a locale per request, not per process, for the framework's own messages; stage specs carry every language at once and let the client choose |
|
|
132
135
|
|
|
133
136
|
## Documentation
|
|
134
137
|
|
|
@@ -144,6 +147,7 @@ The reference covers the rest:
|
|
|
144
147
|
[Policy](https://leo-need-more-coffee.github.io/stageflow/policy/) ·
|
|
145
148
|
[Limits](https://leo-need-more-coffee.github.io/stageflow/limits/) ·
|
|
146
149
|
[Variable typing](https://leo-need-more-coffee.github.io/stageflow/variable-typing/) ·
|
|
150
|
+
[Localization](https://leo-need-more-coffee.github.io/stageflow/localization/) ·
|
|
147
151
|
[Session control](https://leo-need-more-coffee.github.io/stageflow/session-control/) ·
|
|
148
152
|
[Step debugging](https://leo-need-more-coffee.github.io/stageflow/step-debugging/)
|
|
149
153
|
|
|
@@ -96,6 +96,7 @@ against it, so a wrong argument name is an error before anything runs.
|
|
|
96
96
|
| **Budgets that hold** | counters and gauges for time, steps, tokens, fan-out and depth; a ceiling a `try` block cannot catch |
|
|
97
97
|
| **Gradual typing** | declare the variables that matter; checked at validation and on every write |
|
|
98
98
|
| **Step debugging** | stop between nodes, read and edit the frame, replay the event stream |
|
|
99
|
+
| **Answers in a language** | a locale per request, not per process, for the framework's own messages; stage specs carry every language at once and let the client choose |
|
|
99
100
|
|
|
100
101
|
## Documentation
|
|
101
102
|
|
|
@@ -111,6 +112,7 @@ The reference covers the rest:
|
|
|
111
112
|
[Policy](https://leo-need-more-coffee.github.io/stageflow/policy/) ·
|
|
112
113
|
[Limits](https://leo-need-more-coffee.github.io/stageflow/limits/) ·
|
|
113
114
|
[Variable typing](https://leo-need-more-coffee.github.io/stageflow/variable-typing/) ·
|
|
115
|
+
[Localization](https://leo-need-more-coffee.github.io/stageflow/localization/) ·
|
|
114
116
|
[Session control](https://leo-need-more-coffee.github.io/stageflow/session-control/) ·
|
|
115
117
|
[Step debugging](https://leo-need-more-coffee.github.io/stageflow/step-debugging/)
|
|
116
118
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "stageflow-framework"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.13.0"
|
|
8
8
|
description = "StageFlow: pipeline framework for stages"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -45,10 +45,23 @@ docs = [
|
|
|
45
45
|
"mkdocs-material",
|
|
46
46
|
"mkdocs-static-i18n",
|
|
47
47
|
]
|
|
48
|
+
# only for working ON the translations — running StageFlow in any language
|
|
49
|
+
# needs nothing beyond the compiled catalogs in the package
|
|
50
|
+
i18n = [
|
|
51
|
+
"babel",
|
|
52
|
+
]
|
|
48
53
|
|
|
49
54
|
[tool.setuptools.packages.find]
|
|
50
55
|
where = ["."]
|
|
51
56
|
include = ["stageflow*"]
|
|
52
57
|
|
|
53
58
|
[tool.setuptools.package-data]
|
|
54
|
-
"stageflow" = [
|
|
59
|
+
"stageflow" = [
|
|
60
|
+
"py.typed",
|
|
61
|
+
"docs/schemas/pipeline.json",
|
|
62
|
+
# the compiled catalogs are what the runtime reads; the .po ships next to
|
|
63
|
+
# them so a translator who has the wheel has the source too
|
|
64
|
+
"locale/*/LC_MESSAGES/*.mo",
|
|
65
|
+
"locale/*/LC_MESSAGES/*.po",
|
|
66
|
+
"locale/*.pot",
|
|
67
|
+
]
|
|
@@ -54,6 +54,15 @@ from .exceptions import (
|
|
|
54
54
|
TypeCheckError,
|
|
55
55
|
TypeDeclarationError,
|
|
56
56
|
)
|
|
57
|
+
from .i18n import (
|
|
58
|
+
SOURCE_LOCALE,
|
|
59
|
+
available_locales,
|
|
60
|
+
get_locale,
|
|
61
|
+
negotiate,
|
|
62
|
+
register_domain,
|
|
63
|
+
set_locale,
|
|
64
|
+
use_locale,
|
|
65
|
+
)
|
|
57
66
|
|
|
58
67
|
__all__ = [
|
|
59
68
|
"__version__",
|
|
@@ -96,6 +105,13 @@ __all__ = [
|
|
|
96
105
|
"parse_type",
|
|
97
106
|
"generate_stages_yaml",
|
|
98
107
|
"generate_stages_json",
|
|
108
|
+
"SOURCE_LOCALE",
|
|
109
|
+
"available_locales",
|
|
110
|
+
"get_locale",
|
|
111
|
+
"negotiate",
|
|
112
|
+
"register_domain",
|
|
113
|
+
"set_locale",
|
|
114
|
+
"use_locale",
|
|
99
115
|
"StageFlowError",
|
|
100
116
|
"RegistryError",
|
|
101
117
|
"PipelineDefinitionError",
|
|
@@ -3,12 +3,14 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from ..exceptions import StageContractError
|
|
6
|
+
from ..i18n import _
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def require_list(stage_name: str, field: str, value: Any) -> list:
|
|
9
10
|
if not isinstance(value, list):
|
|
10
11
|
raise StageContractError(
|
|
11
|
-
|
|
12
|
+
_("{stage}: '{field}' must be a list, got {got}",
|
|
13
|
+
stage=stage_name, field=field, got=type(value).__name__)
|
|
12
14
|
)
|
|
13
15
|
return value
|
|
14
16
|
|
|
@@ -16,7 +18,8 @@ def require_list(stage_name: str, field: str, value: Any) -> list:
|
|
|
16
18
|
def require_dict(stage_name: str, field: str, value: Any) -> dict:
|
|
17
19
|
if not isinstance(value, dict):
|
|
18
20
|
raise StageContractError(
|
|
19
|
-
|
|
21
|
+
_("{stage}: '{field}' must be an object, got {got}",
|
|
22
|
+
stage=stage_name, field=field, got=type(value).__name__)
|
|
20
23
|
)
|
|
21
24
|
return value
|
|
22
25
|
|
|
@@ -24,12 +27,15 @@ def require_dict(stage_name: str, field: str, value: Any) -> dict:
|
|
|
24
27
|
def require_number(stage_name: str, field: str, value: Any) -> int | float:
|
|
25
28
|
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
26
29
|
raise StageContractError(
|
|
27
|
-
|
|
30
|
+
_("{stage}: '{field}' must be a number, got {got}",
|
|
31
|
+
stage=stage_name, field=field, got=type(value).__name__)
|
|
28
32
|
)
|
|
29
33
|
return value
|
|
30
34
|
|
|
31
35
|
|
|
32
36
|
def require_present(stage_name: str, field: str, value: Any) -> Any:
|
|
33
37
|
if value is None:
|
|
34
|
-
raise StageContractError(
|
|
38
|
+
raise StageContractError(
|
|
39
|
+
_("{stage}: argument '{field}' is required", stage=stage_name, field=field)
|
|
40
|
+
)
|
|
35
41
|
return value
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""The base of the stages that ship with the framework.
|
|
2
|
+
|
|
3
|
+
It exists for one attribute. The prose of a built-in stage is the framework's
|
|
4
|
+
own text, translated in the framework's own catalog, while a host's stage is
|
|
5
|
+
translated by whoever wrote it — and the difference has to be written down
|
|
6
|
+
somewhere. Written here, it is written once.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from ..core.stage import BaseStage
|
|
12
|
+
from ..i18n import DOMAIN
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BuiltinStage(BaseStage):
|
|
16
|
+
i18n_domain = DOMAIN
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
from ..core.stage import
|
|
1
|
+
from ..core.stage import register_stage
|
|
2
2
|
from ._args import require_dict, require_list
|
|
3
|
+
from ._base import BuiltinStage
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
@register_stage("PickKeysStage")
|
|
6
|
-
class PickKeysStage(
|
|
7
|
+
class PickKeysStage(BuiltinStage):
|
|
7
8
|
"""
|
|
8
9
|
description: "Pick only specified keys from a dict and return new object"
|
|
9
10
|
icon: "⊂"
|
|
@@ -30,7 +31,7 @@ class PickKeysStage(BaseStage):
|
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
@register_stage("DropKeysStage")
|
|
33
|
-
class DropKeysStage(
|
|
34
|
+
class DropKeysStage(BuiltinStage):
|
|
34
35
|
"""
|
|
35
36
|
description: "Remove specified keys from dict and return cleaned object"
|
|
36
37
|
icon: "⊘"
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from ..core.context import Context
|
|
2
|
-
from ..core.stage import
|
|
2
|
+
from ..core.stage import register_stage
|
|
3
|
+
from ._base import BuiltinStage
|
|
3
4
|
from ..exceptions import StageContractError
|
|
5
|
+
from ..i18n import _
|
|
4
6
|
from ._args import require_list
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
@register_stage("AppendListStage")
|
|
8
|
-
class AppendListStage(
|
|
10
|
+
class AppendListStage(BuiltinStage):
|
|
9
11
|
"""
|
|
10
12
|
description: "Append value to list (creates list if missing)"
|
|
11
13
|
icon: "⊕"
|
|
@@ -29,14 +31,17 @@ class AppendListStage(BaseStage):
|
|
|
29
31
|
async def run(self):
|
|
30
32
|
args = self.get_arguments()
|
|
31
33
|
if "value" not in args:
|
|
32
|
-
raise StageContractError(
|
|
34
|
+
raise StageContractError(
|
|
35
|
+
_("{stage}: argument '{field}' is required",
|
|
36
|
+
stage="AppendListStage", field="value")
|
|
37
|
+
)
|
|
33
38
|
value = args["value"]
|
|
34
39
|
base = require_list("AppendListStage", "list", args.get("list") or [])
|
|
35
40
|
self.set_outputs({"list": [*base, value]})
|
|
36
41
|
|
|
37
42
|
|
|
38
43
|
@register_stage("ExtendListStage")
|
|
39
|
-
class ExtendListStage(
|
|
44
|
+
class ExtendListStage(BuiltinStage):
|
|
40
45
|
"""
|
|
41
46
|
description: "Extend list with items from arguments"
|
|
42
47
|
icon: "⊞"
|
|
@@ -63,13 +68,14 @@ class ExtendListStage(BaseStage):
|
|
|
63
68
|
items = args.get("items", [])
|
|
64
69
|
if not isinstance(items, (list, tuple)):
|
|
65
70
|
raise StageContractError(
|
|
66
|
-
|
|
71
|
+
_("{stage}: '{field}' must be a list, got {got}",
|
|
72
|
+
stage="ExtendListStage", field="items", got=type(items).__name__)
|
|
67
73
|
)
|
|
68
74
|
self.set_outputs({"list": [*base, *items]})
|
|
69
75
|
|
|
70
76
|
|
|
71
77
|
@register_stage("FilterListStage")
|
|
72
|
-
class FilterListStage(
|
|
78
|
+
class FilterListStage(BuiltinStage):
|
|
73
79
|
"""
|
|
74
80
|
description: "Filter list items by CEL condition; current element is bound as `item`"
|
|
75
81
|
icon: "▽"
|
|
@@ -95,7 +101,10 @@ class FilterListStage(BaseStage):
|
|
|
95
101
|
args = self.get_arguments()
|
|
96
102
|
condition = args.pop("condition", None)
|
|
97
103
|
if condition is None:
|
|
98
|
-
raise StageContractError(
|
|
104
|
+
raise StageContractError(
|
|
105
|
+
_("{stage}: argument '{field}' is required",
|
|
106
|
+
stage="FilterListStage", field="condition")
|
|
107
|
+
)
|
|
99
108
|
items = require_list("FilterListStage", "items", args.pop("items", []))
|
|
100
109
|
scope = Context(vars=args)
|
|
101
110
|
result = [item for item in items if self.session.cel.eval(condition, scope, item=item)]
|
|
@@ -103,7 +112,7 @@ class FilterListStage(BaseStage):
|
|
|
103
112
|
|
|
104
113
|
|
|
105
114
|
@register_stage("UniqueListStage")
|
|
106
|
-
class UniqueListStage(
|
|
115
|
+
class UniqueListStage(BuiltinStage):
|
|
107
116
|
"""
|
|
108
117
|
description: "Deduplicate list while preserving original order"
|
|
109
118
|
icon: "≠"
|
|
@@ -131,7 +140,7 @@ class UniqueListStage(BaseStage):
|
|
|
131
140
|
|
|
132
141
|
|
|
133
142
|
@register_stage("PopListStage")
|
|
134
|
-
class PopListStage(
|
|
143
|
+
class PopListStage(BuiltinStage):
|
|
135
144
|
"""
|
|
136
145
|
description: "Pop element from list (default last) and return list+popped value"
|
|
137
146
|
icon: "⊟"
|
|
@@ -159,13 +168,16 @@ class PopListStage(BaseStage):
|
|
|
159
168
|
args = self.get_arguments()
|
|
160
169
|
items = require_list("PopListStage", "items", args.get("items", []))
|
|
161
170
|
if not items:
|
|
162
|
-
raise StageContractError(
|
|
171
|
+
raise StageContractError(
|
|
172
|
+
_("PopListStage: cannot pop from an empty list")
|
|
173
|
+
)
|
|
163
174
|
index = args.get("index", -1)
|
|
164
175
|
remaining = list(items)
|
|
165
176
|
try:
|
|
166
177
|
popped = remaining.pop(index)
|
|
167
178
|
except IndexError:
|
|
168
179
|
raise StageContractError(
|
|
169
|
-
|
|
180
|
+
_("PopListStage: index {index} is out of range for a list of "
|
|
181
|
+
"length {length}", index=index, length=len(items))
|
|
170
182
|
) from None
|
|
171
183
|
self.set_outputs({"list": remaining, "popped": popped})
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
3
|
from ..core.context import Context
|
|
4
|
-
from ..core.stage import
|
|
4
|
+
from ..core.stage import register_stage
|
|
5
|
+
from ..i18n import _
|
|
5
6
|
from ._args import require_number, require_present
|
|
7
|
+
from ._base import BuiltinStage
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
@register_stage("AssertStage")
|
|
9
|
-
class AssertStage(
|
|
11
|
+
class AssertStage(BuiltinStage):
|
|
10
12
|
"""
|
|
11
13
|
description: "Validate CEL condition against stage arguments, raise on failure"
|
|
12
14
|
icon: "✓"
|
|
@@ -30,14 +32,14 @@ class AssertStage(BaseStage):
|
|
|
30
32
|
async def run(self):
|
|
31
33
|
args = self.get_arguments()
|
|
32
34
|
condition = require_present("AssertStage", "condition", args.pop("condition", None))
|
|
33
|
-
message = args.pop("message", "assertion failed")
|
|
35
|
+
message = args.pop("message", None) or _("assertion failed")
|
|
34
36
|
scope = Context(vars=args)
|
|
35
37
|
if not self.session.cel.eval(condition, scope):
|
|
36
38
|
raise AssertionError(message)
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
@register_stage("FailStage")
|
|
40
|
-
class FailStage(
|
|
42
|
+
class FailStage(BuiltinStage):
|
|
41
43
|
"""
|
|
42
44
|
description: "Always raise a runtime error with provided message"
|
|
43
45
|
icon: "✕"
|
|
@@ -57,7 +59,7 @@ class FailStage(BaseStage):
|
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
@register_stage("LogStage")
|
|
60
|
-
class LogStage(
|
|
62
|
+
class LogStage(BuiltinStage):
|
|
61
63
|
"""
|
|
62
64
|
description: "Emit log event with message and the stage's own arguments"
|
|
63
65
|
icon: "≡"
|
|
@@ -80,7 +82,7 @@ class LogStage(BaseStage):
|
|
|
80
82
|
|
|
81
83
|
|
|
82
84
|
@register_stage("SleepStage")
|
|
83
|
-
class SleepStage(
|
|
85
|
+
class SleepStage(BuiltinStage):
|
|
84
86
|
"""
|
|
85
87
|
description: "Async sleep for the given number of seconds"
|
|
86
88
|
icon: "⏱"
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import string
|
|
2
2
|
|
|
3
|
-
from ..core.stage import
|
|
3
|
+
from ..core.stage import register_stage
|
|
4
|
+
from ._base import BuiltinStage
|
|
4
5
|
from ..exceptions import StageContractError
|
|
6
|
+
from ..i18n import _
|
|
5
7
|
from ._args import require_list, require_present
|
|
6
8
|
|
|
7
9
|
|
|
@@ -9,19 +11,21 @@ class _NameOnlyFormatter(string.Formatter):
|
|
|
9
11
|
def get_field(self, field_name, args, kwargs):
|
|
10
12
|
if not field_name.isidentifier():
|
|
11
13
|
raise StageContractError(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
_("TemplateStage: placeholder '{{{field}}}' is not an argument name; "
|
|
15
|
+
"attributes, indexes and positional numbers are not allowed",
|
|
16
|
+
field=field_name)
|
|
14
17
|
)
|
|
15
18
|
if field_name not in kwargs:
|
|
16
19
|
raise StageContractError(
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
_("TemplateStage: template references '{field}', "
|
|
21
|
+
"but there is no such argument (available: {available})",
|
|
22
|
+
field=field_name, available=sorted(kwargs))
|
|
19
23
|
)
|
|
20
24
|
return kwargs[field_name], field_name
|
|
21
25
|
|
|
22
26
|
|
|
23
27
|
@register_stage("ConcatStage")
|
|
24
|
-
class ConcatStage(
|
|
28
|
+
class ConcatStage(BuiltinStage):
|
|
25
29
|
"""
|
|
26
30
|
description: "Concatenate stringified parts with separator"
|
|
27
31
|
icon: "⧺"
|
|
@@ -50,7 +54,7 @@ class ConcatStage(BaseStage):
|
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
@register_stage("TemplateStage")
|
|
53
|
-
class TemplateStage(
|
|
57
|
+
class TemplateStage(BuiltinStage):
|
|
54
58
|
"""
|
|
55
59
|
description: "Format template string with the stage's own arguments"
|
|
56
60
|
icon: "{}"
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
from ..core.stage import
|
|
1
|
+
from ..core.stage import register_stage
|
|
2
|
+
from ._base import BuiltinStage
|
|
2
3
|
from ..exceptions import StageContractError
|
|
4
|
+
from ..i18n import _
|
|
3
5
|
from ._args import require_dict, require_number
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
@register_stage("SetValueStage")
|
|
7
|
-
class SetValueStage(
|
|
9
|
+
class SetValueStage(BuiltinStage):
|
|
8
10
|
"""
|
|
9
11
|
description: "Set value from arguments to the target path"
|
|
10
12
|
icon: "="
|
|
@@ -23,12 +25,15 @@ class SetValueStage(BaseStage):
|
|
|
23
25
|
async def run(self):
|
|
24
26
|
args = self.get_arguments()
|
|
25
27
|
if "value" not in args:
|
|
26
|
-
raise StageContractError(
|
|
28
|
+
raise StageContractError(
|
|
29
|
+
_("{stage}: argument '{field}' is required",
|
|
30
|
+
stage="SetValueStage", field="value")
|
|
31
|
+
)
|
|
27
32
|
self.set_outputs({"value": args["value"]})
|
|
28
33
|
|
|
29
34
|
|
|
30
35
|
@register_stage("CopyValueStage")
|
|
31
|
-
class CopyValueStage(
|
|
36
|
+
class CopyValueStage(BuiltinStage):
|
|
32
37
|
"""
|
|
33
38
|
description: "Copy value to output path, falling back to default when absent"
|
|
34
39
|
icon: "⧉"
|
|
@@ -57,7 +62,7 @@ class CopyValueStage(BaseStage):
|
|
|
57
62
|
|
|
58
63
|
|
|
59
64
|
@register_stage("IncrementStage")
|
|
60
|
-
class IncrementStage(
|
|
65
|
+
class IncrementStage(BuiltinStage):
|
|
61
66
|
"""
|
|
62
67
|
description: "Increment numeric value by delta"
|
|
63
68
|
icon: "+"
|
|
@@ -86,7 +91,7 @@ class IncrementStage(BaseStage):
|
|
|
86
91
|
|
|
87
92
|
|
|
88
93
|
@register_stage("MergeDictStage")
|
|
89
|
-
class MergeDictStage(
|
|
94
|
+
class MergeDictStage(BuiltinStage):
|
|
90
95
|
"""
|
|
91
96
|
description: "Shallow merge src dict into dst"
|
|
92
97
|
icon: "⋈"
|
|
@@ -6,6 +6,7 @@ from typing import Any
|
|
|
6
6
|
from .core.nodes import get_node_types
|
|
7
7
|
from .core.policy import Policy
|
|
8
8
|
from .core.stage import get_stages
|
|
9
|
+
from .i18n import available_locales
|
|
9
10
|
|
|
10
11
|
DISTRIBUTION = "stageflow-framework"
|
|
11
12
|
|
|
@@ -38,6 +39,9 @@ def capabilities(policy: Policy | None = None) -> dict[str, Any]:
|
|
|
38
39
|
- `stages` — how many stages may be used. The specs themselves are a
|
|
39
40
|
separate, much larger answer (`get_stages`), so this is only a hint that
|
|
40
41
|
a registry is there at all.
|
|
42
|
+
- `locales` — the languages the core can put its own text into, the source
|
|
43
|
+
language first. A client cannot guess this: a catalog is a file in the
|
|
44
|
+
build, so the build has to say.
|
|
41
45
|
|
|
42
46
|
With a `policy` the answer narrows to what that policy allows, which is
|
|
43
47
|
what a backend should serve to a client: an editor needs to know what
|
|
@@ -59,4 +63,5 @@ def capabilities(policy: Policy | None = None) -> dict[str, Any]:
|
|
|
59
63
|
"stageflow": __version__,
|
|
60
64
|
"node_types": node_types,
|
|
61
65
|
"stages": len(stages),
|
|
66
|
+
"locales": available_locales(),
|
|
62
67
|
}
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
5
|
from ..exceptions import ExpressionError
|
|
6
|
+
from ..i18n import _
|
|
6
7
|
from .context import Context
|
|
7
8
|
|
|
8
9
|
CelError = ExpressionError
|
|
@@ -28,8 +29,8 @@ class CelEngine:
|
|
|
28
29
|
self._compiled: dict[str, Any] = {}
|
|
29
30
|
if _BACKEND is None: # pragma: no cover
|
|
30
31
|
raise ExpressionError(
|
|
31
|
-
"No CEL backend found. Install "
|
|
32
|
-
|
|
32
|
+
_("No CEL backend found. Install "
|
|
33
|
+
"'common-expression-language' (recommended) or 'cel-python'.")
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
@property
|
|
@@ -46,7 +47,9 @@ class CelEngine:
|
|
|
46
47
|
env = _celpy.Environment()
|
|
47
48
|
program = env.program(env.compile(expr))
|
|
48
49
|
except Exception as exc: # noqa: BLE001
|
|
49
|
-
raise ExpressionError(
|
|
50
|
+
raise ExpressionError(
|
|
51
|
+
_("Cannot compile CEL {expr}: {reason}", expr=repr(expr), reason=exc)
|
|
52
|
+
) from exc
|
|
50
53
|
self._compiled[expr] = program
|
|
51
54
|
return program
|
|
52
55
|
|
|
@@ -75,4 +78,6 @@ class CelEngine:
|
|
|
75
78
|
return program.execute(activation)
|
|
76
79
|
return program.evaluate(activation) # pragma: no cover
|
|
77
80
|
except Exception as exc: # noqa: BLE001
|
|
78
|
-
raise ExpressionError(
|
|
81
|
+
raise ExpressionError(
|
|
82
|
+
_("Cannot evaluate CEL {expr}: {reason}", expr=repr(expr), reason=exc)
|
|
83
|
+
) from exc
|
|
@@ -5,6 +5,7 @@ from typing import Any
|
|
|
5
5
|
import immutables
|
|
6
6
|
|
|
7
7
|
from ..exceptions import PipelineDefinitionError
|
|
8
|
+
from ..i18n import _
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class Context:
|
|
@@ -39,8 +40,8 @@ class Context:
|
|
|
39
40
|
def from_dict(cls, data: dict[str, Any]) -> "Context":
|
|
40
41
|
if "local" in data and "vars" not in data:
|
|
41
42
|
raise PipelineDefinitionError(
|
|
42
|
-
"Snapshot contains the 'local' scope (pre-0.7.0); "
|
|
43
|
-
|
|
43
|
+
_("Snapshot contains the 'local' scope (pre-0.7.0); "
|
|
44
|
+
"the frame is now called 'vars'")
|
|
44
45
|
)
|
|
45
46
|
return cls(vars=data.get("vars", {}))
|
|
46
47
|
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import threading
|
|
5
5
|
from typing import Any, Callable
|
|
6
6
|
|
|
7
|
+
from ..i18n import _
|
|
7
8
|
from .context import Context
|
|
8
9
|
|
|
9
10
|
RUN = "run"
|
|
@@ -124,7 +125,10 @@ class StepDebugger:
|
|
|
124
125
|
for name, value in values.items():
|
|
125
126
|
try:
|
|
126
127
|
if types is not None:
|
|
127
|
-
types.check_write(
|
|
128
|
+
types.check_write(
|
|
129
|
+
name, value,
|
|
130
|
+
_("debugger before node '{node}'", node=self.node),
|
|
131
|
+
)
|
|
128
132
|
except Exception as exc: # noqa: BLE001
|
|
129
133
|
self._emit("var_rejected", name=name, error=str(exc))
|
|
130
134
|
continue
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from ...exceptions import PipelineDefinitionError
|
|
6
|
+
from ...i18n import _
|
|
6
7
|
from ..context import Context
|
|
7
8
|
from ..registry import Registry
|
|
8
9
|
from .recovery import Retrier
|
|
@@ -46,7 +47,9 @@ class Node:
|
|
|
46
47
|
def from_dict(data: dict) -> "Node":
|
|
47
48
|
node_type = data.get("type")
|
|
48
49
|
if node_type not in _node_types:
|
|
49
|
-
raise PipelineDefinitionError(
|
|
50
|
+
raise PipelineDefinitionError(
|
|
51
|
+
_("Unknown node type: {type}", type=repr(node_type))
|
|
52
|
+
)
|
|
50
53
|
return _node_types.get(node_type)._parse(data)
|
|
51
54
|
|
|
52
55
|
@classmethod
|
|
@@ -77,11 +80,14 @@ class Node:
|
|
|
77
80
|
errors: list[str] = []
|
|
78
81
|
for src, dst in self.expose.items():
|
|
79
82
|
malformed = False
|
|
80
|
-
|
|
83
|
+
# a whole sentence per side rather than the English noun dropped
|
|
84
|
+
# into one — see the reason in `registry.py`
|
|
85
|
+
for path, message in (
|
|
86
|
+
(src, _("{node}: expose source '{path}' must be a variable name")),
|
|
87
|
+
(dst, _("{node}: expose destination '{path}' must be a variable name")),
|
|
88
|
+
):
|
|
81
89
|
if not isinstance(path, str) or not path.isidentifier():
|
|
82
|
-
errors.append(
|
|
83
|
-
f"{self.id}: expose {side} '{path}' must be a variable name"
|
|
84
|
-
)
|
|
90
|
+
errors.append(message.format(node=self.id, path=path))
|
|
85
91
|
malformed = True
|
|
86
92
|
if malformed:
|
|
87
93
|
continue
|
|
@@ -90,8 +96,10 @@ class Node:
|
|
|
90
96
|
dst_type = ts.declared(dst)
|
|
91
97
|
if src_type and dst_type and not ts.expose_compatible(src_type, dst_type):
|
|
92
98
|
errors.append(
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
_("{node}: expose {source} -> {target}: incompatible types "
|
|
100
|
+
"'{source_type}' and '{target_type}'",
|
|
101
|
+
node=self.id, source=src, target=dst,
|
|
102
|
+
source_type=src_type, target_type=dst_type)
|
|
95
103
|
)
|
|
96
104
|
return errors
|
|
97
105
|
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
from ...exceptions import PipelineDefinitionError, StageOutputError
|
|
6
|
+
from ...i18n import _
|
|
6
7
|
from ..context import Context
|
|
7
8
|
|
|
8
9
|
if TYPE_CHECKING: # pragma: no cover
|
|
@@ -20,7 +21,7 @@ def normalize_bucket(bucket: Any) -> dict[str, str]:
|
|
|
20
21
|
if isinstance(bucket, dict):
|
|
21
22
|
return dict(bucket)
|
|
22
23
|
raise PipelineDefinitionError(
|
|
23
|
-
|
|
24
|
+
_("Bucket must be a list or an object, got {got}", got=type(bucket).__name__)
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
|
|
@@ -75,7 +76,8 @@ def apply_outputs(
|
|
|
75
76
|
dest = spec
|
|
76
77
|
if key not in output_ns:
|
|
77
78
|
raise StageOutputError(
|
|
78
|
-
|
|
79
|
+
_("Stage did not return field '{field}' (available: {available})",
|
|
80
|
+
field=key, available=sorted(output_ns))
|
|
79
81
|
)
|
|
80
82
|
value = output_ns[key]
|
|
81
83
|
writes.append((dest, value))
|