aetherius 0.2.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.
- aetherius/__init__.py +42 -0
- aetherius/__main__.py +12 -0
- aetherius/_contracts/__init__.py +0 -0
- aetherius/_contracts/blueprint.schema.json +144 -0
- aetherius/acts/__init__.py +1 -0
- aetherius/acts/_shared.py +61 -0
- aetherius/acts/continuum/__init__.py +7 -0
- aetherius/acts/continuum/actions.py +149 -0
- aetherius/acts/continuum/bridge.py +159 -0
- aetherius/acts/continuum/browser.py +164 -0
- aetherius/acts/continuum/debug_overlay.py +65 -0
- aetherius/acts/continuum/driver.py +127 -0
- aetherius/acts/continuum/human_actions.py +74 -0
- aetherius/acts/oracle/__init__.py +1 -0
- aetherius/acts/oracle/driver.py +1 -0
- aetherius/acts/oracle/locator.py +1 -0
- aetherius/acts/oracle/model.py +1 -0
- aetherius/acts/oracle/perception.py +1 -0
- aetherius/acts/phantom/__init__.py +1 -0
- aetherius/acts/phantom/driver.py +1 -0
- aetherius/acts/phantom/loop.py +1 -0
- aetherius/acts/phantom/memory.py +1 -0
- aetherius/acts/phantom/perception.py +1 -0
- aetherius/acts/phantom/planner.py +1 -0
- aetherius/acts/vector/__init__.py +1 -0
- aetherius/acts/vector/auth.py +108 -0
- aetherius/acts/vector/client.py +129 -0
- aetherius/acts/vector/driver.py +128 -0
- aetherius/builder/__init__.py +40 -0
- aetherius/builder/catalog.py +93 -0
- aetherius/builder/factory.py +243 -0
- aetherius/builder/templates.py +150 -0
- aetherius/builder/validation.py +120 -0
- aetherius/cli.py +236 -0
- aetherius/config/__init__.py +1 -0
- aetherius/config/secrets.py +71 -0
- aetherius/config/settings.py +44 -0
- aetherius/console/__init__.py +1 -0
- aetherius/console/app.py +66 -0
- aetherius/console/console.tcss +97 -0
- aetherius/console/daemon_control.py +83 -0
- aetherius/console/run_bridge.py +43 -0
- aetherius/console/screens/__init__.py +1 -0
- aetherius/console/screens/_pending.py +38 -0
- aetherius/console/screens/builder/__init__.py +1 -0
- aetherius/console/screens/builder/act_picker.py +75 -0
- aetherius/console/screens/builder/io_editor.py +167 -0
- aetherius/console/screens/builder/options_editor.py +132 -0
- aetherius/console/screens/builder/preview.py +57 -0
- aetherius/console/screens/builder/screen.py +235 -0
- aetherius/console/screens/builder/step_editor.py +249 -0
- aetherius/console/screens/catalog.py +52 -0
- aetherius/console/screens/home.py +90 -0
- aetherius/console/screens/library.py +124 -0
- aetherius/console/screens/library_scan.py +99 -0
- aetherius/console/screens/recorder.py +193 -0
- aetherius/console/screens/runs.py +138 -0
- aetherius/console/screens/sessions.py +14 -0
- aetherius/console/screens/settings.py +132 -0
- aetherius/console/screenshots.py +173 -0
- aetherius/console/theme.py +114 -0
- aetherius/console/widgets/__init__.py +1 -0
- aetherius/console/widgets/event_log.py +44 -0
- aetherius/console/widgets/form.py +101 -0
- aetherius/console/widgets/json_preview.py +18 -0
- aetherius/console/widgets/run_summary.py +82 -0
- aetherius/core/__init__.py +1 -0
- aetherius/core/actions/__init__.py +1 -0
- aetherius/core/actions/base.py +99 -0
- aetherius/core/actions/data.py +126 -0
- aetherius/core/actions/flow.py +76 -0
- aetherius/core/actions/interaction.py +109 -0
- aetherius/core/actions/navigation.py +32 -0
- aetherius/core/actions/registry.py +70 -0
- aetherius/core/actions/spec.py +42 -0
- aetherius/core/blueprint/__init__.py +1 -0
- aetherius/core/blueprint/loader.py +82 -0
- aetherius/core/blueprint/models.py +81 -0
- aetherius/core/blueprint/template.py +101 -0
- aetherius/core/blueprint/validator.py +45 -0
- aetherius/core/driver.py +36 -0
- aetherius/core/errors.py +134 -0
- aetherius/core/events/__init__.py +1 -0
- aetherius/core/events/bus.py +28 -0
- aetherius/core/events/models.py +29 -0
- aetherius/core/events/sinks.py +55 -0
- aetherius/core/extraction/__init__.py +1 -0
- aetherius/core/extraction/html_extractor.py +52 -0
- aetherius/core/extraction/json_extractor.py +125 -0
- aetherius/core/runtime/__init__.py +1 -0
- aetherius/core/runtime/context.py +61 -0
- aetherius/core/runtime/engine.py +188 -0
- aetherius/core/runtime/result.py +39 -0
- aetherius/core/runtime/selector.py +1 -0
- aetherius/models/__init__.py +1 -0
- aetherius/models/registry.py +1 -0
- aetherius/models/store/.gitkeep +0 -0
- aetherius/recorder/__init__.py +12 -0
- aetherius/recorder/_capture_js.py +80 -0
- aetherius/recorder/_gesture_js.py +40 -0
- aetherius/recorder/_names.py +20 -0
- aetherius/recorder/_overlay_js.py +262 -0
- aetherius/recorder/_playwright.py +49 -0
- aetherius/recorder/_selector_js.py +148 -0
- aetherius/recorder/_transform.py +214 -0
- aetherius/recorder/_vector_js.py +174 -0
- aetherius/recorder/base.py +92 -0
- aetherius/recorder/blueprint_recorder.py +72 -0
- aetherius/recorder/capture.py +59 -0
- aetherius/recorder/continuum_backend.py +119 -0
- aetherius/recorder/gesture_recorder.py +193 -0
- aetherius/recorder/selector_synth.py +83 -0
- aetherius/recorder/session.py +101 -0
- aetherius/recorder/vector_backend.py +158 -0
- aetherius/server/__init__.py +12 -0
- aetherius/server/app.py +37 -0
- aetherius/server/config.py +25 -0
- aetherius/server/deps.py +50 -0
- aetherius/server/jobs.py +155 -0
- aetherius/server/routes/__init__.py +1 -0
- aetherius/server/routes/blueprints.py +45 -0
- aetherius/server/routes/recorder.py +25 -0
- aetherius/server/routes/runs.py +49 -0
- aetherius/server/routes/stream.py +53 -0
- aetherius/server/schemas.py +78 -0
- aetherius/stealth/__init__.py +1 -0
- aetherius/stealth/fingerprint/__init__.py +1 -0
- aetherius/stealth/fingerprint/patch.py +38 -0
- aetherius/stealth/fingerprint/profile.py +95 -0
- aetherius/stealth/gestures/__init__.py +1 -0
- aetherius/stealth/gestures/data/human_library.json +7107 -0
- aetherius/stealth/gestures/library.py +126 -0
- aetherius/stealth/gestures/seed.py +97 -0
- aetherius/stealth/humanizer/__init__.py +1 -0
- aetherius/stealth/humanizer/input.py +89 -0
- aetherius/stealth/humanizer/keyboard.py +75 -0
- aetherius/stealth/humanizer/mouse.py +153 -0
- aetherius/stealth/humanizer/scroll.py +55 -0
- aetherius/stealth/humanizer/timing.py +54 -0
- aetherius/stealth/ml/__init__.py +1 -0
- aetherius/stealth/ml/fingerprint_model.py +1 -0
- aetherius/stealth/ml/motion_model.py +1 -0
- aetherius/stealth/policy.py +133 -0
- aetherius/stealth/session/__init__.py +1 -0
- aetherius/stealth/session/store.py +35 -0
- aetherius/stealth/session/warmup.py +52 -0
- aetherius/version.py +3 -0
- aetherius-0.2.0.data/data/aetherius/_contracts/__init__.py +0 -0
- aetherius-0.2.0.data/data/aetherius/_contracts/blueprint.schema.json +144 -0
- aetherius-0.2.0.dist-info/METADATA +606 -0
- aetherius-0.2.0.dist-info/RECORD +154 -0
- aetherius-0.2.0.dist-info/WHEEL +4 -0
- aetherius-0.2.0.dist-info/entry_points.txt +2 -0
- aetherius-0.2.0.dist-info/licenses/LICENSE +11 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Capability enum and ACT_CAPABILITIES map: the authoritative action/Act compatibility table."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Capability(str, Enum):
|
|
9
|
+
# ── Act I — Vector ──────────────────────────────────────────────────────────
|
|
10
|
+
HTTP_REQUEST = "http.request"
|
|
11
|
+
SET = "set"
|
|
12
|
+
ASSERT = "assert"
|
|
13
|
+
EMIT = "emit"
|
|
14
|
+
WAIT = "wait"
|
|
15
|
+
|
|
16
|
+
# ── Act II — Continuum (browser) ─────────────────────────────────────────
|
|
17
|
+
NAVIGATE = "navigate"
|
|
18
|
+
BACK = "back"
|
|
19
|
+
FORWARD = "forward"
|
|
20
|
+
RELOAD = "reload"
|
|
21
|
+
CLICK = "click"
|
|
22
|
+
FILL = "fill"
|
|
23
|
+
TYPE = "type"
|
|
24
|
+
PRESS = "press"
|
|
25
|
+
SELECT = "select"
|
|
26
|
+
HOVER = "hover"
|
|
27
|
+
SCROLL = "scroll"
|
|
28
|
+
UPLOAD = "upload"
|
|
29
|
+
DRAG = "drag"
|
|
30
|
+
SCREENSHOT = "screenshot"
|
|
31
|
+
EVALUATE = "evaluate"
|
|
32
|
+
WAIT_FOR = "wait_for"
|
|
33
|
+
|
|
34
|
+
# ── Flow (all Acts) ──────────────────────────────────────────────────────
|
|
35
|
+
IF = "if"
|
|
36
|
+
REPEAT = "repeat"
|
|
37
|
+
FOR_EACH = "for_each"
|
|
38
|
+
|
|
39
|
+
# ── Extract (all Acts) ───────────────────────────────────────────────────
|
|
40
|
+
EXTRACT = "extract"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_VECTOR_CAPS: frozenset[Capability] = frozenset(
|
|
44
|
+
{
|
|
45
|
+
Capability.HTTP_REQUEST,
|
|
46
|
+
Capability.SET,
|
|
47
|
+
Capability.ASSERT,
|
|
48
|
+
Capability.EMIT,
|
|
49
|
+
Capability.WAIT,
|
|
50
|
+
Capability.IF,
|
|
51
|
+
Capability.FOR_EACH,
|
|
52
|
+
Capability.EXTRACT,
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
_CONTINUUM_CAPS: frozenset[Capability] = _VECTOR_CAPS | frozenset(
|
|
57
|
+
{
|
|
58
|
+
Capability.NAVIGATE,
|
|
59
|
+
Capability.BACK,
|
|
60
|
+
Capability.FORWARD,
|
|
61
|
+
Capability.RELOAD,
|
|
62
|
+
Capability.CLICK,
|
|
63
|
+
Capability.FILL,
|
|
64
|
+
Capability.TYPE,
|
|
65
|
+
Capability.PRESS,
|
|
66
|
+
Capability.SELECT,
|
|
67
|
+
Capability.HOVER,
|
|
68
|
+
Capability.SCROLL,
|
|
69
|
+
Capability.UPLOAD,
|
|
70
|
+
Capability.DRAG,
|
|
71
|
+
Capability.SCREENSHOT,
|
|
72
|
+
Capability.EVALUATE,
|
|
73
|
+
Capability.WAIT_FOR,
|
|
74
|
+
Capability.REPEAT,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
ACT_CAPABILITIES: dict[str, frozenset[Capability]] = {
|
|
79
|
+
"vector": _VECTOR_CAPS,
|
|
80
|
+
"continuum": _CONTINUUM_CAPS,
|
|
81
|
+
# Oracle and Phantom inherit all Continuum capabilities plus vision-specific ones.
|
|
82
|
+
"oracle": _CONTINUUM_CAPS,
|
|
83
|
+
"phantom": _CONTINUUM_CAPS,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Actions listed in ACT_CAPABILITIES but not yet dispatched by the Act's driver: declaring them
|
|
88
|
+
# keeps the capability table forward-looking, but the builder must flag them "not runnable yet".
|
|
89
|
+
# ``extract`` (vector) is implemented inside ``http.request`` rather than as a standalone step;
|
|
90
|
+
# ``http.request`` (continuum) is inherited from the vector capability set but the browser driver
|
|
91
|
+
# does not run it yet. Guarded by tests/unit/acts/test_action_dispatch.py, which fails the moment a
|
|
92
|
+
# declared capability is neither dispatched nor listed here; shrink each entry as drivers catch up.
|
|
93
|
+
# Only runnable Acts appear (see IMPLEMENTED_ACTS).
|
|
94
|
+
PENDING_ACTIONS: dict[str, frozenset[Capability]] = {
|
|
95
|
+
"vector": frozenset({Capability.IF, Capability.FOR_EACH, Capability.EXTRACT}),
|
|
96
|
+
"continuum": frozenset(
|
|
97
|
+
{Capability.IF, Capability.FOR_EACH, Capability.REPEAT, Capability.HTTP_REQUEST}
|
|
98
|
+
),
|
|
99
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Data actions: http.request, extract, assert, set, screenshot, evaluate.
|
|
2
|
+
|
|
3
|
+
Specs projected by the builder catalogue. Object/array parameters (headers, form, outputs, ...) are
|
|
4
|
+
edited as JSON: the schema leaves their shape open on purpose, and the placeholders show the
|
|
5
|
+
expected form.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
from .spec import ActionSpec, ParamSpec
|
|
13
|
+
|
|
14
|
+
SPECS: Final[tuple[ActionSpec, ...]] = (
|
|
15
|
+
ActionSpec(
|
|
16
|
+
"http.request",
|
|
17
|
+
"Send an HTTP request and optionally extract from the response (Act I).",
|
|
18
|
+
params=(
|
|
19
|
+
ParamSpec("method", "string", default="GET", help="HTTP method.", placeholder="POST"),
|
|
20
|
+
ParamSpec(
|
|
21
|
+
"url",
|
|
22
|
+
"string",
|
|
23
|
+
required=True,
|
|
24
|
+
help="Request URL.",
|
|
25
|
+
placeholder="{{ vars.domain }}/api",
|
|
26
|
+
),
|
|
27
|
+
ParamSpec(
|
|
28
|
+
"headers",
|
|
29
|
+
"object",
|
|
30
|
+
help="Request headers.",
|
|
31
|
+
placeholder='{"Accept": "application/json"}',
|
|
32
|
+
),
|
|
33
|
+
ParamSpec("params", "object", help="Query-string parameters."),
|
|
34
|
+
ParamSpec(
|
|
35
|
+
"form", "object", help="URL-encoded form body (mutually exclusive with json)."
|
|
36
|
+
),
|
|
37
|
+
ParamSpec("json", "object", help="JSON body (mutually exclusive with form)."),
|
|
38
|
+
ParamSpec(
|
|
39
|
+
"expect", "object", help="Response assertions.", placeholder='{"status": 200}'
|
|
40
|
+
),
|
|
41
|
+
ParamSpec(
|
|
42
|
+
"extract",
|
|
43
|
+
"object",
|
|
44
|
+
help="Named JSONPath/CSS extractions from the response.",
|
|
45
|
+
placeholder='{"items": {"from": "json", "path": "$[*]"}}',
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
),
|
|
49
|
+
ActionSpec(
|
|
50
|
+
"extract",
|
|
51
|
+
"Read values from the current page's DOM into named outputs (Act II).",
|
|
52
|
+
params=(
|
|
53
|
+
ParamSpec(
|
|
54
|
+
"outputs",
|
|
55
|
+
"object",
|
|
56
|
+
required=True,
|
|
57
|
+
help="Map of output name to a selector spec (as: text/number/html/attr/list, "
|
|
58
|
+
"or each/fields for records).",
|
|
59
|
+
placeholder='{"title": {"selector": "h1", "as": "text"}}',
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
ActionSpec(
|
|
64
|
+
"assert",
|
|
65
|
+
"Fail the run unless a rendered condition is truthy.",
|
|
66
|
+
params=(
|
|
67
|
+
ParamSpec(
|
|
68
|
+
"condition",
|
|
69
|
+
"string",
|
|
70
|
+
required=True,
|
|
71
|
+
help="Expression that must be truthy.",
|
|
72
|
+
placeholder="{{ steps.week.status_code == 200 }}",
|
|
73
|
+
),
|
|
74
|
+
ParamSpec("message", "string", help="Message shown when the assertion fails."),
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
ActionSpec(
|
|
78
|
+
"set",
|
|
79
|
+
"Store a rendered value under this step's id, for later interpolation.",
|
|
80
|
+
params=(
|
|
81
|
+
ParamSpec(
|
|
82
|
+
"value",
|
|
83
|
+
"string",
|
|
84
|
+
required=True,
|
|
85
|
+
help="Value to store.",
|
|
86
|
+
placeholder="{{ inputs.group }}",
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
ActionSpec(
|
|
91
|
+
"screenshot",
|
|
92
|
+
"Capture a screenshot artifact of the page or an element (Act II).",
|
|
93
|
+
params=(
|
|
94
|
+
ParamSpec(
|
|
95
|
+
"name", "string", help="File name for the artifact (defaults to the step id)."
|
|
96
|
+
),
|
|
97
|
+
ParamSpec("selector", "string", help="Element to capture; omit for the whole page."),
|
|
98
|
+
ParamSpec(
|
|
99
|
+
"full_page",
|
|
100
|
+
"boolean",
|
|
101
|
+
default=False,
|
|
102
|
+
help="Capture the full scrollable page when no selector is given.",
|
|
103
|
+
),
|
|
104
|
+
ParamSpec(
|
|
105
|
+
"selector_type",
|
|
106
|
+
"string",
|
|
107
|
+
default="css",
|
|
108
|
+
help="How to read 'selector': css (default), xpath or text.",
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
ActionSpec(
|
|
113
|
+
"evaluate",
|
|
114
|
+
"Run injected JavaScript in the page and return its result (Act II).",
|
|
115
|
+
params=(
|
|
116
|
+
ParamSpec(
|
|
117
|
+
"script",
|
|
118
|
+
"string",
|
|
119
|
+
required=True,
|
|
120
|
+
help="JavaScript expression or function.",
|
|
121
|
+
placeholder="() => document.title",
|
|
122
|
+
),
|
|
123
|
+
ParamSpec("arg", "object", help="Optional argument passed to the script."),
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Control-flow actions: wait, wait_for, emit, if, repeat, for_each.
|
|
2
|
+
|
|
3
|
+
Specs projected by the builder catalogue. ``if``/``repeat``/``for_each`` are declared in the
|
|
4
|
+
capability table but not executed by any driver yet, so the builder marks them "not runnable yet";
|
|
5
|
+
their parameter shapes here are indicative and will firm up when a driver implements them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
from .spec import ActionSpec, ParamSpec
|
|
13
|
+
|
|
14
|
+
SPECS: Final[tuple[ActionSpec, ...]] = (
|
|
15
|
+
ActionSpec(
|
|
16
|
+
"wait",
|
|
17
|
+
"Pause for a fixed number of milliseconds.",
|
|
18
|
+
params=(ParamSpec("ms", "number", help="Milliseconds to wait.", placeholder="1000"),),
|
|
19
|
+
),
|
|
20
|
+
ActionSpec(
|
|
21
|
+
"wait_for",
|
|
22
|
+
"Block until a selector reaches a state (Act II).",
|
|
23
|
+
params=(
|
|
24
|
+
ParamSpec(
|
|
25
|
+
"selector",
|
|
26
|
+
"string",
|
|
27
|
+
required=True,
|
|
28
|
+
help="Selector to wait for.",
|
|
29
|
+
placeholder=".results",
|
|
30
|
+
),
|
|
31
|
+
ParamSpec(
|
|
32
|
+
"state",
|
|
33
|
+
"string",
|
|
34
|
+
default="visible",
|
|
35
|
+
help="Target state: visible, attached, hidden or detached.",
|
|
36
|
+
),
|
|
37
|
+
ParamSpec("timeout_ms", "number", help="Give up after this many milliseconds."),
|
|
38
|
+
ParamSpec(
|
|
39
|
+
"on_timeout", "string", help="Named failure on timeout, e.g. 'fail:LOGIN_FAILED'."
|
|
40
|
+
),
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
ActionSpec(
|
|
44
|
+
"emit",
|
|
45
|
+
"Emit a progress event with a message.",
|
|
46
|
+
params=(
|
|
47
|
+
ParamSpec("message", "string", help="Event message.", placeholder="LOGIN_SUCCESS"),
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
ActionSpec(
|
|
51
|
+
"if",
|
|
52
|
+
"Run nested steps conditionally (declared, not runnable yet).",
|
|
53
|
+
params=(
|
|
54
|
+
ParamSpec("condition", "string", help="Expression deciding the branch."),
|
|
55
|
+
ParamSpec("then", "array", help="Steps to run when the condition holds."),
|
|
56
|
+
ParamSpec("else", "array", help="Steps to run otherwise."),
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
ActionSpec(
|
|
60
|
+
"repeat",
|
|
61
|
+
"Run nested steps a fixed number of times (declared, not runnable yet).",
|
|
62
|
+
params=(
|
|
63
|
+
ParamSpec("times", "integer", help="How many iterations."),
|
|
64
|
+
ParamSpec("steps", "array", help="Steps to repeat."),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
ActionSpec(
|
|
68
|
+
"for_each",
|
|
69
|
+
"Run nested steps once per item (declared, not runnable yet).",
|
|
70
|
+
params=(
|
|
71
|
+
ParamSpec("items", "string", help="Expression yielding the items to iterate."),
|
|
72
|
+
ParamSpec("as", "string", help="Loop variable name."),
|
|
73
|
+
ParamSpec("steps", "array", help="Steps to run per item."),
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Interaction actions: click, fill, type, press, select, hover, scroll, upload, drag.
|
|
2
|
+
|
|
3
|
+
Specs projected by the builder catalogue. Parameter names mirror the Playwright page operations in
|
|
4
|
+
``acts/continuum/actions.py`` exactly, so the forms produce steps the driver runs unchanged.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Final
|
|
10
|
+
|
|
11
|
+
from .spec import ActionSpec, ParamSpec
|
|
12
|
+
|
|
13
|
+
# Selector-based actions share these two parameters; declared once to stay in sync.
|
|
14
|
+
_SELECTOR = ParamSpec(
|
|
15
|
+
"selector", "string", required=True, help="Target element selector.", placeholder="#submit"
|
|
16
|
+
)
|
|
17
|
+
_SELECTOR_TYPE = ParamSpec(
|
|
18
|
+
"selector_type",
|
|
19
|
+
"string",
|
|
20
|
+
default="css",
|
|
21
|
+
help="How to read 'selector': css (default), xpath or text.",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
SPECS: Final[tuple[ActionSpec, ...]] = (
|
|
25
|
+
ActionSpec("click", "Click an element.", params=(_SELECTOR, _SELECTOR_TYPE)),
|
|
26
|
+
ActionSpec(
|
|
27
|
+
"fill",
|
|
28
|
+
"Set an input's value in one shot (clears it first).",
|
|
29
|
+
params=(
|
|
30
|
+
_SELECTOR,
|
|
31
|
+
ParamSpec(
|
|
32
|
+
"value",
|
|
33
|
+
"string",
|
|
34
|
+
required=True,
|
|
35
|
+
help="Value to set.",
|
|
36
|
+
placeholder="{{ inputs.query }}",
|
|
37
|
+
),
|
|
38
|
+
_SELECTOR_TYPE,
|
|
39
|
+
),
|
|
40
|
+
),
|
|
41
|
+
ActionSpec(
|
|
42
|
+
"type",
|
|
43
|
+
"Type text character by character (for fields that react to each keystroke).",
|
|
44
|
+
params=(
|
|
45
|
+
_SELECTOR,
|
|
46
|
+
ParamSpec("text", "string", required=True, help="Text to type."),
|
|
47
|
+
ParamSpec("delay_ms", "number", help="Delay between keystrokes, in milliseconds."),
|
|
48
|
+
_SELECTOR_TYPE,
|
|
49
|
+
),
|
|
50
|
+
),
|
|
51
|
+
ActionSpec(
|
|
52
|
+
"press",
|
|
53
|
+
"Press a keyboard key, optionally focused on an element.",
|
|
54
|
+
params=(
|
|
55
|
+
ParamSpec("key", "string", required=True, help="Key name.", placeholder="Enter"),
|
|
56
|
+
ParamSpec("selector", "string", help="Element to focus before pressing (optional)."),
|
|
57
|
+
_SELECTOR_TYPE,
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
ActionSpec(
|
|
61
|
+
"select",
|
|
62
|
+
"Choose one or more options in a <select> element.",
|
|
63
|
+
params=(
|
|
64
|
+
_SELECTOR,
|
|
65
|
+
ParamSpec(
|
|
66
|
+
"values",
|
|
67
|
+
"array",
|
|
68
|
+
required=True,
|
|
69
|
+
help="Option value(s) to select.",
|
|
70
|
+
placeholder='["option-1"]',
|
|
71
|
+
),
|
|
72
|
+
_SELECTOR_TYPE,
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
ActionSpec("hover", "Move the pointer over an element.", params=(_SELECTOR, _SELECTOR_TYPE)),
|
|
76
|
+
ActionSpec(
|
|
77
|
+
"scroll",
|
|
78
|
+
"Scroll an element into view, or scroll the page by a delta.",
|
|
79
|
+
params=(
|
|
80
|
+
ParamSpec("selector", "string", help="Element to scroll into view (optional)."),
|
|
81
|
+
ParamSpec("dx", "number", help="Horizontal wheel delta when no selector is given."),
|
|
82
|
+
ParamSpec("dy", "number", help="Vertical wheel delta when no selector is given."),
|
|
83
|
+
_SELECTOR_TYPE,
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
ActionSpec(
|
|
87
|
+
"upload",
|
|
88
|
+
"Set the file(s) of a file input.",
|
|
89
|
+
params=(
|
|
90
|
+
_SELECTOR,
|
|
91
|
+
ParamSpec(
|
|
92
|
+
"file",
|
|
93
|
+
"string",
|
|
94
|
+
required=True,
|
|
95
|
+
help="Path to the file to upload.",
|
|
96
|
+
placeholder="{{ inputs.video }}",
|
|
97
|
+
),
|
|
98
|
+
_SELECTOR_TYPE,
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
ActionSpec(
|
|
102
|
+
"drag",
|
|
103
|
+
"Drag one element onto another.",
|
|
104
|
+
params=(
|
|
105
|
+
ParamSpec("source", "string", required=True, help="Selector to drag from."),
|
|
106
|
+
ParamSpec("target", "string", required=True, help="Selector to drop onto."),
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Navigation actions: navigate, back, forward, reload. Specs projected by the builder catalogue."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Final
|
|
6
|
+
|
|
7
|
+
from .spec import ActionSpec, ParamSpec
|
|
8
|
+
|
|
9
|
+
SPECS: Final[tuple[ActionSpec, ...]] = (
|
|
10
|
+
ActionSpec(
|
|
11
|
+
"navigate",
|
|
12
|
+
"Open a URL in the browser.",
|
|
13
|
+
params=(
|
|
14
|
+
ParamSpec(
|
|
15
|
+
"url",
|
|
16
|
+
"string",
|
|
17
|
+
required=True,
|
|
18
|
+
help="Absolute URL to open.",
|
|
19
|
+
placeholder="https://example.com",
|
|
20
|
+
),
|
|
21
|
+
ParamSpec(
|
|
22
|
+
"wait_until",
|
|
23
|
+
"string",
|
|
24
|
+
default="load",
|
|
25
|
+
help="Load state to wait for: load, domcontentloaded, networkidle or commit.",
|
|
26
|
+
),
|
|
27
|
+
),
|
|
28
|
+
),
|
|
29
|
+
ActionSpec("back", "Go back one entry in the browser history."),
|
|
30
|
+
ActionSpec("forward", "Go forward one entry in the browser history."),
|
|
31
|
+
ActionSpec("reload", "Reload the current page."),
|
|
32
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Central action registry: single source of truth for all supported action handlers.
|
|
2
|
+
|
|
3
|
+
Handlers self-register via the @register decorator at module import time.
|
|
4
|
+
The engine imports the relevant driver module to trigger registration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable
|
|
10
|
+
|
|
11
|
+
from ..errors import ActionError
|
|
12
|
+
from .spec import ActionSpec
|
|
13
|
+
|
|
14
|
+
# action name → handler callable
|
|
15
|
+
ActionHandler = Callable[..., dict[str, Any]]
|
|
16
|
+
|
|
17
|
+
_registry: dict[str, ActionHandler] = {}
|
|
18
|
+
|
|
19
|
+
# action name → declarative field spec, aggregated lazily from the family modules on first use.
|
|
20
|
+
_specs: dict[str, ActionSpec] | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register(name: str) -> Callable[[ActionHandler], ActionHandler]:
|
|
24
|
+
"""Decorator that registers a handler function under *name* in the registry."""
|
|
25
|
+
|
|
26
|
+
def decorator(fn: ActionHandler) -> ActionHandler:
|
|
27
|
+
_registry[name] = fn
|
|
28
|
+
return fn
|
|
29
|
+
|
|
30
|
+
return decorator
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_handler(action_name: str) -> ActionHandler:
|
|
34
|
+
"""Return the handler for *action_name* or raise ActionError."""
|
|
35
|
+
try:
|
|
36
|
+
return _registry[action_name]
|
|
37
|
+
except KeyError:
|
|
38
|
+
raise ActionError(f"Unknown action: {action_name!r}. Available: {sorted(_registry)}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def registered_actions() -> list[str]:
|
|
42
|
+
"""Return a sorted list of all registered action names."""
|
|
43
|
+
return sorted(_registry)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def action_specs() -> dict[str, ActionSpec]:
|
|
47
|
+
"""Return every action's field spec, keyed by name.
|
|
48
|
+
|
|
49
|
+
Aggregates the per-family ``SPECS`` tuples lazily (imported here, not at module load, to keep
|
|
50
|
+
``import aetherius`` light) and caches the result. The union covers exactly the Capability set,
|
|
51
|
+
a bijection guarded by tests/unit/core/actions/test_specs.py.
|
|
52
|
+
"""
|
|
53
|
+
global _specs
|
|
54
|
+
if _specs is None:
|
|
55
|
+
from . import data, flow, interaction, navigation
|
|
56
|
+
|
|
57
|
+
collected: dict[str, ActionSpec] = {}
|
|
58
|
+
for module in (navigation, interaction, data, flow):
|
|
59
|
+
for spec in module.SPECS:
|
|
60
|
+
collected[spec.name] = spec
|
|
61
|
+
_specs = collected
|
|
62
|
+
return _specs
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_spec(name: str) -> ActionSpec:
|
|
66
|
+
"""Return the field spec for *name* or raise ActionError."""
|
|
67
|
+
try:
|
|
68
|
+
return action_specs()[name]
|
|
69
|
+
except KeyError:
|
|
70
|
+
raise ActionError(f"Unknown action: {name!r}. Available: {sorted(action_specs())}")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Declarative field specs for actions: the typed metadata the builder projects into forms.
|
|
2
|
+
|
|
3
|
+
The action *registry* dispatches by name; this module describes each action's parameters (name,
|
|
4
|
+
kind, whether required, a one-line help, an example placeholder) so a UI can generate a form and a
|
|
5
|
+
validator can flag a missing required field before the step ever runs. The specs are the single
|
|
6
|
+
source the builder catalogue is a projection of — they carry no behaviour, only shape.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
# Object/array params are edited as JSON (their shape is left open by the schema on purpose);
|
|
15
|
+
# scalar kinds map to a flat form field.
|
|
16
|
+
ParamKind = Literal["string", "number", "integer", "boolean", "object", "array"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ParamSpec:
|
|
21
|
+
"""One parameter of an action: how a form renders it and how a validator checks it."""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
kind: ParamKind
|
|
25
|
+
required: bool = False
|
|
26
|
+
help: str = ""
|
|
27
|
+
default: Any = None
|
|
28
|
+
# Example value shown to guide the user; a JSON snippet for object/array params.
|
|
29
|
+
placeholder: str = ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ActionSpec:
|
|
34
|
+
"""An action's name, a one-line summary, and its ordered parameters."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
summary: str
|
|
38
|
+
params: tuple[ParamSpec, ...] = field(default_factory=tuple)
|
|
39
|
+
|
|
40
|
+
def required_params(self) -> tuple[ParamSpec, ...]:
|
|
41
|
+
"""The subset of parameters a Blueprint step must provide for this action."""
|
|
42
|
+
return tuple(p for p in self.params if p.required)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Blueprint subsystem: typed model, loader, semantic validator and template interpolation for the instruction files."""
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Load and parse a Blueprint from a JSON or YAML file, validate it against the JSON Schema,
|
|
2
|
+
then return a typed Blueprint model."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import importlib.resources
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import jsonschema
|
|
12
|
+
import yaml
|
|
13
|
+
from pydantic import ValidationError
|
|
14
|
+
|
|
15
|
+
from ..errors import BlueprintLoadError, BlueprintSchemaError
|
|
16
|
+
from .models import Blueprint
|
|
17
|
+
|
|
18
|
+
_schema: dict[str, Any] | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def blueprint_schema() -> dict[str, Any]:
|
|
22
|
+
"""Return the packaged Blueprint JSON Schema (cached singleton)."""
|
|
23
|
+
global _schema
|
|
24
|
+
if _schema is None:
|
|
25
|
+
pkg = importlib.resources.files("aetherius._contracts")
|
|
26
|
+
schema_text = (pkg / "blueprint.schema.json").read_text(encoding="utf-8")
|
|
27
|
+
_schema = json.loads(schema_text)
|
|
28
|
+
return _schema
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_schema() -> dict[str, Any]:
|
|
32
|
+
"""Deprecated internal alias for blueprint_schema(); kept for existing callers."""
|
|
33
|
+
return blueprint_schema()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_blueprint_data(data: Any, *, source: str = "<data>") -> Blueprint:
|
|
37
|
+
"""Validate an in-memory Blueprint dict against the schema and model, returning the model.
|
|
38
|
+
|
|
39
|
+
The counterpart to :func:`load_blueprint` for data that never touched disk (the builder assembles
|
|
40
|
+
a Blueprint before there is a file). *source* only labels error messages.
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
BlueprintSchemaError: JSON Schema violation or Pydantic constraint failure.
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
jsonschema.validate(data, blueprint_schema())
|
|
47
|
+
except jsonschema.ValidationError as exc:
|
|
48
|
+
raise BlueprintSchemaError(
|
|
49
|
+
f"Blueprint schema violation in {source}: {exc.message}"
|
|
50
|
+
) from exc
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
return Blueprint.model_validate(data)
|
|
54
|
+
except ValidationError as exc:
|
|
55
|
+
raise BlueprintSchemaError(f"Blueprint model validation failed in {source}: {exc}") from exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_blueprint(path: str | Path) -> Blueprint:
|
|
59
|
+
"""Parse a Blueprint file (JSON or YAML) and return a validated model.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
BlueprintLoadError: file not found or not parseable.
|
|
63
|
+
BlueprintSchemaError: JSON Schema violation or Pydantic constraint failure.
|
|
64
|
+
"""
|
|
65
|
+
path = Path(path)
|
|
66
|
+
try:
|
|
67
|
+
raw = path.read_text(encoding="utf-8")
|
|
68
|
+
except FileNotFoundError:
|
|
69
|
+
raise BlueprintLoadError(f"Blueprint file not found: {path}")
|
|
70
|
+
except OSError as exc:
|
|
71
|
+
raise BlueprintLoadError(f"Cannot read Blueprint file: {path} — {exc}") from exc
|
|
72
|
+
|
|
73
|
+
suffix = path.suffix.lower()
|
|
74
|
+
try:
|
|
75
|
+
if suffix in {".yaml", ".yml"}:
|
|
76
|
+
data: Any = yaml.safe_load(raw)
|
|
77
|
+
else:
|
|
78
|
+
data = json.loads(raw)
|
|
79
|
+
except (json.JSONDecodeError, yaml.YAMLError) as exc:
|
|
80
|
+
raise BlueprintLoadError(f"Cannot parse Blueprint file: {path} — {exc}") from exc
|
|
81
|
+
|
|
82
|
+
return validate_blueprint_data(data, source=str(path))
|