androidctl 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. androidctl/__init__.py +5 -0
  2. androidctl/__main__.py +4 -0
  3. androidctl/_version.py +1 -0
  4. androidctl/app.py +73 -0
  5. androidctl/cli_options.py +27 -0
  6. androidctl/command_payloads.py +264 -0
  7. androidctl/command_views.py +157 -0
  8. androidctl/commands/__init__.py +1 -0
  9. androidctl/commands/actions.py +236 -0
  10. androidctl/commands/adb_wireless.py +157 -0
  11. androidctl/commands/close.py +30 -0
  12. androidctl/commands/connect.py +69 -0
  13. androidctl/commands/execute.py +179 -0
  14. androidctl/commands/list_apps.py +26 -0
  15. androidctl/commands/observe.py +26 -0
  16. androidctl/commands/open.py +41 -0
  17. androidctl/commands/plumbing.py +58 -0
  18. androidctl/commands/run_pipeline.py +307 -0
  19. androidctl/commands/screenshot.py +29 -0
  20. androidctl/commands/setup.py +301 -0
  21. androidctl/commands/wait.py +60 -0
  22. androidctl/daemon/__init__.py +1 -0
  23. androidctl/daemon/client.py +348 -0
  24. androidctl/daemon/discovery.py +190 -0
  25. androidctl/daemon/launcher.py +26 -0
  26. androidctl/daemon/owner.py +349 -0
  27. androidctl/errors/__init__.py +1 -0
  28. androidctl/errors/mapping.py +149 -0
  29. androidctl/errors/models.py +16 -0
  30. androidctl/exit_codes.py +8 -0
  31. androidctl/output.py +147 -0
  32. androidctl/parsing/__init__.py +1 -0
  33. androidctl/parsing/duration.py +17 -0
  34. androidctl/parsing/open_target.py +51 -0
  35. androidctl/parsing/refs.py +12 -0
  36. androidctl/parsing/screen_id.py +10 -0
  37. androidctl/parsing/wait.py +70 -0
  38. androidctl/renderers/__init__.py +110 -0
  39. androidctl/renderers/_paths.py +109 -0
  40. androidctl/renderers/xml.py +234 -0
  41. androidctl/renderers/xml_projection.py +732 -0
  42. androidctl/resources/__init__.py +1 -0
  43. androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
  44. androidctl/setup/__init__.py +1 -0
  45. androidctl/setup/accessibility.py +159 -0
  46. androidctl/setup/adb.py +586 -0
  47. androidctl/setup/apk_resource.py +29 -0
  48. androidctl/setup/pairing.py +70 -0
  49. androidctl/setup/verify.py +175 -0
  50. androidctl/workspace/__init__.py +3 -0
  51. androidctl/workspace/resolve.py +27 -0
  52. androidctl-0.1.0.dist-info/METADATA +217 -0
  53. androidctl-0.1.0.dist-info/RECORD +187 -0
  54. androidctl-0.1.0.dist-info/WHEEL +5 -0
  55. androidctl-0.1.0.dist-info/entry_points.txt +3 -0
  56. androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
  57. androidctl-0.1.0.dist-info/top_level.txt +3 -0
  58. androidctl_contracts/__init__.py +55 -0
  59. androidctl_contracts/_version.py +1 -0
  60. androidctl_contracts/_wire_helpers.py +31 -0
  61. androidctl_contracts/base.py +142 -0
  62. androidctl_contracts/command_catalog.py +414 -0
  63. androidctl_contracts/command_results.py +630 -0
  64. androidctl_contracts/daemon_api.py +335 -0
  65. androidctl_contracts/errors.py +44 -0
  66. androidctl_contracts/paths.py +5 -0
  67. androidctl_contracts/public_screen.py +579 -0
  68. androidctl_contracts/user_state.py +23 -0
  69. androidctl_contracts/vocabulary.py +82 -0
  70. androidctld/__init__.py +5 -0
  71. androidctld/__main__.py +63 -0
  72. androidctld/_version.py +1 -0
  73. androidctld/actions/__init__.py +1 -0
  74. androidctld/actions/action_target.py +142 -0
  75. androidctld/actions/capabilities.py +539 -0
  76. androidctld/actions/executor.py +894 -0
  77. androidctld/actions/focus_confirmation.py +177 -0
  78. androidctld/actions/focused_input_admissibility.py +120 -0
  79. androidctld/actions/fresh_current.py +176 -0
  80. androidctld/actions/postconditions.py +473 -0
  81. androidctld/actions/repair.py +101 -0
  82. androidctld/actions/request_builder.py +204 -0
  83. androidctld/actions/settle.py +146 -0
  84. androidctld/actions/submit_confirmation.py +211 -0
  85. androidctld/actions/submit_routing.py +311 -0
  86. androidctld/actions/type_confirmation.py +257 -0
  87. androidctld/app_targets.py +71 -0
  88. androidctld/artifacts/__init__.py +1 -0
  89. androidctld/artifacts/models.py +26 -0
  90. androidctld/artifacts/screen_lookup.py +241 -0
  91. androidctld/artifacts/screen_payloads.py +109 -0
  92. androidctld/artifacts/writer.py +286 -0
  93. androidctld/auth/__init__.py +1 -0
  94. androidctld/auth/active_registry.py +266 -0
  95. androidctld/auth/secret_files.py +52 -0
  96. androidctld/auth/token_store.py +59 -0
  97. androidctld/commands/__init__.py +1 -0
  98. androidctld/commands/assembly.py +231 -0
  99. androidctld/commands/command_models.py +254 -0
  100. androidctld/commands/dispatch.py +99 -0
  101. androidctld/commands/executor.py +31 -0
  102. androidctld/commands/from_boundary.py +175 -0
  103. androidctld/commands/handlers/__init__.py +15 -0
  104. androidctld/commands/handlers/action.py +439 -0
  105. androidctld/commands/handlers/connect.py +94 -0
  106. androidctld/commands/handlers/list_apps.py +215 -0
  107. androidctld/commands/handlers/observe.py +121 -0
  108. androidctld/commands/handlers/screenshot.py +105 -0
  109. androidctld/commands/handlers/wait.py +286 -0
  110. androidctld/commands/models.py +65 -0
  111. androidctld/commands/open_targets.py +56 -0
  112. androidctld/commands/orchestration.py +353 -0
  113. androidctld/commands/registry.py +116 -0
  114. androidctld/commands/result_builders.py +40 -0
  115. androidctld/commands/result_models.py +555 -0
  116. androidctld/commands/results.py +108 -0
  117. androidctld/commands/semantic_command_names.py +17 -0
  118. androidctld/commands/semantic_error_mapping.py +93 -0
  119. androidctld/commands/semantic_truth.py +135 -0
  120. androidctld/commands/service.py +67 -0
  121. androidctld/config.py +75 -0
  122. androidctld/daemon/__init__.py +1 -0
  123. androidctld/daemon/active_slot.py +326 -0
  124. androidctld/daemon/envelope.py +30 -0
  125. androidctld/daemon/http_host.py +123 -0
  126. androidctld/daemon/ingress.py +112 -0
  127. androidctld/daemon/ownership_probe.py +204 -0
  128. androidctld/daemon/server.py +286 -0
  129. androidctld/daemon/service.py +99 -0
  130. androidctld/device/__init__.py +1 -0
  131. androidctld/device/action_models.py +154 -0
  132. androidctld/device/action_serialization.py +121 -0
  133. androidctld/device/adapters.py +220 -0
  134. androidctld/device/bootstrap.py +153 -0
  135. androidctld/device/connectors.py +231 -0
  136. androidctld/device/errors.py +100 -0
  137. androidctld/device/interfaces.py +58 -0
  138. androidctld/device/parsing.py +320 -0
  139. androidctld/device/rpc.py +483 -0
  140. androidctld/device/schema.py +114 -0
  141. androidctld/device/types.py +161 -0
  142. androidctld/errors/__init__.py +94 -0
  143. androidctld/logging/__init__.py +22 -0
  144. androidctld/observation.py +98 -0
  145. androidctld/protocol.py +53 -0
  146. androidctld/refs/__init__.py +1 -0
  147. androidctld/refs/models.py +54 -0
  148. androidctld/refs/repair.py +284 -0
  149. androidctld/refs/service.py +422 -0
  150. androidctld/rendering/__init__.py +1 -0
  151. androidctld/rendering/screen_xml.py +256 -0
  152. androidctld/runtime/__init__.py +21 -0
  153. androidctld/runtime/kernel.py +548 -0
  154. androidctld/runtime/lifecycle.py +19 -0
  155. androidctld/runtime/models.py +48 -0
  156. androidctld/runtime/screen_state.py +117 -0
  157. androidctld/runtime/state_repo.py +70 -0
  158. androidctld/runtime/store.py +76 -0
  159. androidctld/runtime_policy.py +127 -0
  160. androidctld/schema/__init__.py +5 -0
  161. androidctld/schema/base.py +132 -0
  162. androidctld/schema/core.py +35 -0
  163. androidctld/schema/daemon_api.py +108 -0
  164. androidctld/schema/persistence.py +161 -0
  165. androidctld/schema/persistence_io.py +41 -0
  166. androidctld/schema/validation_errors.py +309 -0
  167. androidctld/semantics/__init__.py +1 -0
  168. androidctld/semantics/compiler.py +610 -0
  169. androidctld/semantics/continuity.py +107 -0
  170. androidctld/semantics/labels.py +252 -0
  171. androidctld/semantics/models.py +25 -0
  172. androidctld/semantics/policy.py +23 -0
  173. androidctld/semantics/public_models.py +123 -0
  174. androidctld/semantics/registries.py +13 -0
  175. androidctld/semantics/submit_refs.py +417 -0
  176. androidctld/semantics/surface.py +254 -0
  177. androidctld/semantics/targets.py +167 -0
  178. androidctld/snapshots/__init__.py +1 -0
  179. androidctld/snapshots/models.py +219 -0
  180. androidctld/snapshots/refresh.py +273 -0
  181. androidctld/snapshots/schema.py +74 -0
  182. androidctld/snapshots/service.py +138 -0
  183. androidctld/text_equivalence.py +67 -0
  184. androidctld/waits/__init__.py +1 -0
  185. androidctld/waits/evaluators.py +216 -0
  186. androidctld/waits/loop.py +305 -0
  187. androidctld/waits/matcher.py +41 -0
