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.
Files changed (84) hide show
  1. {stageflow_framework-0.12.0/stageflow_framework.egg-info → stageflow_framework-0.13.0}/PKG-INFO +5 -1
  2. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/README.md +2 -0
  3. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/pyproject.toml +15 -2
  4. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/__init__.py +16 -0
  5. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/_args.py +10 -4
  6. stageflow_framework-0.13.0/stageflow/builtins/_base.py +16 -0
  7. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/dicts.py +4 -3
  8. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/lists.py +23 -11
  9. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/logic.py +8 -6
  10. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/strings.py +11 -7
  11. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/vars.py +11 -6
  12. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/capabilities.py +5 -0
  13. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/cel.py +9 -4
  14. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/context.py +3 -2
  15. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/debug.py +5 -1
  16. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/base.py +15 -7
  17. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/bindings.py +4 -2
  18. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/branching.py +20 -5
  19. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/entry.py +20 -7
  20. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/map_block.py +34 -16
  21. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/parallel.py +14 -7
  22. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/stage.py +27 -13
  23. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/subpipeline.py +19 -6
  24. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/try_block.py +31 -8
  25. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/payload_schema.py +10 -4
  26. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/pipeline.py +33 -16
  27. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/policy.py +23 -12
  28. stageflow_framework-0.13.0/stageflow/core/registry.py +62 -0
  29. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/session.py +19 -4
  30. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/spec.py +62 -7
  31. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/stage.py +33 -3
  32. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/typesys.py +43 -17
  33. stageflow_framework-0.13.0/stageflow/docs/schema.py +43 -0
  34. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/exceptions.py +5 -1
  35. stageflow_framework-0.13.0/stageflow/i18n.py +370 -0
  36. stageflow_framework-0.13.0/stageflow/locale/ru/LC_MESSAGES/stageflow.mo +0 -0
  37. stageflow_framework-0.13.0/stageflow/locale/ru/LC_MESSAGES/stageflow.po +935 -0
  38. stageflow_framework-0.13.0/stageflow/locale/stageflow.pot +910 -0
  39. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0/stageflow_framework.egg-info}/PKG-INFO +5 -1
  40. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow_framework.egg-info/SOURCES.txt +6 -0
  41. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow_framework.egg-info/requires.txt +3 -0
  42. stageflow_framework-0.13.0/tests/test_i18n.py +396 -0
  43. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_try_block.py +29 -0
  44. stageflow_framework-0.12.0/stageflow/core/registry.py +0 -36
  45. stageflow_framework-0.12.0/stageflow/docs/schema.py +0 -35
  46. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/LICENSE +0 -0
  47. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/setup.cfg +0 -0
  48. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/builtins/__init__.py +0 -0
  49. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/__init__.py +0 -0
  50. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/budget.py +0 -0
  51. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/event.py +0 -0
  52. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/inputs.py +0 -0
  53. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/__init__.py +0 -0
  54. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/recovery.py +0 -0
  55. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/core/nodes/terminal.py +0 -0
  56. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/docs/__init__.py +0 -0
  57. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/docs/schemas/pipeline.json +0 -0
  58. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/py.typed +0 -0
  59. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow/testing.py +0 -0
  60. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow_framework.egg-info/dependency_links.txt +0 -0
  61. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/stageflow_framework.egg-info/top_level.txt +0 -0
  62. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_budget.py +0 -0
  63. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_budget_meters.py +0 -0
  64. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_budget_spending.py +0 -0
  65. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_capabilities.py +0 -0
  66. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_core_flow.py +0 -0
  67. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_cycles.py +0 -0
  68. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_debug.py +0 -0
  69. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_docs_schema.py +0 -0
  70. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_entry_node.py +0 -0
  71. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_full_pipeline.py +0 -0
  72. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_map.py +0 -0
  73. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_packaging.py +0 -0
  74. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_parallel.py +0 -0
  75. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_payload_validation.py +0 -0
  76. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_pipeline_tester.py +0 -0
  77. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_policy.py +0 -0
  78. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_removed_local_scope.py +0 -0
  79. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_session_control.py +0 -0
  80. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_session_wait_input.py +0 -0
  81. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_snapshot.py +0 -0
  82. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_std_stages.py +0 -0
  83. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_subpipeline.py +0 -0
  84. {stageflow_framework-0.12.0 → stageflow_framework-0.13.0}/tests/test_typesystem.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stageflow-framework
3
- Version: 0.12.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.12.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" = ["py.typed", "docs/schemas/pipeline.json"]
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
- f"{stage_name}: '{field}' must be a list, got {type(value).__name__}"
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
- f"{stage_name}: '{field}' must be an object, got {type(value).__name__}"
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
- f"{stage_name}: '{field}' must be a number, got {type(value).__name__}"
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(f"{stage_name}: argument '{field}' is required")
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 BaseStage, register_stage
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(BaseStage):
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(BaseStage):
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 BaseStage, register_stage
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(BaseStage):
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("AppendListStage: argument 'value' is required")
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(BaseStage):
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
- f"ExtendListStage: 'items' must be a list, got {type(items).__name__}"
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(BaseStage):
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("FilterListStage: argument 'condition' is required")
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(BaseStage):
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(BaseStage):
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("PopListStage: cannot pop from an empty list")
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
- f"PopListStage: index {index} is out of range for a list of length {len(items)}"
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 BaseStage, register_stage
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(BaseStage):
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(BaseStage):
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(BaseStage):
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(BaseStage):
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 BaseStage, register_stage
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
- f"TemplateStage: placeholder '{{{field_name}}}' is not an argument name; "
13
- "attributes, indexes and positional numbers are not allowed"
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
- f"TemplateStage: template references '{field_name}', "
18
- f"but there is no such argument (available: {sorted(kwargs)})"
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(BaseStage):
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(BaseStage):
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 BaseStage, register_stage
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(BaseStage):
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("SetValueStage: argument 'value' is required")
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(BaseStage):
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(BaseStage):
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(BaseStage):
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
- "'common-expression-language' (recommended) or 'cel-python'."
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(f"Cannot compile CEL {expr!r}: {exc}") from exc
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(f"Cannot evaluate CEL {expr!r}: {exc}") from exc
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
- "the frame is now called 'vars'"
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(name, value, f"debugger before node '{self.node}'")
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(f"Unknown node type: {node_type!r}")
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
- for path, side in ((src, "source"), (dst, "destination")):
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
- f"{self.id}: expose {src} -> {dst}: incompatible types "
94
- f"'{src_type}' and '{dst_type}'"
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
- f"Bucket must be a list or an object, got {type(bucket).__name__}"
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
- f"Stage did not return field '{key}' (available: {sorted(output_ns)})"
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))