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.
Files changed (154) hide show
  1. aetherius/__init__.py +42 -0
  2. aetherius/__main__.py +12 -0
  3. aetherius/_contracts/__init__.py +0 -0
  4. aetherius/_contracts/blueprint.schema.json +144 -0
  5. aetherius/acts/__init__.py +1 -0
  6. aetherius/acts/_shared.py +61 -0
  7. aetherius/acts/continuum/__init__.py +7 -0
  8. aetherius/acts/continuum/actions.py +149 -0
  9. aetherius/acts/continuum/bridge.py +159 -0
  10. aetherius/acts/continuum/browser.py +164 -0
  11. aetherius/acts/continuum/debug_overlay.py +65 -0
  12. aetherius/acts/continuum/driver.py +127 -0
  13. aetherius/acts/continuum/human_actions.py +74 -0
  14. aetherius/acts/oracle/__init__.py +1 -0
  15. aetherius/acts/oracle/driver.py +1 -0
  16. aetherius/acts/oracle/locator.py +1 -0
  17. aetherius/acts/oracle/model.py +1 -0
  18. aetherius/acts/oracle/perception.py +1 -0
  19. aetherius/acts/phantom/__init__.py +1 -0
  20. aetherius/acts/phantom/driver.py +1 -0
  21. aetherius/acts/phantom/loop.py +1 -0
  22. aetherius/acts/phantom/memory.py +1 -0
  23. aetherius/acts/phantom/perception.py +1 -0
  24. aetherius/acts/phantom/planner.py +1 -0
  25. aetherius/acts/vector/__init__.py +1 -0
  26. aetherius/acts/vector/auth.py +108 -0
  27. aetherius/acts/vector/client.py +129 -0
  28. aetherius/acts/vector/driver.py +128 -0
  29. aetherius/builder/__init__.py +40 -0
  30. aetherius/builder/catalog.py +93 -0
  31. aetherius/builder/factory.py +243 -0
  32. aetherius/builder/templates.py +150 -0
  33. aetherius/builder/validation.py +120 -0
  34. aetherius/cli.py +236 -0
  35. aetherius/config/__init__.py +1 -0
  36. aetherius/config/secrets.py +71 -0
  37. aetherius/config/settings.py +44 -0
  38. aetherius/console/__init__.py +1 -0
  39. aetherius/console/app.py +66 -0
  40. aetherius/console/console.tcss +97 -0
  41. aetherius/console/daemon_control.py +83 -0
  42. aetherius/console/run_bridge.py +43 -0
  43. aetherius/console/screens/__init__.py +1 -0
  44. aetherius/console/screens/_pending.py +38 -0
  45. aetherius/console/screens/builder/__init__.py +1 -0
  46. aetherius/console/screens/builder/act_picker.py +75 -0
  47. aetherius/console/screens/builder/io_editor.py +167 -0
  48. aetherius/console/screens/builder/options_editor.py +132 -0
  49. aetherius/console/screens/builder/preview.py +57 -0
  50. aetherius/console/screens/builder/screen.py +235 -0
  51. aetherius/console/screens/builder/step_editor.py +249 -0
  52. aetherius/console/screens/catalog.py +52 -0
  53. aetherius/console/screens/home.py +90 -0
  54. aetherius/console/screens/library.py +124 -0
  55. aetherius/console/screens/library_scan.py +99 -0
  56. aetherius/console/screens/recorder.py +193 -0
  57. aetherius/console/screens/runs.py +138 -0
  58. aetherius/console/screens/sessions.py +14 -0
  59. aetherius/console/screens/settings.py +132 -0
  60. aetherius/console/screenshots.py +173 -0
  61. aetherius/console/theme.py +114 -0
  62. aetherius/console/widgets/__init__.py +1 -0
  63. aetherius/console/widgets/event_log.py +44 -0
  64. aetherius/console/widgets/form.py +101 -0
  65. aetherius/console/widgets/json_preview.py +18 -0
  66. aetherius/console/widgets/run_summary.py +82 -0
  67. aetherius/core/__init__.py +1 -0
  68. aetherius/core/actions/__init__.py +1 -0
  69. aetherius/core/actions/base.py +99 -0
  70. aetherius/core/actions/data.py +126 -0
  71. aetherius/core/actions/flow.py +76 -0
  72. aetherius/core/actions/interaction.py +109 -0
  73. aetherius/core/actions/navigation.py +32 -0
  74. aetherius/core/actions/registry.py +70 -0
  75. aetherius/core/actions/spec.py +42 -0
  76. aetherius/core/blueprint/__init__.py +1 -0
  77. aetherius/core/blueprint/loader.py +82 -0
  78. aetherius/core/blueprint/models.py +81 -0
  79. aetherius/core/blueprint/template.py +101 -0
  80. aetherius/core/blueprint/validator.py +45 -0
  81. aetherius/core/driver.py +36 -0
  82. aetherius/core/errors.py +134 -0
  83. aetherius/core/events/__init__.py +1 -0
  84. aetherius/core/events/bus.py +28 -0
  85. aetherius/core/events/models.py +29 -0
  86. aetherius/core/events/sinks.py +55 -0
  87. aetherius/core/extraction/__init__.py +1 -0
  88. aetherius/core/extraction/html_extractor.py +52 -0
  89. aetherius/core/extraction/json_extractor.py +125 -0
  90. aetherius/core/runtime/__init__.py +1 -0
  91. aetherius/core/runtime/context.py +61 -0
  92. aetherius/core/runtime/engine.py +188 -0
  93. aetherius/core/runtime/result.py +39 -0
  94. aetherius/core/runtime/selector.py +1 -0
  95. aetherius/models/__init__.py +1 -0
  96. aetherius/models/registry.py +1 -0
  97. aetherius/models/store/.gitkeep +0 -0
  98. aetherius/recorder/__init__.py +12 -0
  99. aetherius/recorder/_capture_js.py +80 -0
  100. aetherius/recorder/_gesture_js.py +40 -0
  101. aetherius/recorder/_names.py +20 -0
  102. aetherius/recorder/_overlay_js.py +262 -0
  103. aetherius/recorder/_playwright.py +49 -0
  104. aetherius/recorder/_selector_js.py +148 -0
  105. aetherius/recorder/_transform.py +214 -0
  106. aetherius/recorder/_vector_js.py +174 -0
  107. aetherius/recorder/base.py +92 -0
  108. aetherius/recorder/blueprint_recorder.py +72 -0
  109. aetherius/recorder/capture.py +59 -0
  110. aetherius/recorder/continuum_backend.py +119 -0
  111. aetherius/recorder/gesture_recorder.py +193 -0
  112. aetherius/recorder/selector_synth.py +83 -0
  113. aetherius/recorder/session.py +101 -0
  114. aetherius/recorder/vector_backend.py +158 -0
  115. aetherius/server/__init__.py +12 -0
  116. aetherius/server/app.py +37 -0
  117. aetherius/server/config.py +25 -0
  118. aetherius/server/deps.py +50 -0
  119. aetherius/server/jobs.py +155 -0
  120. aetherius/server/routes/__init__.py +1 -0
  121. aetherius/server/routes/blueprints.py +45 -0
  122. aetherius/server/routes/recorder.py +25 -0
  123. aetherius/server/routes/runs.py +49 -0
  124. aetherius/server/routes/stream.py +53 -0
  125. aetherius/server/schemas.py +78 -0
  126. aetherius/stealth/__init__.py +1 -0
  127. aetherius/stealth/fingerprint/__init__.py +1 -0
  128. aetherius/stealth/fingerprint/patch.py +38 -0
  129. aetherius/stealth/fingerprint/profile.py +95 -0
  130. aetherius/stealth/gestures/__init__.py +1 -0
  131. aetherius/stealth/gestures/data/human_library.json +7107 -0
  132. aetherius/stealth/gestures/library.py +126 -0
  133. aetherius/stealth/gestures/seed.py +97 -0
  134. aetherius/stealth/humanizer/__init__.py +1 -0
  135. aetherius/stealth/humanizer/input.py +89 -0
  136. aetherius/stealth/humanizer/keyboard.py +75 -0
  137. aetherius/stealth/humanizer/mouse.py +153 -0
  138. aetherius/stealth/humanizer/scroll.py +55 -0
  139. aetherius/stealth/humanizer/timing.py +54 -0
  140. aetherius/stealth/ml/__init__.py +1 -0
  141. aetherius/stealth/ml/fingerprint_model.py +1 -0
  142. aetherius/stealth/ml/motion_model.py +1 -0
  143. aetherius/stealth/policy.py +133 -0
  144. aetherius/stealth/session/__init__.py +1 -0
  145. aetherius/stealth/session/store.py +35 -0
  146. aetherius/stealth/session/warmup.py +52 -0
  147. aetherius/version.py +3 -0
  148. aetherius-0.2.0.data/data/aetherius/_contracts/__init__.py +0 -0
  149. aetherius-0.2.0.data/data/aetherius/_contracts/blueprint.schema.json +144 -0
  150. aetherius-0.2.0.dist-info/METADATA +606 -0
  151. aetherius-0.2.0.dist-info/RECORD +154 -0
  152. aetherius-0.2.0.dist-info/WHEEL +4 -0
  153. aetherius-0.2.0.dist-info/entry_points.txt +2 -0
  154. 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))