@@ -0,0 +1,252 @@
1
+ """Semantic compiler label and state-description rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from androidctld.semantics.policy import LABEL_MAX_LENGTH
10
+ from androidctld.snapshots.models import RawNode
11
+ from androidctld.text_equivalence import (
12
+ canonical_text_key,
13
+ normalized_text_surface,
14
+ semantic_state_description_remainder,
15
+ )
16
+
17
+ _WHITESPACE_RE = re.compile(r"\s+")
18
+ _STATE_DESCRIPTION_SPLIT_RE = re.compile(r"[,;/]+")
19
+ GENERIC_SEMANTIC_ROLES = {"container", "text", "image", "list-item"}
20
+ LABEL_QUALITY_RESOURCE_ID = 1
21
+ LABEL_QUALITY_STATE_DESCRIPTION = 2
22
+ LABEL_QUALITY_NEARBY = 3
23
+ LABEL_QUALITY_HINT = 4
24
+ LABEL_QUALITY_CONTENT_DESC = 5
25
+ LABEL_QUALITY_TEXT = 6
26
+ _STATE_DESCRIPTION_STATE_MAP = {
27
+ "on": "checked",
28
+ "off": "unchecked",
29
+ "checked": "checked",
30
+ "unchecked": "unchecked",
31
+ "not checked": "unchecked",
32
+ "selected": "selected",
33
+ "disabled": "disabled",
34
+ "focused": "focused",
35
+ "expanded": "expanded",
36
+ "collapsed": "collapsed",
37
+ "password": "password",
38
+ }
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class LabelInfo:
43
+ label: str
44
+ quality: int
45
+
46
+
47
+ def parent_node_for(
48
+ raw_node: RawNode, raw_nodes_by_rid: dict[str, RawNode]
49
+ ) -> RawNode | None:
50
+ if raw_node.parent_rid is None:
51
+ return None
52
+ return raw_nodes_by_rid.get(raw_node.parent_rid)
53
+
54
+
55
+ def infer_role(raw_node: RawNode | None) -> str:
56
+ if raw_node is None:
57
+ return "container"
58
+ class_name = raw_node.class_name
59
+ if "Dialog" in class_name:
60
+ return "dialog"
61
+ if "Tab" in class_name:
62
+ return "tab"
63
+ if "Keyboard" in class_name or "Key" in class_name:
64
+ return "keyboard-key"
65
+ if raw_node.editable or "EditText" in class_name:
66
+ return "input"
67
+ if "Switch" in class_name:
68
+ return "switch"
69
+ if "CheckBox" in class_name:
70
+ return "checkbox"
71
+ if "RadioButton" in class_name:
72
+ return "radio"
73
+ if "Button" in class_name:
74
+ return "button"
75
+ if "Image" in class_name:
76
+ return "image"
77
+ if raw_node.clickable:
78
+ return "list-item"
79
+ if raw_node.text or raw_node.content_desc:
80
+ return "text"
81
+ return "container"
82
+
83
+
84
+ def synthesize_label_info(
85
+ raw_node: RawNode | None, raw_nodes_by_rid: dict[str, RawNode]
86
+ ) -> LabelInfo:
87
+ if raw_node is None:
88
+ return LabelInfo(label="", quality=0)
89
+ for quality, value in (
90
+ (LABEL_QUALITY_TEXT, raw_node.text),
91
+ (LABEL_QUALITY_CONTENT_DESC, raw_node.content_desc),
92
+ (LABEL_QUALITY_HINT, raw_node.hint_text),
93
+ ):
94
+ label = normalize_text(value)
95
+ if label:
96
+ return LabelInfo(label=label[:LABEL_MAX_LENGTH], quality=quality)
97
+ near_label = near_label_for_node(raw_node, raw_nodes_by_rid)
98
+ if near_label:
99
+ return LabelInfo(
100
+ label=near_label[:LABEL_MAX_LENGTH],
101
+ quality=LABEL_QUALITY_NEARBY,
102
+ )
103
+ state_description_label = label_from_state_description(raw_node.state_description)
104
+ if state_description_label:
105
+ return LabelInfo(
106
+ label=state_description_label[:LABEL_MAX_LENGTH],
107
+ quality=LABEL_QUALITY_STATE_DESCRIPTION,
108
+ )
109
+ resource_id = normalize_text(raw_node.resource_id)
110
+ if resource_id:
111
+ return LabelInfo(
112
+ label=resource_id.split("/")[-1][:LABEL_MAX_LENGTH],
113
+ quality=LABEL_QUALITY_RESOURCE_ID,
114
+ )
115
+ return LabelInfo(label="", quality=0)
116
+
117
+
118
+ def fallback_label(raw_node: RawNode | None) -> str:
119
+ if raw_node is None:
120
+ return "item"
121
+ return raw_node.class_name.split(".")[-1] or "item"
122
+
123
+
124
+ def extract_state(raw_node: RawNode) -> list[str]:
125
+ state = []
126
+ if raw_node.checked:
127
+ state.append("checked")
128
+ elif raw_node.checkable:
129
+ state.append("unchecked")
130
+ if raw_node.selected:
131
+ state.append("selected")
132
+ if not raw_node.enabled:
133
+ state.append("disabled")
134
+ if raw_node.focused:
135
+ state.append("focused")
136
+ if raw_node.password:
137
+ state.append("password")
138
+ for token in states_from_state_description(raw_node.state_description):
139
+ if token not in state:
140
+ state.append(token)
141
+ return state
142
+
143
+
144
+ def semantic_role(
145
+ raw_node: RawNode,
146
+ anchor_node: RawNode,
147
+ anchor_actions: list[str],
148
+ ) -> str:
149
+ source_role = infer_role(raw_node)
150
+ anchor_role = infer_role(anchor_node)
151
+ if not anchor_actions or source_role not in GENERIC_SEMANTIC_ROLES:
152
+ role = source_role
153
+ elif anchor_role not in GENERIC_SEMANTIC_ROLES:
154
+ role = anchor_role
155
+ elif "type" in anchor_actions:
156
+ role = "input"
157
+ elif "scroll" in anchor_actions:
158
+ role = "container"
159
+ else:
160
+ role = anchor_role
161
+ if role == "input" and not anchor_node.editable:
162
+ if anchor_role != "input":
163
+ return anchor_role
164
+ if "scroll" in anchor_actions:
165
+ return "container"
166
+ if "tap" in anchor_actions or "longTap" in anchor_actions:
167
+ return "list-item"
168
+ return "text"
169
+ return role
170
+
171
+
172
+ def near_label_for_node(raw_node: RawNode, raw_nodes_by_rid: dict[str, RawNode]) -> str:
173
+ parent_node = parent_node_for(raw_node, raw_nodes_by_rid)
174
+ if parent_node is None:
175
+ return ""
176
+ for child_rid in parent_node.child_rids:
177
+ if child_rid == raw_node.rid:
178
+ continue
179
+ sibling = raw_nodes_by_rid.get(child_rid)
180
+ if sibling is None:
181
+ continue
182
+ label = normalize_text(sibling.text or sibling.content_desc)
183
+ if label:
184
+ return label
185
+ return normalize_text(parent_node.text or parent_node.content_desc)
186
+
187
+
188
+ def sibling_labels_for_node(
189
+ raw_node: RawNode,
190
+ raw_nodes_by_rid: dict[str, RawNode],
191
+ label_infos: dict[str, LabelInfo],
192
+ ) -> list[str]:
193
+ parent_node = parent_node_for(raw_node, raw_nodes_by_rid)
194
+ if parent_node is None:
195
+ return []
196
+ labels = []
197
+ for child_rid in parent_node.child_rids:
198
+ if child_rid == raw_node.rid:
199
+ continue
200
+ sibling = raw_nodes_by_rid.get(child_rid)
201
+ if sibling is None:
202
+ continue
203
+ label = label_infos[sibling.rid].label or fallback_label(sibling)
204
+ if label:
205
+ labels.append(label)
206
+ return labels
207
+
208
+
209
+ def relative_bounds_for_node(
210
+ raw_node: RawNode, parent_node: RawNode | None
211
+ ) -> tuple[int, int, int, int]:
212
+ if parent_node is None:
213
+ return raw_node.bounds
214
+ return (
215
+ raw_node.bounds[0] - parent_node.bounds[0],
216
+ raw_node.bounds[1] - parent_node.bounds[1],
217
+ raw_node.bounds[2] - parent_node.bounds[0],
218
+ raw_node.bounds[3] - parent_node.bounds[1],
219
+ )
220
+
221
+
222
+ def normalize_text(value: Any) -> str:
223
+ if value is None:
224
+ return ""
225
+ return _WHITESPACE_RE.sub(" ", str(value)).strip()
226
+
227
+
228
+ def states_from_state_description(value: Any) -> list[str]:
229
+ states: list[str] = []
230
+ for part in state_description_parts(value):
231
+ token = _STATE_DESCRIPTION_STATE_MAP.get(canonical_text_key(part))
232
+ if token and token not in states:
233
+ states.append(token)
234
+ return states
235
+
236
+
237
+ def label_from_state_description(value: Any) -> str:
238
+ return semantic_state_description_remainder(value)
239
+
240
+
241
+ def state_description_parts(value: Any) -> list[str]:
242
+ normalized = normalized_text_surface(value)
243
+ if not normalized:
244
+ return []
245
+ parts = [
246
+ part.strip()
247
+ for part in _STATE_DESCRIPTION_SPLIT_RE.split(normalized)
248
+ if part.strip()
249
+ ]
250
+ if parts:
251
+ return parts
252
+ return [normalized]
@@ -0,0 +1,25 @@
1
+ """Typed semantic compiler support models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class SemanticMeta:
10
+ resource_id: str | None
11
+ class_name: str
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class RelationScopeNode:
16
+ rid: str
17
+ window_id: str
18
+ parent_rid: str | None
19
+ bounds: tuple[int, int, int, int]
20
+ resource_id: str
21
+ class_name: str
22
+ text: str
23
+ content_desc: str
24
+ pane_title: str
25
+ is_window_root: bool
@@ -0,0 +1,23 @@
1
+ """Semantic screen compilation policy constants."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ LABEL_MAX_LENGTH: Final[int] = 48
8
+
9
+ ROLE_BASE_SCORES: Final[dict[str, int]] = {
10
+ "button": 50,
11
+ "input": 45,
12
+ "switch": 40,
13
+ "checkbox": 35,
14
+ "radio": 35,
15
+ "tab": 30,
16
+ "list-item": 25,
17
+ "keyboard-key": 20,
18
+ "text": 5,
19
+ }
20
+
21
+ TARGETABLE_SCORE_BONUS: Final[int] = 100
22
+ FOCUSED_SCORE_BONUS: Final[int] = 10
23
+ ENABLED_SCORE_BONUS: Final[int] = 5
@@ -0,0 +1,123 @@
1
+ """Daemon helpers for shared public screen contract models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Iterator
6
+
7
+ from androidctl_contracts.public_screen import (
8
+ BLOCKING_GROUP_NAMES,
9
+ PUBLIC_GROUP_NAMES,
10
+ PUBLIC_NODE_ACTION_VALUES,
11
+ PUBLIC_NODE_AMBIGUITY_VALUES,
12
+ PUBLIC_NODE_ORIGIN_VALUES,
13
+ PUBLIC_NODE_ROLE_VALUES,
14
+ PUBLIC_NODE_STATE_VALUES,
15
+ SCROLL_DIRECTION_VALUES,
16
+ AppMatchType,
17
+ BlockingGroupName,
18
+ OmittedEntry,
19
+ OmittedReason,
20
+ PublicApp,
21
+ PublicFocus,
22
+ PublicGroup,
23
+ PublicGroupName,
24
+ PublicItemKind,
25
+ PublicNode,
26
+ PublicNodeAction,
27
+ PublicNodeRole,
28
+ PublicNodeState,
29
+ PublicScreen,
30
+ PublicSemanticMeta,
31
+ PublicSurface,
32
+ ScrollDirection,
33
+ TransientItem,
34
+ TransientKind,
35
+ VisibleWindow,
36
+ )
37
+
38
+ GROUP_NAMES = PUBLIC_GROUP_NAMES
39
+
40
+
41
+ def build_public_groups(
42
+ *,
43
+ order: tuple[PublicGroupName, ...] = PUBLIC_GROUP_NAMES,
44
+ targets: tuple[PublicNode, ...] = (),
45
+ keyboard: tuple[PublicNode, ...] = (),
46
+ system: tuple[PublicNode, ...] = (),
47
+ context: tuple[PublicNode, ...] = (),
48
+ dialog: tuple[PublicNode, ...] = (),
49
+ ) -> tuple[PublicGroup, ...]:
50
+ nodes_by_group: dict[PublicGroupName, tuple[PublicNode, ...]] = {
51
+ "targets": targets,
52
+ "keyboard": keyboard,
53
+ "system": system,
54
+ "context": context,
55
+ "dialog": dialog,
56
+ }
57
+ if set(order) != set(PUBLIC_GROUP_NAMES) or len(order) != len(PUBLIC_GROUP_NAMES):
58
+ raise ValueError("groups order must contain each public group exactly once")
59
+ return tuple(
60
+ PublicGroup(name=group_name, nodes=nodes_by_group[group_name])
61
+ for group_name in order
62
+ )
63
+
64
+
65
+ def public_groups_by_name(
66
+ screen: PublicScreen,
67
+ ) -> dict[PublicGroupName, tuple[PublicNode, ...]]:
68
+ return {group.name: group.nodes for group in screen.groups}
69
+
70
+
71
+ def public_group_nodes(
72
+ screen: PublicScreen,
73
+ group_name: PublicGroupName,
74
+ ) -> tuple[PublicNode, ...]:
75
+ return public_groups_by_name(screen).get(group_name, ())
76
+
77
+
78
+ def iter_public_nodes(nodes: Iterable[PublicNode]) -> Iterator[PublicNode]:
79
+ for node in nodes:
80
+ yield node
81
+ yield from iter_public_nodes(node.children)
82
+
83
+
84
+ def dump_public_screen(screen: PublicScreen) -> dict[str, object]:
85
+ return screen.model_dump(by_alias=True, mode="json")
86
+
87
+
88
+ __all__ = [
89
+ "BLOCKING_GROUP_NAMES",
90
+ "GROUP_NAMES",
91
+ "PUBLIC_GROUP_NAMES",
92
+ "PUBLIC_NODE_ACTION_VALUES",
93
+ "PUBLIC_NODE_AMBIGUITY_VALUES",
94
+ "PUBLIC_NODE_ORIGIN_VALUES",
95
+ "PUBLIC_NODE_ROLE_VALUES",
96
+ "PUBLIC_NODE_STATE_VALUES",
97
+ "SCROLL_DIRECTION_VALUES",
98
+ "AppMatchType",
99
+ "BlockingGroupName",
100
+ "OmittedEntry",
101
+ "OmittedReason",
102
+ "PublicApp",
103
+ "PublicFocus",
104
+ "PublicGroup",
105
+ "PublicGroupName",
106
+ "PublicItemKind",
107
+ "PublicNode",
108
+ "PublicNodeAction",
109
+ "PublicNodeRole",
110
+ "PublicNodeState",
111
+ "PublicScreen",
112
+ "PublicSemanticMeta",
113
+ "PublicSurface",
114
+ "ScrollDirection",
115
+ "TransientItem",
116
+ "TransientKind",
117
+ "VisibleWindow",
118
+ "build_public_groups",
119
+ "dump_public_screen",
120
+ "iter_public_nodes",
121
+ "public_group_nodes",
122
+ "public_groups_by_name",
123
+ ]
@@ -0,0 +1,13 @@
1
+ """Frozen semantic screen registries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ GROUP_ORDER: Final[tuple[str, ...]] = (
8
+ "targets",
9
+ "keyboard",
10
+ "system",
11
+ "context",
12
+ "dialog",
13
+ )