je-action-core 0.0.1__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.
- je_action_core-0.0.1/LICENSE +21 -0
- je_action_core-0.0.1/PKG-INFO +145 -0
- je_action_core-0.0.1/README.md +125 -0
- je_action_core-0.0.1/je_action_core/__init__.py +73 -0
- je_action_core-0.0.1/je_action_core/action_list.py +173 -0
- je_action_core-0.0.1/je_action_core/builtins_policy.py +18 -0
- je_action_core-0.0.1/je_action_core/callback.py +123 -0
- je_action_core-0.0.1/je_action_core/exceptions.py +25 -0
- je_action_core-0.0.1/je_action_core/executor.py +108 -0
- je_action_core-0.0.1/je_action_core/file_listing.py +19 -0
- je_action_core-0.0.1/je_action_core/json_io.py +108 -0
- je_action_core-0.0.1/je_action_core/package_manager.py +208 -0
- je_action_core-0.0.1/je_action_core/py.typed +0 -0
- je_action_core-0.0.1/je_action_core/registry.py +105 -0
- je_action_core-0.0.1/je_action_core/reporting.py +65 -0
- je_action_core-0.0.1/je_action_core/socket_server.py +111 -0
- je_action_core-0.0.1/je_action_core.egg-info/PKG-INFO +145 -0
- je_action_core-0.0.1/je_action_core.egg-info/SOURCES.txt +31 -0
- je_action_core-0.0.1/je_action_core.egg-info/dependency_links.txt +1 -0
- je_action_core-0.0.1/je_action_core.egg-info/top_level.txt +1 -0
- je_action_core-0.0.1/pyproject.toml +38 -0
- je_action_core-0.0.1/setup.cfg +4 -0
- je_action_core-0.0.1/test/test_action_list.py +117 -0
- je_action_core-0.0.1/test/test_callback.py +78 -0
- je_action_core-0.0.1/test/test_executor.py +145 -0
- je_action_core-0.0.1/test/test_file_listing_and_builtins.py +31 -0
- je_action_core-0.0.1/test/test_json_io.py +65 -0
- je_action_core-0.0.1/test/test_package_manager.py +135 -0
- je_action_core-0.0.1/test/test_public_api.py +13 -0
- je_action_core-0.0.1/test/test_registry.py +64 -0
- je_action_core-0.0.1/test/test_socket_server.py +93 -0
- je_action_core-0.0.1/test/test_workflow_actions.py +124 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 JE-Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: je_action_core
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The keyword-driven action executor shared by APITestka, LoadDensity, MailThunder and FileAutomation
|
|
5
|
+
Author-email: JE-Chen <jechenmailman@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Code, https://github.com/Integration-Automation/ActionCore
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Other Environment
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# ActionCore
|
|
22
|
+
|
|
23
|
+
**English** | [繁體中文](README/README_zh-TW.md) | [简体中文](README/README_zh-CN.md)
|
|
24
|
+
|
|
25
|
+
`je_action_core` is the keyword-driven action executor shared by
|
|
26
|
+
[APITestka](https://github.com/Integration-Automation/APITestka), [LoadDensity](https://github.com/Integration-Automation/LoadDensity),
|
|
27
|
+
[MailThunder](https://github.com/Integration-Automation/MailThunder) and [FileAutomation](https://github.com/Integration-Automation/FileAutomation).
|
|
28
|
+
Each of them carried its own copy of the same executor, package manager, callback executor, action-file reader
|
|
29
|
+
and socket server. This package replaces those copies: a project configures it with its own command prefix,
|
|
30
|
+
document key, exceptions and messages.
|
|
31
|
+
|
|
32
|
+
It has no dependencies and supports Python 3.10 to 3.14.
|
|
33
|
+
|
|
34
|
+
## Contents
|
|
35
|
+
|
|
36
|
+
- [Installation](#installation)
|
|
37
|
+
- [Actions](#actions)
|
|
38
|
+
- [Quick start](#quick-start)
|
|
39
|
+
- [The pieces](#the-pieces)
|
|
40
|
+
- [Package gate](#package-gate)
|
|
41
|
+
- [Socket server protocol](#socket-server-protocol)
|
|
42
|
+
- [Who uses it](#who-uses-it)
|
|
43
|
+
- [Development](#development)
|
|
44
|
+
- [License](#license)
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install je_action_core
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A framework that is built on it installs it as a dependency.
|
|
53
|
+
|
|
54
|
+
## Actions
|
|
55
|
+
|
|
56
|
+
An action list is a JSON list of actions, or a document that keeps the list under one key
|
|
57
|
+
(`{"api_testka": [...]}`). Each action takes one of three forms:
|
|
58
|
+
|
|
59
|
+
| Action | Call |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `["name"]` | `command()` |
|
|
62
|
+
| `["name", {"a": 1}]` | `command(a=1)` |
|
|
63
|
+
| `["name", [1, 2]]` | `command(1, 2)` |
|
|
64
|
+
|
|
65
|
+
Running a list returns one record per action: the command's return value, or `repr(error)` when it raised. One
|
|
66
|
+
failing action does not stop the rest.
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from je_action_core import (
|
|
72
|
+
ActionExecutor, ActionListRules, CommandPolicy, CommandRegistry, ExecutorSettings, LoggingReporter,
|
|
73
|
+
)
|
|
74
|
+
import logging
|
|
75
|
+
|
|
76
|
+
registry = CommandRegistry({"MY_add": lambda a, b: a + b}, policy=CommandPolicy.FUNCTIONS_ONLY)
|
|
77
|
+
executor = ActionExecutor(
|
|
78
|
+
ExecutorSettings(rules=ActionListRules("my_tool"), reporter=LoggingReporter(logging.getLogger("my_tool"))),
|
|
79
|
+
registry,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
executor.execute_action({"my_tool": [["MY_add", [1, 2]], ["MY_add", {"a": 3, "b": 4}]]})
|
|
83
|
+
# {"execute: ['MY_add', [1, 2]]": 3, "execute: ['MY_add', {'a': 3, 'b': 4}]": 7}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## The pieces
|
|
87
|
+
|
|
88
|
+
| Module | What it gives you | Settings |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `registry` | `CommandRegistry`: commands by name, `event_dict` as the live mapping | `CommandPolicy.FUNCTIONS_ONLY` or `ANY_CALLABLE` for caller-added commands; the exception for a refused one |
|
|
91
|
+
| `action_list` | `ActionListRules` (where the list is), `LegacyActionParser` and `StrictActionParser` | document key and legacy keys (with a `DeprecationWarning`); empty list raises or returns `{}`; error messages |
|
|
92
|
+
| `executor` | `ActionExecutor`: `execute_action`, `execute_files`, `add_command_to_executor` | `ExecutorSettings`: rules, parser, reporter, file reader, record key (`execute: …` or `execute[i]: …`), action rewrite |
|
|
93
|
+
| `reporting` | `LoggingReporter`, `PrintReporter`, or your own `ExecutionReporter` | where events, failures and records go |
|
|
94
|
+
| `package_manager` | `PackageManager`: load an installed package's members as commands, behind the [package gate](#package-gate) | member naming (`<package>_<member>` or bare), predicates, name check, errors to log |
|
|
95
|
+
| `callback` | `CallbackFunctionExecutor`: run a trigger, then a callback | legacy or strict checking; raise or log and return `None` |
|
|
96
|
+
| `json_io` | `ActionJsonFile`, `read_action_json`, `write_action_json`: UTF-8, locked, non-ASCII kept | exception class and message templates |
|
|
97
|
+
| `file_listing` | `get_dir_files_as_list`: action files under a directory | — |
|
|
98
|
+
| `socket_server` | `start_action_socket_server`: the plain TCP action server | executor, payload check, errors answered, oversize policy |
|
|
99
|
+
| `builtins_policy` | `SAFE_BUILTINS`: the builtins an action list may call | — |
|
|
100
|
+
|
|
101
|
+
`__all__` in `je_action_core/__init__.py` is the supported import surface.
|
|
102
|
+
|
|
103
|
+
## Package gate
|
|
104
|
+
|
|
105
|
+
`PackageManager.add_package_to_executor` imports a package and registers its members as commands. An action list
|
|
106
|
+
that can name `os` or `subprocess` could therefore run anything. The host program decides what may load:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
package_manager.allow_packages("my_helpers") # these, and their submodules
|
|
110
|
+
package_manager.set_allow_arbitrary_packages(False) # refuse everything else before importing it
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Neither switch should ever be exposed as an action command, so an action list cannot open its own gate. A
|
|
114
|
+
refused package raises the project's `refused` exception, which the executor records as that action's result.
|
|
115
|
+
Until the host calls either switch, any package still loads but raises a `DeprecationWarning`. A project can
|
|
116
|
+
turn the gate off (`PackageGate.OFF`) while it moves to it.
|
|
117
|
+
|
|
118
|
+
## Socket server protocol
|
|
119
|
+
|
|
120
|
+
`start_action_socket_server(host, port, settings)` serves on a daemon thread. A client sends one JSON action
|
|
121
|
+
document per connection. The server replies with each record value on its own line, then
|
|
122
|
+
`Return_Data_Over_JE`. A failure replies with the error text and the same marker. `quit_server` stops the server
|
|
123
|
+
and sets `close_flag`. The server has no authentication, so bind it only to a trust## Who uses it
|
|
124
|
+
|
|
125
|
+
APITestka (`AT_`, port 9939), LoadDensity (`LD_`), MailThunder (`MT_`, port 9942) and FileAutomation (`FA_`) are
|
|
126
|
+
the projects this package is for. `architecture.md` §6 lists which of them have moved to it and which pieces and
|
|
127
|
+
settings each one uses. LoadDensity and FileAutomation keep their own socket servers: a gevent server with
|
|
128
|
+
framing, a token and TLS, and a server with authentication and an ACL.
|
|
129
|
+
|
|
130
|
+
n and an ACL.
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pip install -e .
|
|
136
|
+
pip install pytest
|
|
137
|
+
python -m pytest test/
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`architecture.md` describes the layers and the contracts with the four projects. Outstanding work is in
|
|
141
|
+
`progress.md`, and finished work is recorded in `docs/updates/`.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# ActionCore
|
|
2
|
+
|
|
3
|
+
**English** | [繁體中文](README/README_zh-TW.md) | [简体中文](README/README_zh-CN.md)
|
|
4
|
+
|
|
5
|
+
`je_action_core` is the keyword-driven action executor shared by
|
|
6
|
+
[APITestka](https://github.com/Integration-Automation/APITestka), [LoadDensity](https://github.com/Integration-Automation/LoadDensity),
|
|
7
|
+
[MailThunder](https://github.com/Integration-Automation/MailThunder) and [FileAutomation](https://github.com/Integration-Automation/FileAutomation).
|
|
8
|
+
Each of them carried its own copy of the same executor, package manager, callback executor, action-file reader
|
|
9
|
+
and socket server. This package replaces those copies: a project configures it with its own command prefix,
|
|
10
|
+
document key, exceptions and messages.
|
|
11
|
+
|
|
12
|
+
It has no dependencies and supports Python 3.10 to 3.14.
|
|
13
|
+
|
|
14
|
+
## Contents
|
|
15
|
+
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Actions](#actions)
|
|
18
|
+
- [Quick start](#quick-start)
|
|
19
|
+
- [The pieces](#the-pieces)
|
|
20
|
+
- [Package gate](#package-gate)
|
|
21
|
+
- [Socket server protocol](#socket-server-protocol)
|
|
22
|
+
- [Who uses it](#who-uses-it)
|
|
23
|
+
- [Development](#development)
|
|
24
|
+
- [License](#license)
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install je_action_core
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
A framework that is built on it installs it as a dependency.
|
|
33
|
+
|
|
34
|
+
## Actions
|
|
35
|
+
|
|
36
|
+
An action list is a JSON list of actions, or a document that keeps the list under one key
|
|
37
|
+
(`{"api_testka": [...]}`). Each action takes one of three forms:
|
|
38
|
+
|
|
39
|
+
| Action | Call |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `["name"]` | `command()` |
|
|
42
|
+
| `["name", {"a": 1}]` | `command(a=1)` |
|
|
43
|
+
| `["name", [1, 2]]` | `command(1, 2)` |
|
|
44
|
+
|
|
45
|
+
Running a list returns one record per action: the command's return value, or `repr(error)` when it raised. One
|
|
46
|
+
failing action does not stop the rest.
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from je_action_core import (
|
|
52
|
+
ActionExecutor, ActionListRules, CommandPolicy, CommandRegistry, ExecutorSettings, LoggingReporter,
|
|
53
|
+
)
|
|
54
|
+
import logging
|
|
55
|
+
|
|
56
|
+
registry = CommandRegistry({"MY_add": lambda a, b: a + b}, policy=CommandPolicy.FUNCTIONS_ONLY)
|
|
57
|
+
executor = ActionExecutor(
|
|
58
|
+
ExecutorSettings(rules=ActionListRules("my_tool"), reporter=LoggingReporter(logging.getLogger("my_tool"))),
|
|
59
|
+
registry,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
executor.execute_action({"my_tool": [["MY_add", [1, 2]], ["MY_add", {"a": 3, "b": 4}]]})
|
|
63
|
+
# {"execute: ['MY_add', [1, 2]]": 3, "execute: ['MY_add', {'a': 3, 'b': 4}]": 7}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## The pieces
|
|
67
|
+
|
|
68
|
+
| Module | What it gives you | Settings |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `registry` | `CommandRegistry`: commands by name, `event_dict` as the live mapping | `CommandPolicy.FUNCTIONS_ONLY` or `ANY_CALLABLE` for caller-added commands; the exception for a refused one |
|
|
71
|
+
| `action_list` | `ActionListRules` (where the list is), `LegacyActionParser` and `StrictActionParser` | document key and legacy keys (with a `DeprecationWarning`); empty list raises or returns `{}`; error messages |
|
|
72
|
+
| `executor` | `ActionExecutor`: `execute_action`, `execute_files`, `add_command_to_executor` | `ExecutorSettings`: rules, parser, reporter, file reader, record key (`execute: …` or `execute[i]: …`), action rewrite |
|
|
73
|
+
| `reporting` | `LoggingReporter`, `PrintReporter`, or your own `ExecutionReporter` | where events, failures and records go |
|
|
74
|
+
| `package_manager` | `PackageManager`: load an installed package's members as commands, behind the [package gate](#package-gate) | member naming (`<package>_<member>` or bare), predicates, name check, errors to log |
|
|
75
|
+
| `callback` | `CallbackFunctionExecutor`: run a trigger, then a callback | legacy or strict checking; raise or log and return `None` |
|
|
76
|
+
| `json_io` | `ActionJsonFile`, `read_action_json`, `write_action_json`: UTF-8, locked, non-ASCII kept | exception class and message templates |
|
|
77
|
+
| `file_listing` | `get_dir_files_as_list`: action files under a directory | — |
|
|
78
|
+
| `socket_server` | `start_action_socket_server`: the plain TCP action server | executor, payload check, errors answered, oversize policy |
|
|
79
|
+
| `builtins_policy` | `SAFE_BUILTINS`: the builtins an action list may call | — |
|
|
80
|
+
|
|
81
|
+
`__all__` in `je_action_core/__init__.py` is the supported import surface.
|
|
82
|
+
|
|
83
|
+
## Package gate
|
|
84
|
+
|
|
85
|
+
`PackageManager.add_package_to_executor` imports a package and registers its members as commands. An action list
|
|
86
|
+
that can name `os` or `subprocess` could therefore run anything. The host program decides what may load:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
package_manager.allow_packages("my_helpers") # these, and their submodules
|
|
90
|
+
package_manager.set_allow_arbitrary_packages(False) # refuse everything else before importing it
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Neither switch should ever be exposed as an action command, so an action list cannot open its own gate. A
|
|
94
|
+
refused package raises the project's `refused` exception, which the executor records as that action's result.
|
|
95
|
+
Until the host calls either switch, any package still loads but raises a `DeprecationWarning`. A project can
|
|
96
|
+
turn the gate off (`PackageGate.OFF`) while it moves to it.
|
|
97
|
+
|
|
98
|
+
## Socket server protocol
|
|
99
|
+
|
|
100
|
+
`start_action_socket_server(host, port, settings)` serves on a daemon thread. A client sends one JSON action
|
|
101
|
+
document per connection. The server replies with each record value on its own line, then
|
|
102
|
+
`Return_Data_Over_JE`. A failure replies with the error text and the same marker. `quit_server` stops the server
|
|
103
|
+
and sets `close_flag`. The server has no authentication, so bind it only to a trust## Who uses it
|
|
104
|
+
|
|
105
|
+
APITestka (`AT_`, port 9939), LoadDensity (`LD_`), MailThunder (`MT_`, port 9942) and FileAutomation (`FA_`) are
|
|
106
|
+
the projects this package is for. `architecture.md` §6 lists which of them have moved to it and which pieces and
|
|
107
|
+
settings each one uses. LoadDensity and FileAutomation keep their own socket servers: a gevent server with
|
|
108
|
+
framing, a token and TLS, and a server with authentication and an ACL.
|
|
109
|
+
|
|
110
|
+
n and an ACL.
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install -e .
|
|
116
|
+
pip install pytest
|
|
117
|
+
python -m pytest test/
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`architecture.md` describes the layers and the contracts with the four projects. Outstanding work is in
|
|
121
|
+
`progress.md`, and finished work is recorded in `docs/updates/`.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
je_action_core: the keyword-driven action executor shared by APITestka, LoadDensity, MailThunder and
|
|
3
|
+
FileAutomation.
|
|
4
|
+
|
|
5
|
+
Each project configures these pieces with its own command prefix, document key, exceptions and messages;
|
|
6
|
+
``__all__`` is the supported import surface.
|
|
7
|
+
"""
|
|
8
|
+
from je_action_core.action_list import (
|
|
9
|
+
ActionListRules,
|
|
10
|
+
ActionParser,
|
|
11
|
+
BoundAction,
|
|
12
|
+
EmptyListPolicy,
|
|
13
|
+
LegacyActionParser,
|
|
14
|
+
ParsedAction,
|
|
15
|
+
StrictActionParser,
|
|
16
|
+
)
|
|
17
|
+
from je_action_core.builtins_policy import SAFE_BUILTINS, safe_builtin_commands
|
|
18
|
+
from je_action_core.callback import CallbackErrorPolicy, CallbackFunctionExecutor, CallbackSettings, CallbackStyle
|
|
19
|
+
from je_action_core.exceptions import (
|
|
20
|
+
ActionCoreException,
|
|
21
|
+
ActionExecuteException,
|
|
22
|
+
ActionJsonException,
|
|
23
|
+
AddCommandException,
|
|
24
|
+
CallbackExecutorException,
|
|
25
|
+
PackageNotAllowedException,
|
|
26
|
+
)
|
|
27
|
+
from je_action_core.executor import ActionExecutor, ExecutorSettings, indexed_record_key, plain_record_key
|
|
28
|
+
from je_action_core.file_listing import get_dir_files_as_list
|
|
29
|
+
from je_action_core.json_io import (
|
|
30
|
+
ActionJsonFile,
|
|
31
|
+
JsonFileMessages,
|
|
32
|
+
JsonFileSettings,
|
|
33
|
+
read_action_json,
|
|
34
|
+
write_action_json,
|
|
35
|
+
)
|
|
36
|
+
from je_action_core.package_manager import (
|
|
37
|
+
MemberNaming,
|
|
38
|
+
PackageGate,
|
|
39
|
+
PackageManager,
|
|
40
|
+
PackageManagerSettings,
|
|
41
|
+
is_identifier_path,
|
|
42
|
+
is_module_name,
|
|
43
|
+
)
|
|
44
|
+
from je_action_core.registry import Command, CommandPolicy, CommandRegistry
|
|
45
|
+
from je_action_core.reporting import ExecutionReporter, LoggingReporter, PrintReporter
|
|
46
|
+
from je_action_core.socket_server import (
|
|
47
|
+
END_MARKER,
|
|
48
|
+
MAX_PAYLOAD_BYTES,
|
|
49
|
+
QUIT_COMMAND,
|
|
50
|
+
ActionRequestHandler,
|
|
51
|
+
ActionTCPServer,
|
|
52
|
+
OversizePolicy,
|
|
53
|
+
SocketServerSettings,
|
|
54
|
+
start_action_socket_server,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"ActionListRules", "ActionParser", "BoundAction", "EmptyListPolicy", "LegacyActionParser", "ParsedAction",
|
|
59
|
+
"StrictActionParser",
|
|
60
|
+
"SAFE_BUILTINS", "safe_builtin_commands",
|
|
61
|
+
"CallbackErrorPolicy", "CallbackFunctionExecutor", "CallbackSettings", "CallbackStyle",
|
|
62
|
+
"ActionCoreException", "ActionExecuteException", "ActionJsonException", "AddCommandException",
|
|
63
|
+
"CallbackExecutorException", "PackageNotAllowedException",
|
|
64
|
+
"ActionExecutor", "ExecutorSettings", "indexed_record_key", "plain_record_key",
|
|
65
|
+
"get_dir_files_as_list",
|
|
66
|
+
"ActionJsonFile", "JsonFileMessages", "JsonFileSettings", "read_action_json", "write_action_json",
|
|
67
|
+
"MemberNaming", "PackageGate", "PackageManager", "PackageManagerSettings", "is_identifier_path",
|
|
68
|
+
"is_module_name",
|
|
69
|
+
"Command", "CommandPolicy", "CommandRegistry",
|
|
70
|
+
"ExecutionReporter", "LoggingReporter", "PrintReporter",
|
|
71
|
+
"END_MARKER", "MAX_PAYLOAD_BYTES", "QUIT_COMMAND", "ActionRequestHandler", "ActionTCPServer",
|
|
72
|
+
"OversizePolicy", "SocketServerSettings", "start_action_socket_server",
|
|
73
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Action documents and single actions.
|
|
3
|
+
|
|
4
|
+
An action document is a list of actions, or a mapping that keeps the list under one key (``{"api_testka": [...]}``).
|
|
5
|
+
An action is ``[name]``, ``[name, {kwargs}]`` or ``[name, [args]]``. :class:`ActionListRules` finds the list;
|
|
6
|
+
a parser binds one action to its command.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import warnings
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
from typing import Any, Callable, Iterable, Mapping, NamedTuple, Optional, Protocol, Tuple, Type
|
|
15
|
+
|
|
16
|
+
from je_action_core.exceptions import ActionExecuteException
|
|
17
|
+
|
|
18
|
+
Resolver = Callable[[Any], Optional[Callable[..., Any]]]
|
|
19
|
+
_NAME_ONLY: int = 1
|
|
20
|
+
_NAME_AND_PAYLOAD: int = 2
|
|
21
|
+
# warnings.warn -> from_document -> extract -> ActionExecutor.execute_action -> the caller of execute_action
|
|
22
|
+
_LEGACY_KEY_STACKLEVEL: int = 4
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EmptyListPolicy(Enum):
|
|
26
|
+
"""What an executor does with an empty action list, or with something that is not a list."""
|
|
27
|
+
|
|
28
|
+
RAISE = "raise"
|
|
29
|
+
RETURN_EMPTY = "return_empty" # run nothing and return an empty record
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ActionListRules:
|
|
34
|
+
"""
|
|
35
|
+
Where a document keeps its action list and what is wrong with a bad one.
|
|
36
|
+
|
|
37
|
+
The messages are ``str.format`` templates: ``missing_message`` gets ``key``, ``not_list_message`` gets
|
|
38
|
+
``type`` (the type name of what was found).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
key: str
|
|
42
|
+
legacy_keys: Tuple[str, ...] = ()
|
|
43
|
+
error: Type[Exception] = ActionExecuteException
|
|
44
|
+
missing_message: str = "action document has no {key!r} list"
|
|
45
|
+
not_list_message: str = "action list must be a list, got {type}"
|
|
46
|
+
empty_message: str = "action list is empty"
|
|
47
|
+
empty: EmptyListPolicy = EmptyListPolicy.RAISE
|
|
48
|
+
|
|
49
|
+
def extract(self, action_list: Any) -> Optional[list]:
|
|
50
|
+
"""
|
|
51
|
+
Return the actions to run, or ``None`` when :attr:`empty` says to run nothing.
|
|
52
|
+
|
|
53
|
+
:raises error: the document has no list under :attr:`key` (or a legacy key), or, under
|
|
54
|
+
:attr:`EmptyListPolicy.RAISE`, the list is not a list or is empty.
|
|
55
|
+
"""
|
|
56
|
+
actions = action_list
|
|
57
|
+
if isinstance(action_list, Mapping):
|
|
58
|
+
actions = self.from_document(action_list, stacklevel=_LEGACY_KEY_STACKLEVEL)
|
|
59
|
+
if actions is None:
|
|
60
|
+
raise self.error(self.missing_message.format(key=self.key))
|
|
61
|
+
if isinstance(actions, list) and actions:
|
|
62
|
+
return actions
|
|
63
|
+
if self.empty is EmptyListPolicy.RETURN_EMPTY:
|
|
64
|
+
return None
|
|
65
|
+
if not isinstance(actions, list):
|
|
66
|
+
raise self.error(self.not_list_message.format(type=type(actions).__name__))
|
|
67
|
+
raise self.error(self.empty_message)
|
|
68
|
+
|
|
69
|
+
def from_document(self, document: Mapping[str, Any], stacklevel: int = 2) -> Any:
|
|
70
|
+
"""
|
|
71
|
+
The value under :attr:`key`, else under the first legacy key present (with a ``DeprecationWarning``
|
|
72
|
+
attributed ``stacklevel`` frames up), else ``None``. The value is returned as found, unchecked.
|
|
73
|
+
"""
|
|
74
|
+
if self.key in document:
|
|
75
|
+
return document[self.key]
|
|
76
|
+
for legacy_key in self.legacy_keys:
|
|
77
|
+
if legacy_key in document:
|
|
78
|
+
warnings.warn(f'the "{legacy_key}" key is deprecated; use "{self.key}"', DeprecationWarning,
|
|
79
|
+
stacklevel=stacklevel)
|
|
80
|
+
return document[legacy_key]
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class BoundAction(NamedTuple):
|
|
85
|
+
"""An action bound to its command; calling it is ``command(*args, **kwargs)``."""
|
|
86
|
+
|
|
87
|
+
name: Any
|
|
88
|
+
command: Callable[..., Any]
|
|
89
|
+
args: Iterable[Any] = ()
|
|
90
|
+
kwargs: Mapping[str, Any] = MappingProxyType({})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ActionParser(Protocol):
|
|
94
|
+
"""Binds one action to its command."""
|
|
95
|
+
|
|
96
|
+
def bind(self, action: Any, resolve: Resolver) -> BoundAction:
|
|
97
|
+
"""Return ``action`` bound to the command ``resolve`` finds for its name; raise when it cannot."""
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class LegacyActionParser:
|
|
102
|
+
"""
|
|
103
|
+
The rules APITestka, LoadDensity and MailThunder have always used.
|
|
104
|
+
|
|
105
|
+
The name is looked up first. A dict payload is passed as keyword arguments and any other payload is
|
|
106
|
+
unpacked as positional arguments. An unknown name or more than two elements raises ``error`` with
|
|
107
|
+
``f"{message} {action}"``. An empty action raises ``IndexError``.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
error: Type[Exception] = ActionExecuteException
|
|
111
|
+
message: str = "invalid action"
|
|
112
|
+
|
|
113
|
+
def bind(self, action: Any, resolve: Resolver) -> BoundAction:
|
|
114
|
+
"""See the class docstring."""
|
|
115
|
+
command = resolve(action[0])
|
|
116
|
+
if command is None:
|
|
117
|
+
raise self.error(f"{self.message} {action}")
|
|
118
|
+
if len(action) == _NAME_AND_PAYLOAD:
|
|
119
|
+
payload = action[1]
|
|
120
|
+
if isinstance(payload, dict):
|
|
121
|
+
return BoundAction(action[0], command, (), payload)
|
|
122
|
+
return BoundAction(action[0], command, payload, {})
|
|
123
|
+
if len(action) == _NAME_ONLY:
|
|
124
|
+
return BoundAction(action[0], command)
|
|
125
|
+
raise self.error(f"{self.message} {action}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ParsedAction(NamedTuple):
|
|
129
|
+
"""A checked action: ``kind`` is ``"none"``, ``"kwargs"`` or ``"args"``."""
|
|
130
|
+
|
|
131
|
+
name: str
|
|
132
|
+
kind: str
|
|
133
|
+
payload: Any
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True)
|
|
137
|
+
class StrictActionParser:
|
|
138
|
+
"""
|
|
139
|
+
FileAutomation's rules: the action must be a non-empty list, the name a string and the payload a dict or a
|
|
140
|
+
list. The shape is checked before the name is looked up.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
error: Type[Exception] = ActionExecuteException
|
|
144
|
+
|
|
145
|
+
def parse(self, action: Any) -> ParsedAction:
|
|
146
|
+
"""Check the shape of ``action`` without looking its name up."""
|
|
147
|
+
if not isinstance(action, list) or not action:
|
|
148
|
+
raise self.error(f"malformed action: {action!r}")
|
|
149
|
+
name = action[0]
|
|
150
|
+
if not isinstance(name, str):
|
|
151
|
+
raise self.error(f"action name must be str: {action!r}")
|
|
152
|
+
if len(action) == _NAME_ONLY:
|
|
153
|
+
return ParsedAction(name, "none", None)
|
|
154
|
+
if len(action) == _NAME_AND_PAYLOAD:
|
|
155
|
+
payload = action[1]
|
|
156
|
+
if isinstance(payload, dict):
|
|
157
|
+
return ParsedAction(name, "kwargs", payload)
|
|
158
|
+
if isinstance(payload, list):
|
|
159
|
+
return ParsedAction(name, "args", payload)
|
|
160
|
+
raise self.error(f"action {name!r} payload must be dict or list, got {type(payload).__name__}")
|
|
161
|
+
raise self.error(f"action has too many elements: {action!r}")
|
|
162
|
+
|
|
163
|
+
def bind(self, action: Any, resolve: Resolver) -> BoundAction:
|
|
164
|
+
""":meth:`parse`, then look the name up; an unknown name raises ``error``."""
|
|
165
|
+
parsed = self.parse(action)
|
|
166
|
+
command = resolve(parsed.name)
|
|
167
|
+
if command is None:
|
|
168
|
+
raise self.error(f"unknown action: {parsed.name!r}")
|
|
169
|
+
if parsed.kind == "kwargs":
|
|
170
|
+
return BoundAction(parsed.name, command, (), parsed.payload)
|
|
171
|
+
if parsed.kind == "args":
|
|
172
|
+
return BoundAction(parsed.name, command, parsed.payload, {})
|
|
173
|
+
return BoundAction(parsed.name, command)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""The Python builtins an action list may call: one allowlist for every executor that registers builtins."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import builtins
|
|
5
|
+
from typing import Callable, Dict, FrozenSet
|
|
6
|
+
|
|
7
|
+
# Anything that can run code, reach attributes or namespaces, or touch files and stdin (eval, exec, compile,
|
|
8
|
+
# __import__, open, input, getattr, globals, ...) is left out, because action lists also arrive over sockets.
|
|
9
|
+
SAFE_BUILTINS: FrozenSet[str] = frozenset({
|
|
10
|
+
"abs", "all", "any", "ascii", "bin", "callable", "chr", "divmod",
|
|
11
|
+
"format", "hash", "hex", "len", "max", "min", "oct", "ord", "pow",
|
|
12
|
+
"print", "repr", "round", "sorted", "sum",
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def safe_builtin_commands() -> Dict[str, Callable[..., object]]:
|
|
17
|
+
"""Return ``{name: builtin}`` for every name in :data:`SAFE_BUILTINS`, in name order."""
|
|
18
|
+
return {name: getattr(builtins, name) for name in sorted(SAFE_BUILTINS)}
|