simplibs-actions 0.1.0__py3-none-any.whl
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.
- simplibs/actions/__init__.py +103 -0
- simplibs/actions/base_class/Action.py +188 -0
- simplibs/actions/base_class/__init__.py +17 -0
- simplibs/actions/base_class/_validations/__init__.py +16 -0
- simplibs/actions/base_class/_validations/raise_invalid_action_target.py +54 -0
- simplibs/actions/containers/__init__.py +88 -0
- simplibs/actions/containers/_helpers/__init__.py +18 -0
- simplibs/actions/containers/_helpers/as_action.py +31 -0
- simplibs/actions/containers/_helpers/as_predicate.py +58 -0
- simplibs/actions/containers/_helpers/validations/__init__.py +16 -0
- simplibs/actions/containers/_helpers/validations/raise_invalid_predicate.py +42 -0
- simplibs/actions/containers/flow_control/__init__.py +21 -0
- simplibs/actions/containers/flow_control/_validations/__init__.py +18 -0
- simplibs/actions/containers/flow_control/_validations/raise_guard_failed.py +42 -0
- simplibs/actions/containers/flow_control/_validations/raise_not_iterable.py +39 -0
- simplibs/actions/containers/flow_control/aliases/__init__.py +24 -0
- simplibs/actions/containers/flow_control/aliases/apply_to_each.py +19 -0
- simplibs/actions/containers/flow_control/aliases/conditional_action.py +24 -0
- simplibs/actions/containers/flow_control/aliases/guarded_action.py +20 -0
- simplibs/actions/containers/flow_control/branch.py +77 -0
- simplibs/actions/containers/flow_control/for_each.py +58 -0
- simplibs/actions/containers/flow_control/guard.py +96 -0
- simplibs/actions/containers/operators/__init__.py +22 -0
- simplibs/actions/containers/operators/aliases/__init__.py +24 -0
- simplibs/actions/containers/operators/aliases/run_in_parallel.py +20 -0
- simplibs/actions/containers/operators/aliases/run_in_sequence.py +20 -0
- simplibs/actions/containers/operators/aliases/try_or_fallback.py +27 -0
- simplibs/actions/containers/operators/compose_class/ParallelCompose.py +53 -0
- simplibs/actions/containers/operators/compose_class/SequenceCompose.py +59 -0
- simplibs/actions/containers/operators/compose_class/__init__.py +23 -0
- simplibs/actions/containers/operators/fallback.py +88 -0
- simplibs/actions/containers/operators/parallel.py +66 -0
- simplibs/actions/containers/operators/sequence.py +74 -0
- simplibs/actions/containers/primitives/__init__.py +19 -0
- simplibs/actions/containers/primitives/aliases/__init__.py +22 -0
- simplibs/actions/containers/primitives/aliases/pass_through.py +19 -0
- simplibs/actions/containers/primitives/aliases/replace_with.py +19 -0
- simplibs/actions/containers/primitives/constant.py +46 -0
- simplibs/actions/containers/primitives/identity.py +53 -0
- simplibs/actions/containers/wrappers/__init__.py +23 -0
- simplibs/actions/containers/wrappers/_validations/__init__.py +16 -0
- simplibs/actions/containers/wrappers/_validations/raise_invalid_attempts.py +40 -0
- simplibs/actions/containers/wrappers/aliases/__init__.py +26 -0
- simplibs/actions/containers/wrappers/aliases/callable_action.py +21 -0
- simplibs/actions/containers/wrappers/aliases/log_step.py +21 -0
- simplibs/actions/containers/wrappers/aliases/retry_on_failure.py +23 -0
- simplibs/actions/containers/wrappers/aliases/run_side_effect.py +21 -0
- simplibs/actions/containers/wrappers/lambda_action.py +55 -0
- simplibs/actions/containers/wrappers/log_action.py +58 -0
- simplibs/actions/containers/wrappers/retry.py +82 -0
- simplibs/actions/containers/wrappers/tap.py +55 -0
- simplibs/actions/creator/__init__.py +26 -0
- simplibs/actions/creator/_helpers/__init__.py +16 -0
- simplibs/actions/creator/_helpers/constants/EMPTY.py +42 -0
- simplibs/actions/creator/_helpers/constants/__init__.py +16 -0
- simplibs/actions/creator/_helpers/creators/__init__.py +20 -0
- simplibs/actions/creator/_helpers/creators/_helpers/__init__.py +24 -0
- simplibs/actions/creator/_helpers/creators/_helpers/_describe_annotation.py +53 -0
- simplibs/actions/creator/_helpers/creators/_helpers/build_call_docstring.py +68 -0
- simplibs/actions/creator/_helpers/creators/_helpers/build_init_docstring.py +56 -0
- simplibs/actions/creator/_helpers/creators/create_act.py +145 -0
- simplibs/actions/creator/_helpers/creators/create_call.py +102 -0
- simplibs/actions/creator/_helpers/creators/create_init_and_slots.py +103 -0
- simplibs/actions/creator/_helpers/resolvers/__init__.py +23 -0
- simplibs/actions/creator/_helpers/resolvers/resolve_class_name.py +54 -0
- simplibs/actions/creator/_helpers/resolvers/resolve_param.py +77 -0
- simplibs/actions/creator/_helpers/resolvers/unwrap_log_this_and_validate_call.py +72 -0
- simplibs/actions/creator/_helpers/resolvers/validations/__init__.py +18 -0
- simplibs/actions/creator/_helpers/resolvers/validations/raise_main_param_not_found.py +39 -0
- simplibs/actions/creator/_helpers/resolvers/validations/raise_no_parameters.py +37 -0
- simplibs/actions/creator/_helpers/validations/__init__.py +23 -0
- simplibs/actions/creator/_helpers/validations/_verify_type_is_validatable.py +48 -0
- simplibs/actions/creator/_helpers/validations/raise_action_instance_error.py +32 -0
- simplibs/actions/creator/_helpers/validations/validate_param_kinds.py +46 -0
- simplibs/actions/creator/_helpers/validations/validate_params_annotations.py +45 -0
- simplibs/actions/creator/_helpers/validations/validate_return_annotations.py +42 -0
- simplibs/actions/creator/create_action.py +229 -0
- simplibs/actions/decorator/__init__.py +26 -0
- simplibs/actions/decorator/to_action.py +109 -0
- simplibs/actions/testing/__init__.py +50 -0
- simplibs/actions/testing/assert_action.py +195 -0
- simplibs/actions/testing/assert_action_alias.py +102 -0
- simplibs/actions/testing/asserts/__init__.py +31 -0
- simplibs/actions/testing/asserts/assert_action_construction.py +105 -0
- simplibs/actions/testing/asserts/assert_action_io_types.py +80 -0
- simplibs/actions/testing/asserts/assert_action_output.py +74 -0
- simplibs/actions/testing/asserts/assert_action_raises.py +80 -0
- simplibs_actions-0.1.0.dist-info/METADATA +374 -0
- simplibs_actions-0.1.0.dist-info/RECORD +92 -0
- simplibs_actions-0.1.0.dist-info/WHEEL +5 -0
- simplibs_actions-0.1.0.dist-info/licenses/LICENSE +21 -0
- simplibs_actions-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# 1. Base Class
|
|
2
|
+
from .base_class import Action
|
|
3
|
+
|
|
4
|
+
# 2. Composition Classes
|
|
5
|
+
from .containers.operators.compose_class import ParallelCompose, SequenceCompose
|
|
6
|
+
|
|
7
|
+
# 3. Primary Containers & Aggregated Aliases Namespace
|
|
8
|
+
from .containers import (
|
|
9
|
+
aliases,
|
|
10
|
+
branch,
|
|
11
|
+
constant,
|
|
12
|
+
fallback,
|
|
13
|
+
for_each,
|
|
14
|
+
guard,
|
|
15
|
+
identity,
|
|
16
|
+
lambda_action,
|
|
17
|
+
log_action,
|
|
18
|
+
parallel,
|
|
19
|
+
retry,
|
|
20
|
+
sequence,
|
|
21
|
+
tap,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# 4. Creators & Decorators
|
|
25
|
+
from .creator import create_action
|
|
26
|
+
from .decorator import to_action
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Base
|
|
31
|
+
"Action",
|
|
32
|
+
# Composition Classes
|
|
33
|
+
"ParallelCompose",
|
|
34
|
+
"SequenceCompose",
|
|
35
|
+
# Flow Control Containers
|
|
36
|
+
"branch",
|
|
37
|
+
"for_each",
|
|
38
|
+
"guard",
|
|
39
|
+
# Operator Containers
|
|
40
|
+
"fallback",
|
|
41
|
+
"parallel",
|
|
42
|
+
"sequence",
|
|
43
|
+
# Primitive Containers
|
|
44
|
+
"constant",
|
|
45
|
+
"identity",
|
|
46
|
+
# Wrapper Containers
|
|
47
|
+
"lambda_action",
|
|
48
|
+
"log_action",
|
|
49
|
+
"retry",
|
|
50
|
+
"tap",
|
|
51
|
+
# Factory & Decorator
|
|
52
|
+
"create_action",
|
|
53
|
+
"to_action",
|
|
54
|
+
# Aggregated Aliases Namespace
|
|
55
|
+
"aliases",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
_DESIGN_NOTES = """
|
|
60
|
+
# Main Simplibs Actions Package
|
|
61
|
+
|
|
62
|
+
## Purpose
|
|
63
|
+
Root public entry point for `simplibs-actions`. Exposes the complete high-level API
|
|
64
|
+
surface for constructing, composing, decorating, and executing data transformation pipelines.
|
|
65
|
+
|
|
66
|
+
## Public Components Registry
|
|
67
|
+
|
|
68
|
+
| Component | Type | Origin Sub-Package | Description |
|
|
69
|
+
| :---------------- | :------- | :----------------- | :------------------------------------------------------------------------ |
|
|
70
|
+
| `Action` | Class | `base_class` | Core abstract base class defining operator composition and execution. |
|
|
71
|
+
| `SequenceCompose` | Class | `containers` | Intermediate lazy composition wrapper created via `>>` / `+` operators. |
|
|
72
|
+
| `ParallelCompose` | Class | `containers` | Intermediate lazy composition wrapper created via `&` operator. |
|
|
73
|
+
| `branch` | Function | `containers` | Soft conditional branching container (`if/then/else`). |
|
|
74
|
+
| `for_each` | Function | `containers` | Collection-processing container over input iterables. |
|
|
75
|
+
| `guard` | Function | `containers` | Hard gate container validating data against a rule before proceeding. |
|
|
76
|
+
| `fallback` | Function | `containers` | Exception mitigation container providing alternative branch execution. |
|
|
77
|
+
| `parallel` | Function | `containers` | Parallel branch execution container returning tuple of outputs. |
|
|
78
|
+
| `sequence` | Function | `containers` | Sequential step execution container feeding results left-to-right. |
|
|
79
|
+
| `constant` | Function | `containers` | Primitive container ignoring input and returning a fixed value. |
|
|
80
|
+
| `identity` | Function | `containers` | Primitive container returning input data unchanged. |
|
|
81
|
+
| `lambda_action` | Function | `containers` | Wrapper transforming raw single-argument callables into Actions. |
|
|
82
|
+
| `log_action` | Function | `containers` | Wrapper logging pipeline data without modifying the payload. |
|
|
83
|
+
| `retry` | Function | `containers` | Wrapper executing action multiple times on specified exception catches. |
|
|
84
|
+
| `tap` | Function | `containers` | Wrapper executing side-effect functions while passing input through. |
|
|
85
|
+
| `create_action` | Function | `creator` | Low-level factory function synthesizing typed `Action` classes/instances. |
|
|
86
|
+
| `to_action` | Function | `decorator` | High-level decorator turning standard callables into two-phase Actions. |
|
|
87
|
+
| `aliases` | Instance | `containers` | Aggregated namespace offering descriptive naming alternatives for containers|
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
## Architectural Design Decisions
|
|
91
|
+
|
|
92
|
+
1. **Single Source of Truth for Aliases**: The `aliases` namespace instance is instantiated strictly
|
|
93
|
+
once within `containers/__init__.py` and simply re-exported here. This guarantees object identity
|
|
94
|
+
(`actions.aliases is actions.containers.aliases`) and eliminates duplication.
|
|
95
|
+
2. **Flattened Public API**: All primary building blocks are re-exported at the package root
|
|
96
|
+
so downstream code can import directly from `simplibs.actions`.
|
|
97
|
+
3. **Explicit Alias Namespacing**: Aliases are intentionally contained inside the `aliases`
|
|
98
|
+
object namespace to prevent polluting the top-level namespace while providing auto-complete
|
|
99
|
+
and clear intent (`actions.aliases.run_in_sequence`).
|
|
100
|
+
4. **Isolation of Testing Tools**: Testing utilities (`assert_action`, `assert_action_alias`)
|
|
101
|
+
are strictly excluded from this root package to keep production imports light and avoid
|
|
102
|
+
unnecessary test-framework dependencies in non-testing environments.
|
|
103
|
+
"""
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
from simplibs.rules import Rule
|
|
4
|
+
# Inners
|
|
5
|
+
from ._validations import raise_invalid_action_target
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Action(ABC):
|
|
9
|
+
"""Abstract base class for all operative actions and transformations.
|
|
10
|
+
|
|
11
|
+
`__call__` is the sole abstract method — unlike `Rule` (where
|
|
12
|
+
`is_valid` is the answer to a question and `__call__` is just
|
|
13
|
+
convenient sugar over it), for an `Action` being callable IS the
|
|
14
|
+
whole point, so no second method (`execute`/`act`) is needed at the
|
|
15
|
+
base level. Concrete subclasses arise in two ways: hand-written
|
|
16
|
+
containers implementing `__call__` directly (e.g. `SequenceCompose`/
|
|
17
|
+
`ParallelCompose`, which need bespoke flattening logic no generic
|
|
18
|
+
factory can derive on its own), or generated from an ordinary
|
|
19
|
+
function via `create_action`/`to_action` (e.g. `guard`, `branch`,
|
|
20
|
+
`sequence`, `fallback` — nearly everything else).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Hook signaling to Rule that this object is not meant to be silently
|
|
24
|
+
# absorbed by `Rule.__and__`/`__or__` as a plain predicate — see
|
|
25
|
+
# Rule.py's own design notes for the full rationale.
|
|
26
|
+
__not_rule__ = True
|
|
27
|
+
|
|
28
|
+
# ----------------------------------------------------------------------
|
|
29
|
+
# 1) Executable Interface
|
|
30
|
+
# ----------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def __call__(self, data: Any, /) -> Any:
|
|
34
|
+
"""Run this action over `data` and return the result."""
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
# ----------------------------------------------------------------------
|
|
38
|
+
# 2) Operator-Based Composition — Forward Direction (self on the left)
|
|
39
|
+
# ----------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
def then(self, next_step: Any) -> "Action":
|
|
42
|
+
"""Chains this action with a subsequent action, rule, or callable."""
|
|
43
|
+
from ..containers.operators.compose_class import SequenceCompose
|
|
44
|
+
return SequenceCompose.compose(first=self, second=self.resolve_action(next_step))
|
|
45
|
+
|
|
46
|
+
def __rshift__(self, next_step: Any) -> "Action":
|
|
47
|
+
"""Chains this action with a subsequent step via the `>>` operator."""
|
|
48
|
+
return self.then(next_step)
|
|
49
|
+
|
|
50
|
+
def __and__(self, other: Any) -> "Action":
|
|
51
|
+
"""Executes actions in parallel over identical input data: `actionA & actionB`."""
|
|
52
|
+
from ..containers.operators.compose_class import ParallelCompose
|
|
53
|
+
return ParallelCompose.compose(first=self, second=self.resolve_action(other))
|
|
54
|
+
|
|
55
|
+
def __or__(self, other: Any) -> "Action":
|
|
56
|
+
"""Fallback handling: executes `other` if this action fails: `actionA | actionB`."""
|
|
57
|
+
from ..containers.operators import fallback
|
|
58
|
+
return fallback(action=self, on_error=self.resolve_action(other))
|
|
59
|
+
|
|
60
|
+
# ----------------------------------------------------------------------
|
|
61
|
+
# 3) Operator-Based Composition — Reflected Direction (self on the right)
|
|
62
|
+
# ----------------------------------------------------------------------
|
|
63
|
+
#
|
|
64
|
+
# Triggered when `Rule.__and__`/`__or__` declines the combination
|
|
65
|
+
# because of `__not_rule__` (see Rule.py's own design notes) and
|
|
66
|
+
# Python falls back to `other.__rand__`/`__ror__`/`__rrshift__` — or
|
|
67
|
+
# when `other` is a plain callable/Rule with no operator of its own
|
|
68
|
+
# at all. Each method simply wraps `other` via `resolve_action` and
|
|
69
|
+
# hands off to the forward operator — one place of truth, no
|
|
70
|
+
# duplicated container-construction logic.
|
|
71
|
+
|
|
72
|
+
def __rand__(self, other: Any) -> "Action":
|
|
73
|
+
"""Supports `rule & action` — verify AND execute in parallel."""
|
|
74
|
+
return self.resolve_action(other) & self
|
|
75
|
+
|
|
76
|
+
def __ror__(self, other: Any) -> "Action":
|
|
77
|
+
"""Supports `rule | action` — verify, or run a fallback action on failure."""
|
|
78
|
+
return self.resolve_action(other) | self
|
|
79
|
+
|
|
80
|
+
def __rrshift__(self, other: Any) -> "Action":
|
|
81
|
+
"""Supports `rule >> action` — verify the rule, then continue with the action."""
|
|
82
|
+
return self.resolve_action(other) >> self
|
|
83
|
+
|
|
84
|
+
# ----------------------------------------------------------------------
|
|
85
|
+
# 4) Resolution Helper
|
|
86
|
+
# ----------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def resolve_action(obj: Any) -> "Action":
|
|
90
|
+
"""Convert an Action, a Rule, or a plain callable into an Action instance.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
obj: An `Action` instance (returned unchanged), a `Rule`
|
|
94
|
+
instance (wrapped into `guard`), or any other callable
|
|
95
|
+
(wrapped into `lambda_action`).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
An `Action` instance ready to be composed.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
TypeError: If `obj` is neither an `Action`, a `Rule`, nor callable.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
# 1. Already an Action instance — return it unchanged
|
|
105
|
+
if isinstance(obj, Action):
|
|
106
|
+
return obj
|
|
107
|
+
|
|
108
|
+
# 2. A Rule instance — wrap it into the hard-gate container
|
|
109
|
+
if isinstance(obj, Rule):
|
|
110
|
+
from ..containers.flow_control import guard
|
|
111
|
+
return guard(rule=obj)
|
|
112
|
+
|
|
113
|
+
# 3. Any other callable — wrap it into the plain-transformer adapter
|
|
114
|
+
if callable(obj):
|
|
115
|
+
from ..containers.wrappers import lambda_action
|
|
116
|
+
return lambda_action(func=obj)
|
|
117
|
+
|
|
118
|
+
# 4. Anything else is not a valid Action target
|
|
119
|
+
return raise_invalid_action_target(obj)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
_DESIGN_NOTES = """
|
|
123
|
+
# Action — base abstract class (redesigned)
|
|
124
|
+
|
|
125
|
+
## Why only `__call__`, no `execute`/`act` at the base level
|
|
126
|
+
|
|
127
|
+
An earlier version had `execute` as the abstract method and `__call__` as
|
|
128
|
+
a thin wrapper over it — mirroring `Rule.is_valid`/`__call__`. That
|
|
129
|
+
mirroring turned out to be a mistake: for `Rule`, `is_valid` is the
|
|
130
|
+
*answer to a question* and callability is a convenience on top; for
|
|
131
|
+
`Action`, callability itself IS the substance — an action *is* a
|
|
132
|
+
function with one main input. The second method was never actually
|
|
133
|
+
needed, and once caused a real bug (a class generated by `create_action`
|
|
134
|
+
had `__call__`, but the base `Action` required `execute` — the class
|
|
135
|
+
could not be instantiated at all). Collapsing to a single abstract
|
|
136
|
+
`__call__` removes that mismatch structurally, not with a patch.
|
|
137
|
+
|
|
138
|
+
Subclasses arise in two ways:
|
|
139
|
+
* **hand-written** — `SequenceCompose`/`ParallelCompose` implement
|
|
140
|
+
`__call__` directly (inherited from their `create_action`-generated
|
|
141
|
+
base, `sequence`/`parallel`) because they need bespoke flattening
|
|
142
|
+
logic (`compose`) that no generic factory can derive on its own;
|
|
143
|
+
* **via `create_action`/`to_action`** — nearly everything else (`guard`,
|
|
144
|
+
`branch`, `for_each`, `sequence`, `parallel`, `fallback`, `identity`,
|
|
145
|
+
`constant`, `lambda_action`, `log_step`, `retry`, `tap`). There,
|
|
146
|
+
`act`/`__init__`/`__call__` are implementation details of the
|
|
147
|
+
*generator* (needed for the separate `validate_call`/`log_this`
|
|
148
|
+
wrapping of each), not a requirement of the base class itself.
|
|
149
|
+
|
|
150
|
+
## Reflected operators — `__rand__`/`__ror__`/`__rrshift__`
|
|
151
|
+
|
|
152
|
+
Triggered exclusively by the `__not_rule__` hook on the `Rule` side (see
|
|
153
|
+
`Rule.py`'s own design notes) — `rule & action` first hits
|
|
154
|
+
`Rule.__and__`, which returns `NotImplemented`, and only then does Python
|
|
155
|
+
try `action.__rand__(rule)`.
|
|
156
|
+
|
|
157
|
+
The semantics of all three are derived from what the `guard` container
|
|
158
|
+
does (pass through unchanged if the `Rule` holds, otherwise raise):
|
|
159
|
+
* `rule >> action` — check, then continue (sequence).
|
|
160
|
+
* `rule & action` — check AND run the action (in parallel; both must
|
|
161
|
+
succeed — the spirit of `AllOf`, shifted from "predicate AND
|
|
162
|
+
predicate" to "gate AND action").
|
|
163
|
+
* `rule | action` — check, and on failure run the action instead (the
|
|
164
|
+
spirit of `AnyOf` — at least one path must succeed, only the first
|
|
165
|
+
path is a hard gate).
|
|
166
|
+
|
|
167
|
+
All three are implemented as the same one-line pattern —
|
|
168
|
+
`self.resolve_action(other) <op> self` — wrap the left-hand operand,
|
|
169
|
+
hand off to the forward operator. No second copy of any container's
|
|
170
|
+
construction logic; one place of truth covers both `rule & action` and
|
|
171
|
+
`action & action`.
|
|
172
|
+
|
|
173
|
+
**A typing cost, not a behavioral one:** `Rule.__and__`/`__or__` can now
|
|
174
|
+
return either `Rule` or `Action`, depending on the right-hand operand's
|
|
175
|
+
type. This is already handled on the `Rule` side via a structural
|
|
176
|
+
`_NotARule` `Protocol` plus `@overload` pairs on every binary operator
|
|
177
|
+
(`__and__`/`__or__`/`__rand__`/`__ror__`) — see `Rule.py`'s own design
|
|
178
|
+
notes, section 6, for why a `Protocol` is used instead of importing
|
|
179
|
+
`Action` directly (keeping `simplibs-validate` independent of
|
|
180
|
+
`simplibs-actions`, even for typing purposes).
|
|
181
|
+
|
|
182
|
+
## `__not_rule__` remains unchanged
|
|
183
|
+
|
|
184
|
+
Still the single hook driving this entire asymmetry — `Rule` declines to
|
|
185
|
+
treat `Action` as a predicate, `Action` supplies its own, meaningful
|
|
186
|
+
interpretation instead. See `Rule.py`'s design notes for the complete
|
|
187
|
+
rationale.
|
|
188
|
+
"""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .Action import Action
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
_DESIGN_NOTES = """
|
|
5
|
+
# Action Base Sub-Package
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
Provides the abstract base class `Action` that defines core pipeline execution,
|
|
9
|
+
dunder-method composition, representation, and polymorphic resolution for all
|
|
10
|
+
action types in the package.
|
|
11
|
+
|
|
12
|
+
## Components Registry
|
|
13
|
+
|
|
14
|
+
| Component | Type | Description |
|
|
15
|
+
| :-------- | :---- | :------------------------------------------------------------------------------ |
|
|
16
|
+
| `Action` | Class | Base class for callable data transformation units with operator composition. |
|
|
17
|
+
"""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .raise_invalid_action_target import raise_invalid_action_target
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
_DESIGN_NOTES = """
|
|
5
|
+
# Action Base Validations Sub-Package
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
Internal diagnostic and validation helpers used by the `Action` base class to enforce
|
|
9
|
+
type safety and provide formatted diagnostics during action resolution and composition.
|
|
10
|
+
|
|
11
|
+
## Internal Components Registry
|
|
12
|
+
|
|
13
|
+
| Component | Type | Description |
|
|
14
|
+
| :----------------------------- | :------- | :-------------------------------------------------------------------------- |
|
|
15
|
+
| `raise_invalid_action_target` | Function | Raises a formatted `ParamError` when an object cannot be converted to Action.|
|
|
16
|
+
"""
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Any, NoReturn
|
|
2
|
+
from simplibs.exception import ParamError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def raise_invalid_action_target(obj: Any) -> NoReturn:
|
|
6
|
+
"""Raises a ParamError when an object cannot be resolved into an Action.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
obj: The candidate target object that failed resolution checks.
|
|
10
|
+
|
|
11
|
+
Raises:
|
|
12
|
+
ParamError: Unconditionally raised with diagnostic context.
|
|
13
|
+
"""
|
|
14
|
+
raise ParamError(
|
|
15
|
+
error_name="INVALID_ACTION_TARGET",
|
|
16
|
+
label="action target candidate",
|
|
17
|
+
value=type(obj).__name__,
|
|
18
|
+
problem=(
|
|
19
|
+
f"Object of type '{type(obj).__name__}' cannot be converted into an Action.",
|
|
20
|
+
"Action resolution expects an Action instance, a Rule, or any Callable.",
|
|
21
|
+
),
|
|
22
|
+
expected="An Action instance, a Rule, or a callable function/object.",
|
|
23
|
+
how_to_fix=(
|
|
24
|
+
"Ensure you pass a valid callable entity to the operator or resolution helper.",
|
|
25
|
+
"If passing a custom class or object, ensure it is callable (implements __call__) "
|
|
26
|
+
"or inherits from Action / Rule.",
|
|
27
|
+
),
|
|
28
|
+
exception=TypeError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_DESIGN_NOTES = """
|
|
33
|
+
# raise_invalid_action_target — Action Resolution Failure Diagnostic
|
|
34
|
+
|
|
35
|
+
## Purpose
|
|
36
|
+
Provides a standard, highly diagnostic `ParamError` exception when an object
|
|
37
|
+
passed to `Action.resolve_action` or compositional operators (`|`, `>>`) cannot
|
|
38
|
+
be converted into a valid `Action` instance.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 1. Unified Exception Interface
|
|
43
|
+
|
|
44
|
+
By delegating invalid resolution reporting to a dedicated function, all
|
|
45
|
+
composition and resolution entry points share identical error diagnostics,
|
|
46
|
+
labels, and remediation instructions.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 2. Dynamic Type Inspection
|
|
51
|
+
|
|
52
|
+
Reflects the actual type of the received `obj` in the diagnostic message to
|
|
53
|
+
give clear feedback to the developer about what unsupported type was provided.
|
|
54
|
+
"""
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Composition Classes
|
|
2
|
+
from .operators.compose_class import ParallelCompose, SequenceCompose
|
|
3
|
+
|
|
4
|
+
# Flow Control Containers
|
|
5
|
+
from .flow_control import branch, for_each, guard
|
|
6
|
+
|
|
7
|
+
# Operator Containers
|
|
8
|
+
from .operators import fallback, parallel, sequence
|
|
9
|
+
|
|
10
|
+
# Primitive Containers
|
|
11
|
+
from .primitives import constant, identity
|
|
12
|
+
|
|
13
|
+
# Wrapper Containers
|
|
14
|
+
from .wrappers import lambda_action, log_action, retry, tap
|
|
15
|
+
|
|
16
|
+
# Aliases Sub-Package / Namespace Import
|
|
17
|
+
from . import flow_control, operators, primitives, wrappers
|
|
18
|
+
|
|
19
|
+
# Aggregated Aliases Namespace
|
|
20
|
+
class _Aliases:
|
|
21
|
+
"""Namespace grouping all alias functions across container sub-packages."""
|
|
22
|
+
# Flow Control Aliases
|
|
23
|
+
apply_to_each = flow_control.aliases.apply_to_each
|
|
24
|
+
conditional_action = flow_control.aliases.conditional_action
|
|
25
|
+
guarded_action = flow_control.aliases.guarded_action
|
|
26
|
+
|
|
27
|
+
# Operator Aliases
|
|
28
|
+
run_in_parallel = operators.aliases.run_in_parallel
|
|
29
|
+
run_in_sequence = operators.aliases.run_in_sequence
|
|
30
|
+
try_or_fallback = operators.aliases.try_or_fallback
|
|
31
|
+
|
|
32
|
+
# Primitive Aliases
|
|
33
|
+
pass_through = primitives.aliases.pass_through
|
|
34
|
+
replace_with = primitives.aliases.replace_with
|
|
35
|
+
|
|
36
|
+
# Wrapper Aliases
|
|
37
|
+
callable_action = wrappers.aliases.callable_action
|
|
38
|
+
log_step = wrappers.aliases.log_step
|
|
39
|
+
retry_on_failure = wrappers.aliases.retry_on_failure
|
|
40
|
+
run_side_effect = wrappers.aliases.run_side_effect
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
aliases = _Aliases()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
# Composition Classes
|
|
48
|
+
"ParallelCompose",
|
|
49
|
+
"SequenceCompose",
|
|
50
|
+
# Flow Control
|
|
51
|
+
"branch",
|
|
52
|
+
"for_each",
|
|
53
|
+
"guard",
|
|
54
|
+
# Operators
|
|
55
|
+
"fallback",
|
|
56
|
+
"parallel",
|
|
57
|
+
"sequence",
|
|
58
|
+
# Primitives
|
|
59
|
+
"constant",
|
|
60
|
+
"identity",
|
|
61
|
+
# Wrappers
|
|
62
|
+
"lambda_action",
|
|
63
|
+
"log_action",
|
|
64
|
+
"retry",
|
|
65
|
+
"tap",
|
|
66
|
+
# Aliases
|
|
67
|
+
"aliases",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_DESIGN_NOTES = """
|
|
72
|
+
# Main Action Containers Package
|
|
73
|
+
|
|
74
|
+
## Purpose
|
|
75
|
+
The `containers` package forms the core structural foundation of the action system. It provides
|
|
76
|
+
a rich set of higher-order action containers that manage execution flow, error handling, batching,
|
|
77
|
+
parallelism, side effects, and pipeline primitive building blocks.
|
|
78
|
+
|
|
79
|
+
## Internal Sub-Packages Registry
|
|
80
|
+
|
|
81
|
+
| Sub-Package | Type | Description |
|
|
82
|
+
| :------------- | :----------------- | :------------------------------------------------------------------------------ |
|
|
83
|
+
| `_helpers` | Internal Helpers | Private utilities for action normalization, inspection, and sequence unwrapping.|
|
|
84
|
+
| `flow_control` | Public Containers | Branching (`branch`), iteration (`for_each`), and conditional evaluation (`guard`).|
|
|
85
|
+
| `operators` | Public Containers | Main composition containers (`sequence`, `parallel`, `fallback`) and classes. |
|
|
86
|
+
| `primitives` | Public Containers | Fundamental pipeline building blocks (`identity`, `constant`). |
|
|
87
|
+
| `wrappers` | Public Containers | Decorating and utility containers (`lambda_action`, `log_action`, `retry`, `tap`).|
|
|
88
|
+
"""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .as_action import as_action
|
|
2
|
+
from .as_predicate import as_predicate
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
_DESIGN_NOTES = """
|
|
6
|
+
# Action Container Helpers Sub-Package
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
Internal resolution and adaptation helpers used across action containers to normalize
|
|
10
|
+
arbitrary runnables into unified `Action` instances and condition inputs into executable predicates.
|
|
11
|
+
|
|
12
|
+
## Internal Components Registry
|
|
13
|
+
|
|
14
|
+
| Component | Type | Description |
|
|
15
|
+
| :------------- | :------- | :------------------------------------------------------------------------------ |
|
|
16
|
+
| `as_action` | Alias | Direct shorthand for `Action.resolve_action` to normalize runnables to Actions. |
|
|
17
|
+
| `as_predicate` | Function | Normalizes `Rule` instances or callables into unified boolean-returning methods.|
|
|
18
|
+
"""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Outers
|
|
2
|
+
from ...base_class import Action
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
as_action = Action.resolve_action
|
|
6
|
+
"""Module-level alias for `Action.resolve_action`.
|
|
7
|
+
|
|
8
|
+
Provides a clean, self-describing shorthand within container implementations,
|
|
9
|
+
eliminating the need to repeatedly access the static method via `Action.resolve_action(...)`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_DESIGN_NOTES = """
|
|
14
|
+
# as_action — module-level alias for Action.resolve_action
|
|
15
|
+
|
|
16
|
+
## Purpose
|
|
17
|
+
Every container that accepts "something runnable" (`then_branch`, `action`,
|
|
18
|
+
`on_error`, individual `sequence`/`parallel` steps...) needs to resolve a
|
|
19
|
+
given object into a uniform `Action` instance.
|
|
20
|
+
`Action.resolve_action` houses the core resolution logic. `as_action` serves
|
|
21
|
+
as a module-level alias to avoid repeated, verbose access via the class name
|
|
22
|
+
(`Action.resolve_action`) across container modules, making the call sites
|
|
23
|
+
substantially more self-describing and expressive.
|
|
24
|
+
|
|
25
|
+
## Why a direct assignment alias, not a wrapping function
|
|
26
|
+
No wrapper overhead or duplicate logic — all resolution mechanics (detecting
|
|
27
|
+
`Action`/`Rule`/callable and adapting accordingly) remain strictly encapsulated
|
|
28
|
+
inside `Action.resolve_action`. Assigning `as_action = Action.resolve_action`
|
|
29
|
+
directly guarantees zero execution overhead while retaining full IDE signature
|
|
30
|
+
hints, docstrings, and type safety.
|
|
31
|
+
"""
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import Any, Callable
|
|
2
|
+
from simplibs.rules import Rule
|
|
3
|
+
# Inners
|
|
4
|
+
from .validations import raise_invalid_predicate
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def as_predicate(condition: Any) -> Callable[[Any], bool]:
|
|
8
|
+
"""Normalize a Rule instance or a plain callable into a single bool-returning call.
|
|
9
|
+
|
|
10
|
+
Replaces repeated duck-typing checks in `guard`/`branch` with a single
|
|
11
|
+
source of truth — aligning with the helper pattern used across the library.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# 1. Official Rule instance -> direct method reference (fastest path)
|
|
15
|
+
if isinstance(condition, Rule):
|
|
16
|
+
return condition.is_valid
|
|
17
|
+
|
|
18
|
+
# 2. Plain lambda / function / any callable
|
|
19
|
+
if callable(condition):
|
|
20
|
+
# noinspection PyTypeChecker
|
|
21
|
+
return condition
|
|
22
|
+
|
|
23
|
+
# 3. Structured exception helper for invalid predicate inputs
|
|
24
|
+
raise_invalid_predicate(condition)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_DESIGN_NOTES = """
|
|
28
|
+
# as_predicate — shared condition adapter
|
|
29
|
+
|
|
30
|
+
## Purpose
|
|
31
|
+
`guard` and `branch` both need the same thing: "give me a function that
|
|
32
|
+
returns bool for `data`", whether `condition`/`rule` arrived as a `Rule`
|
|
33
|
+
instance (`is_valid`) or a plain callable. Without this helper, both
|
|
34
|
+
containers would carry duplicate validation and resolution logic. Using an
|
|
35
|
+
explicit `isinstance(condition, Rule)` check ensures fast and type-safe
|
|
36
|
+
method binding.
|
|
37
|
+
|
|
38
|
+
## Exception Handling
|
|
39
|
+
When an invalid predicate (neither a `Rule` nor a callable) is passed,
|
|
40
|
+
`raise_invalid_predicate` is triggered to throw a structured exception,
|
|
41
|
+
keeping validation error formats consistent across the library.
|
|
42
|
+
|
|
43
|
+
## Why it doesn't special-case Action
|
|
44
|
+
`Action.__call__` is callable too, but this helper treats it like any
|
|
45
|
+
other callable — it falls into the `callable(condition)` branch and gets
|
|
46
|
+
invoked directly. The result (`Any`, not necessarily `bool`) is then
|
|
47
|
+
evaluated by ordinary Python truthiness in `guard`/`branch`. This works,
|
|
48
|
+
but gives no guarantee the return value is meaningful as a condition —
|
|
49
|
+
left unaddressed here on purpose; if it ever needs constraining, that
|
|
50
|
+
belongs in `guard`/`branch` themselves, not this shared helper.
|
|
51
|
+
|
|
52
|
+
## Relationship to as_action
|
|
53
|
+
Sibling in this same `_helpers` package — `as_predicate` answers "is this
|
|
54
|
+
true?", `as_action` answers "run this over data". Split into two files,
|
|
55
|
+
not one `_helpers.py`, since each serves a different group of containers,
|
|
56
|
+
consistent with the "one item per file" convention across the rest
|
|
57
|
+
of the library.
|
|
58
|
+
"""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .raise_invalid_predicate import raise_invalid_predicate
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
_DESIGN_NOTES = """
|
|
5
|
+
# Action Container Helper Validations Sub-Package
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
Internal structured exception emission helpers dedicated to validating condition
|
|
9
|
+
and predicate inputs used by container helper functions.
|
|
10
|
+
|
|
11
|
+
## Internal Components Registry
|
|
12
|
+
|
|
13
|
+
| Component | Type | Description |
|
|
14
|
+
| :------------------------ | :------- | :------------------------------------------------------------------------------ |
|
|
15
|
+
| `raise_invalid_predicate` | Function | Raises a structured `ParamError` when an input is neither a `Rule` nor callable.|
|
|
16
|
+
"""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Any, NoReturn
|
|
2
|
+
from simplibs.exception import ParamError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def raise_invalid_predicate(condition: Any) -> NoReturn:
|
|
6
|
+
"""Raise a ParamError if the given condition is neither a Rule nor a Callable."""
|
|
7
|
+
|
|
8
|
+
# 1. Raise structured ParamError wrapping a TypeError exception
|
|
9
|
+
raise ParamError(
|
|
10
|
+
error_name="INVALID_PREDICATE_TARGET",
|
|
11
|
+
label="condition / rule",
|
|
12
|
+
value=type(condition).__name__,
|
|
13
|
+
problem=(
|
|
14
|
+
f"Expected a Rule or Callable, got: {type(condition).__name__}.",
|
|
15
|
+
"The passed object cannot be evaluated as a predicate or condition.",
|
|
16
|
+
),
|
|
17
|
+
expected="A Rule instance or a callable function (Callable).",
|
|
18
|
+
how_to_fix=(
|
|
19
|
+
"Ensure that you pass a Rule object or a callable function returning a boolean as the condition.",
|
|
20
|
+
),
|
|
21
|
+
exception=TypeError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_DESIGN_NOTES = """
|
|
26
|
+
# raise_invalid_predicate — structured validation error helper
|
|
27
|
+
|
|
28
|
+
## Purpose
|
|
29
|
+
Provides a centralized, structured exception emitter when a non-predicate value
|
|
30
|
+
(neither a `Rule` instance nor a callable object) is supplied where a condition
|
|
31
|
+
is expected (e.g., in `guard` or `branch` containers).
|
|
32
|
+
|
|
33
|
+
## Target Code & Integration
|
|
34
|
+
- **Caller**: `as_predicate` helper in `containers/_helpers/as_predicate.py`.
|
|
35
|
+
- **Target Context**: Triggered during container setup/execution when normalizing
|
|
36
|
+
input arguments passed as rules or conditional branches.
|
|
37
|
+
|
|
38
|
+
## Why a separate helper
|
|
39
|
+
Isolating exception construction keeps the core `as_predicate` resolution loop
|
|
40
|
+
lean, fast, and easy to read, while enforcing consistent structured error messaging
|
|
41
|
+
(`ParamError`) across the library.
|
|
42
|
+
"""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from . import aliases
|
|
2
|
+
from .branch import branch
|
|
3
|
+
from .for_each import for_each
|
|
4
|
+
from .guard import guard
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_DESIGN_NOTES = """
|
|
8
|
+
# Flow Control Containers Sub-Package
|
|
9
|
+
|
|
10
|
+
## Purpose
|
|
11
|
+
Provides flow control containers that manage data routing, conditional execution,
|
|
12
|
+
and collection iteration within action pipelines.
|
|
13
|
+
|
|
14
|
+
## Internal Components Registry
|
|
15
|
+
|
|
16
|
+
| Component | Type | Description |
|
|
17
|
+
| :--------- | :-------- | :------------------------------------------------------------------------------ |
|
|
18
|
+
| `branch` | Container | Soft conditional branching (routes data or passes through unchanged). |
|
|
19
|
+
| `for_each` | Container | Iterative execution (applies an action to each item of an iterable). |
|
|
20
|
+
| `guard` | Container | Hard gate verification (passes data if rule holds, otherwise raises). |
|
|
21
|
+
"""
|