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,579 @@
1
+ """Typed shared public screen projection models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Iterator
7
+ from typing import Any, Literal, TypeAlias, cast, get_args
8
+
9
+ from pydantic import (
10
+ ConfigDict,
11
+ Field,
12
+ field_validator,
13
+ model_serializer,
14
+ model_validator,
15
+ )
16
+
17
+ from .base import DaemonWireModel
18
+
19
+ PublicGroupName = Literal["targets", "keyboard", "system", "context", "dialog"]
20
+ BlockingGroupName = Literal["dialog", "keyboard", "system"]
21
+ OmittedReason = Literal["offscreen", "virtualized", "structureCollapsed"]
22
+ TransientKind = Literal["toast", "snackbar", "banner"]
23
+ AppMatchType = Literal["exact", "alias"]
24
+ PublicItemKind = Literal["node", "container", "text"]
25
+ ScrollDirection = Literal["up", "down", "left", "right", "backward"]
26
+ PublicNodeRole: TypeAlias = Literal[
27
+ "button",
28
+ "input",
29
+ "switch",
30
+ "checkbox",
31
+ "radio",
32
+ "tab",
33
+ "keyboard-key",
34
+ "image",
35
+ "list-item",
36
+ "text",
37
+ "container",
38
+ "dialog",
39
+ "scroll-container",
40
+ ]
41
+ PublicNodeAction: TypeAlias = Literal[
42
+ "tap",
43
+ "longTap",
44
+ "type",
45
+ "scroll",
46
+ "focus",
47
+ "submit",
48
+ ]
49
+ PublicNodeState: TypeAlias = Literal[
50
+ "checked",
51
+ "unchecked",
52
+ "selected",
53
+ "disabled",
54
+ "focused",
55
+ "password",
56
+ "expanded",
57
+ "collapsed",
58
+ ]
59
+
60
+ PUBLIC_GROUP_NAMES: tuple[PublicGroupName, ...] = (
61
+ "targets",
62
+ "keyboard",
63
+ "system",
64
+ "context",
65
+ "dialog",
66
+ )
67
+ BLOCKING_GROUP_NAMES: tuple[BlockingGroupName, ...] = (
68
+ "dialog",
69
+ "keyboard",
70
+ "system",
71
+ )
72
+ OMITTED_REASON_VALUES: tuple[OmittedReason, ...] = (
73
+ "offscreen",
74
+ "virtualized",
75
+ "structureCollapsed",
76
+ )
77
+ TRANSIENT_KIND_VALUES: tuple[TransientKind, ...] = (
78
+ "toast",
79
+ "snackbar",
80
+ "banner",
81
+ )
82
+ SCROLL_DIRECTION_VALUES: tuple[ScrollDirection, ...] = cast(
83
+ tuple[ScrollDirection, ...],
84
+ get_args(ScrollDirection),
85
+ )
86
+ PUBLIC_NODE_ROLE_VALUES: tuple[PublicNodeRole, ...] = cast(
87
+ tuple[PublicNodeRole, ...],
88
+ get_args(PublicNodeRole),
89
+ )
90
+ PUBLIC_NODE_ACTION_VALUES: tuple[PublicNodeAction, ...] = cast(
91
+ tuple[PublicNodeAction, ...],
92
+ get_args(PublicNodeAction),
93
+ )
94
+ PUBLIC_NODE_STATE_VALUES: tuple[PublicNodeState, ...] = cast(
95
+ tuple[PublicNodeState, ...],
96
+ get_args(PublicNodeState),
97
+ )
98
+ PUBLIC_NODE_ORIGIN_VALUES: tuple[str, ...] = ()
99
+ PUBLIC_NODE_AMBIGUITY_VALUES: tuple[str, ...] = ()
100
+ REQUIRED_SCREEN_SEQUENCE_FIELDS: tuple[tuple[str, str], ...] = (
101
+ ("groups", "groups"),
102
+ ("omitted", "omitted"),
103
+ ("visible_windows", "visibleWindows"),
104
+ ("transient", "transient"),
105
+ )
106
+ PUBLIC_REF_RE = re.compile(r"^n[1-9][0-9]*$")
107
+
108
+
109
+ def _list_to_tuple(value: object) -> object:
110
+ if isinstance(value, list):
111
+ return tuple(value)
112
+ return value
113
+
114
+
115
+ def _iter_public_nodes(nodes: tuple[PublicNode, ...]) -> Iterator[PublicNode]:
116
+ for node in nodes:
117
+ yield node
118
+ yield from _iter_public_nodes(node.children)
119
+
120
+
121
+ def _serializes_public_ref(node: PublicNode) -> bool:
122
+ return (node.kind or "node") != "text" and bool(node.ref)
123
+
124
+
125
+ def _validate_registry_token(
126
+ *,
127
+ field_name: str,
128
+ value: str | None,
129
+ allowed_values: tuple[str, ...],
130
+ allow_empty: bool = False,
131
+ ) -> str | None:
132
+ if value is None:
133
+ return None
134
+ if allow_empty and value == "":
135
+ return value
136
+ if value not in allowed_values:
137
+ if allowed_values:
138
+ allowed = ", ".join(allowed_values)
139
+ raise ValueError(f"{field_name} must be one of: {allowed}")
140
+ raise ValueError(f"{field_name} does not define any public tokens")
141
+ return value
142
+
143
+
144
+ class PublicScreenWireModel(DaemonWireModel):
145
+ model_config = ConfigDict(strict=True)
146
+
147
+
148
+ class PublicSemanticMeta(PublicScreenWireModel):
149
+ resource_id: str | None = None
150
+ class_name: str = Field(min_length=1)
151
+
152
+
153
+ class PublicNode(PublicScreenWireModel):
154
+ kind: PublicItemKind | None = None
155
+ role: str = ""
156
+ label: str = ""
157
+ text: str | None = None
158
+ ref: str | None = None
159
+ state: tuple[str, ...] = ()
160
+ actions: tuple[str, ...] = ()
161
+ bounds: tuple[int, int, int, int] | None = None
162
+ meta: PublicSemanticMeta | None = None
163
+ children: tuple[PublicNode, ...] = ()
164
+ scroll_directions: tuple[ScrollDirection, ...] = ()
165
+ submit_refs: tuple[str, ...] = ()
166
+ within: str | None = None
167
+ value: str | None = None
168
+ origin: str | None = None
169
+ window_ref: str | None = None
170
+ ambiguity: str | None = None
171
+
172
+ @field_validator(
173
+ "state",
174
+ "actions",
175
+ "bounds",
176
+ "children",
177
+ "scroll_directions",
178
+ "submit_refs",
179
+ mode="before",
180
+ )
181
+ @classmethod
182
+ def coerce_collections(cls, value: object) -> object:
183
+ return _list_to_tuple(value)
184
+
185
+ @field_validator("role")
186
+ @classmethod
187
+ def validate_role(cls, value: str) -> str:
188
+ return cast(
189
+ str,
190
+ _validate_registry_token(
191
+ field_name="role",
192
+ value=value,
193
+ allowed_values=PUBLIC_NODE_ROLE_VALUES,
194
+ allow_empty=True,
195
+ ),
196
+ )
197
+
198
+ @field_validator("actions")
199
+ @classmethod
200
+ def validate_actions(cls, value: tuple[str, ...]) -> tuple[str, ...]:
201
+ for item in value:
202
+ _validate_registry_token(
203
+ field_name="actions",
204
+ value=item,
205
+ allowed_values=PUBLIC_NODE_ACTION_VALUES,
206
+ )
207
+ return value
208
+
209
+ @field_validator("state")
210
+ @classmethod
211
+ def validate_state(cls, value: tuple[str, ...]) -> tuple[str, ...]:
212
+ for item in value:
213
+ _validate_registry_token(
214
+ field_name="state",
215
+ value=item,
216
+ allowed_values=PUBLIC_NODE_STATE_VALUES,
217
+ )
218
+ return value
219
+
220
+ @field_validator("submit_refs")
221
+ @classmethod
222
+ def validate_submit_refs(cls, value: tuple[str, ...]) -> tuple[str, ...]:
223
+ seen: set[str] = set()
224
+ for item in value:
225
+ if not isinstance(item, str) or not item:
226
+ raise ValueError("submitRefs entries must be non-empty strings")
227
+ if not PUBLIC_REF_RE.fullmatch(item):
228
+ raise ValueError("submitRefs entries must be public refs like n1")
229
+ if item in seen:
230
+ raise ValueError("submitRefs entries must be unique")
231
+ seen.add(item)
232
+ return value
233
+
234
+ @field_validator("origin")
235
+ @classmethod
236
+ def validate_origin(cls, value: str | None) -> str | None:
237
+ return _validate_registry_token(
238
+ field_name="origin",
239
+ value=value,
240
+ allowed_values=PUBLIC_NODE_ORIGIN_VALUES,
241
+ )
242
+
243
+ @field_validator("ambiguity")
244
+ @classmethod
245
+ def validate_ambiguity(cls, value: str | None) -> str | None:
246
+ return _validate_registry_token(
247
+ field_name="ambiguity",
248
+ value=value,
249
+ allowed_values=PUBLIC_NODE_AMBIGUITY_VALUES,
250
+ )
251
+
252
+ @model_validator(mode="before")
253
+ @classmethod
254
+ def normalize_shape(cls, value: object) -> object:
255
+ if not isinstance(value, dict):
256
+ return value
257
+ payload = dict(value)
258
+ if payload.get("children") not in (None, (), []):
259
+ payload.setdefault("kind", "container")
260
+ elif "kind" not in payload:
261
+ payload["kind"] = "node"
262
+ if payload.get("kind") == "text" and ("role" in payload or "label" in payload):
263
+ raise ValueError('kind="text" items cannot include role or label')
264
+ if payload.get("ref") is None:
265
+ payload.pop("meta", None)
266
+ elif payload.get("meta") == {}:
267
+ payload["meta"] = None
268
+ return payload
269
+
270
+ @model_validator(mode="after")
271
+ def validate_shape(self) -> PublicNode:
272
+ kind = self.kind or "node"
273
+ if kind == "text":
274
+ if not self.text:
275
+ raise ValueError("text items must include text")
276
+ if self.children:
277
+ raise ValueError("text items cannot include children")
278
+ if self.scroll_directions:
279
+ raise ValueError("text items cannot include scrollDirections")
280
+ if self.submit_refs:
281
+ raise ValueError("text items cannot include submitRefs")
282
+ return self
283
+ if not self.role:
284
+ raise ValueError("node items must include role")
285
+ if not self.label:
286
+ raise ValueError("node items must include label")
287
+ if self.submit_refs and (self.role != "input" or not self.ref):
288
+ raise ValueError("submitRefs requires an input node with a public ref")
289
+ if kind == "node" and self.children:
290
+ raise ValueError("node items cannot include children")
291
+ if kind != "container" and self.scroll_directions:
292
+ raise ValueError("only container items can include scrollDirections")
293
+ return self
294
+
295
+ @model_serializer(mode="plain")
296
+ def serialize_model(self) -> dict[str, Any]:
297
+ kind = self.kind or "node"
298
+ if kind == "text":
299
+ payload: dict[str, Any] = {
300
+ "kind": "text",
301
+ "text": self.text,
302
+ }
303
+ self._append_optional_fields(payload)
304
+ return payload
305
+
306
+ payload = {
307
+ "role": self.role,
308
+ "label": self.label,
309
+ }
310
+ if kind == "container":
311
+ payload["kind"] = "container"
312
+ if self.ref is not None:
313
+ payload["ref"] = self.ref
314
+ payload["state"] = list(self.state)
315
+ payload["actions"] = list(self.actions)
316
+ payload["bounds"] = None if self.bounds is None else list(self.bounds)
317
+ payload["meta"] = {} if self.meta is None else self.meta.model_dump()
318
+ self._append_optional_fields(payload)
319
+ return payload
320
+
321
+ def _append_optional_fields(self, payload: dict[str, Any]) -> None:
322
+ if self.scroll_directions:
323
+ payload["scrollDirections"] = list(self.scroll_directions)
324
+ if self.submit_refs:
325
+ payload["submitRefs"] = list(self.submit_refs)
326
+ if self.within is not None:
327
+ payload["within"] = self.within
328
+ if self.value is not None:
329
+ payload["value"] = self.value
330
+ if self.origin is not None:
331
+ payload["origin"] = self.origin
332
+ if self.window_ref is not None:
333
+ payload["windowRef"] = self.window_ref
334
+ if self.ambiguity is not None:
335
+ payload["ambiguity"] = self.ambiguity
336
+ if self.children:
337
+ payload["children"] = [
338
+ child.model_dump(by_alias=True, mode="json") for child in self.children
339
+ ]
340
+
341
+
342
+ PublicNode.model_rebuild()
343
+
344
+
345
+ class PublicApp(PublicScreenWireModel):
346
+ package_name: str | None = None
347
+ activity_name: str | None = None
348
+ requested_package_name: str | None = None
349
+ resolved_package_name: str | None = None
350
+ match_type: AppMatchType | None = None
351
+
352
+ @model_serializer(mode="wrap")
353
+ def serialize_model(self, handler: Any) -> dict[str, Any]:
354
+ payload = dict(handler(self))
355
+ if self.package_name is None:
356
+ payload.pop("packageName", None)
357
+ if self.activity_name is None:
358
+ payload.pop("activityName", None)
359
+ if self.requested_package_name is None:
360
+ payload.pop("requestedPackageName", None)
361
+ if self.resolved_package_name is None:
362
+ payload.pop("resolvedPackageName", None)
363
+ if self.match_type is None:
364
+ payload.pop("matchType", None)
365
+ return payload
366
+
367
+
368
+ class PublicFocus(PublicScreenWireModel):
369
+ input_ref: str | None = None
370
+
371
+ @model_serializer(mode="wrap")
372
+ def serialize_model(self, handler: Any) -> dict[str, Any]:
373
+ payload = dict(handler(self))
374
+ if self.input_ref is None:
375
+ payload.pop("inputRef", None)
376
+ return payload
377
+
378
+
379
+ class PublicSurface(PublicScreenWireModel):
380
+ keyboard_visible: bool
381
+ blocking_group: BlockingGroupName | None = None
382
+ focus: PublicFocus
383
+
384
+ @model_serializer(mode="wrap")
385
+ def serialize_model(self, handler: Any) -> dict[str, Any]:
386
+ payload = dict(handler(self))
387
+ if self.blocking_group is None:
388
+ payload.pop("blockingGroup", None)
389
+ return payload
390
+
391
+
392
+ class PublicGroup(PublicScreenWireModel):
393
+ name: PublicGroupName
394
+ nodes: tuple[PublicNode, ...] = ()
395
+
396
+ @field_validator("nodes", mode="before")
397
+ @classmethod
398
+ def coerce_nodes(cls, value: object) -> object:
399
+ return _list_to_tuple(value)
400
+
401
+
402
+ class OmittedEntry(PublicScreenWireModel):
403
+ group: PublicGroupName
404
+ reason: OmittedReason
405
+ count: int | None = None
406
+
407
+ @model_serializer(mode="wrap")
408
+ def serialize_model(self, handler: Any) -> dict[str, Any]:
409
+ payload = dict(handler(self))
410
+ if self.count is None:
411
+ payload.pop("count", None)
412
+ return payload
413
+
414
+
415
+ class VisibleWindow(PublicScreenWireModel):
416
+ window_ref: str = Field(min_length=1)
417
+ role: str = Field(min_length=1)
418
+ focused: bool = False
419
+ blocking: bool = False
420
+
421
+
422
+ class TransientItem(PublicScreenWireModel):
423
+ text: str = Field(min_length=1)
424
+ kind: TransientKind | None = None
425
+
426
+ @model_serializer(mode="wrap")
427
+ def serialize_model(self, handler: Any) -> dict[str, Any]:
428
+ payload = dict(handler(self))
429
+ if self.kind is None:
430
+ payload.pop("kind", None)
431
+ return payload
432
+
433
+
434
+ class PublicScreen(PublicScreenWireModel):
435
+ screen_id: str = Field(min_length=1)
436
+ app: PublicApp
437
+ surface: PublicSurface
438
+ groups: tuple[PublicGroup, ...]
439
+ omitted: tuple[OmittedEntry, ...] = ()
440
+ visible_windows: tuple[VisibleWindow, ...] = ()
441
+ transient: tuple[TransientItem, ...] = ()
442
+
443
+ @field_validator("groups", "omitted", "visible_windows", "transient", mode="before")
444
+ @classmethod
445
+ def coerce_collections(cls, value: object) -> object:
446
+ return _list_to_tuple(value)
447
+
448
+ @model_validator(mode="before")
449
+ @classmethod
450
+ def validate_wire_presence(cls, value: object) -> object:
451
+ if not isinstance(value, dict):
452
+ return value
453
+ missing = [
454
+ alias
455
+ for field_name, alias in REQUIRED_SCREEN_SEQUENCE_FIELDS
456
+ if field_name not in value and alias not in value
457
+ ]
458
+ if missing:
459
+ missing_fields = ", ".join(missing)
460
+ raise ValueError(
461
+ f"screen payload must include {missing_fields} on the wire"
462
+ )
463
+ return value
464
+
465
+ @model_validator(mode="after")
466
+ def validate_groups(self) -> PublicScreen:
467
+ group_names = tuple(group.name for group in self.groups)
468
+ if set(group_names) != set(PUBLIC_GROUP_NAMES) or len(group_names) != len(
469
+ PUBLIC_GROUP_NAMES
470
+ ):
471
+ raise ValueError("groups must contain each public group exactly once")
472
+ blocking_group = self.surface.blocking_group
473
+ if blocking_group is None:
474
+ expected_order = PUBLIC_GROUP_NAMES
475
+ else:
476
+ expected_order = (
477
+ blocking_group,
478
+ *(
479
+ group_name
480
+ for group_name in PUBLIC_GROUP_NAMES
481
+ if group_name != blocking_group
482
+ ),
483
+ )
484
+ if group_names != expected_order:
485
+ raise ValueError("groups order must match canonical public order")
486
+ return self
487
+
488
+ @model_validator(mode="after")
489
+ def validate_window_refs(self) -> PublicScreen:
490
+ declared_window_refs = {window.window_ref for window in self.visible_windows}
491
+ unknown_window_refs = sorted(
492
+ {
493
+ node.window_ref
494
+ for group in self.groups
495
+ for node in _iter_public_nodes(group.nodes)
496
+ if node.window_ref is not None
497
+ and node.window_ref not in declared_window_refs
498
+ }
499
+ )
500
+ if unknown_window_refs:
501
+ raise ValueError(
502
+ "windowRef values must reference declared visibleWindows members: "
503
+ + ", ".join(unknown_window_refs)
504
+ )
505
+ return self
506
+
507
+ @model_validator(mode="after")
508
+ def validate_submit_refs(self) -> PublicScreen:
509
+ nodes = [
510
+ node for group in self.groups for node in _iter_public_nodes(group.nodes)
511
+ ]
512
+ has_submit_refs = any(node.submit_refs for node in nodes)
513
+ declared_refs: dict[str, PublicNode] = {}
514
+ duplicate_refs: set[str] = set()
515
+ for node in nodes:
516
+ if not _serializes_public_ref(node):
517
+ continue
518
+ ref = node.ref
519
+ if ref is None:
520
+ continue
521
+ if ref in declared_refs:
522
+ duplicate_refs.add(ref)
523
+ else:
524
+ declared_refs[ref] = node
525
+ if has_submit_refs and duplicate_refs:
526
+ raise ValueError(
527
+ "public refs must be unique within a screen: "
528
+ + ", ".join(sorted(duplicate_refs))
529
+ )
530
+
531
+ for node in nodes:
532
+ if not node.submit_refs:
533
+ continue
534
+ if not node.ref or not PUBLIC_REF_RE.fullmatch(node.ref):
535
+ raise ValueError(
536
+ "nodes with submitRefs must have a valid public ref like n1"
537
+ )
538
+ for target_ref in node.submit_refs:
539
+ if target_ref not in declared_refs:
540
+ raise ValueError(
541
+ "submitRefs values must reference same-screen public refs: "
542
+ + target_ref
543
+ )
544
+ if target_ref == node.ref:
545
+ raise ValueError("submitRefs cannot reference the source node")
546
+ return self
547
+
548
+
549
+ __all__ = [
550
+ "BLOCKING_GROUP_NAMES",
551
+ "OMITTED_REASON_VALUES",
552
+ "PUBLIC_GROUP_NAMES",
553
+ "PUBLIC_NODE_ACTION_VALUES",
554
+ "PUBLIC_NODE_AMBIGUITY_VALUES",
555
+ "PUBLIC_NODE_ORIGIN_VALUES",
556
+ "PUBLIC_NODE_ROLE_VALUES",
557
+ "PUBLIC_NODE_STATE_VALUES",
558
+ "PUBLIC_REF_RE",
559
+ "SCROLL_DIRECTION_VALUES",
560
+ "TRANSIENT_KIND_VALUES",
561
+ "BlockingGroupName",
562
+ "OmittedEntry",
563
+ "OmittedReason",
564
+ "PublicApp",
565
+ "PublicFocus",
566
+ "PublicGroup",
567
+ "PublicGroupName",
568
+ "PublicNode",
569
+ "PublicNodeAction",
570
+ "PublicNodeRole",
571
+ "PublicNodeState",
572
+ "PublicScreen",
573
+ "PublicSemanticMeta",
574
+ "PublicSurface",
575
+ "ScrollDirection",
576
+ "TransientItem",
577
+ "TransientKind",
578
+ "VisibleWindow",
579
+ ]
@@ -0,0 +1,23 @@
1
+ from .base import DaemonWireModel
2
+
3
+
4
+ class DaemonInstanceIdentity(DaemonWireModel):
5
+ pid: int
6
+ started_at: str
7
+
8
+
9
+ class ActiveDaemonRecord(DaemonWireModel):
10
+ pid: int
11
+ host: str
12
+ port: int
13
+ token: str
14
+ started_at: str
15
+ workspace_root: str
16
+ owner_id: str
17
+
18
+ @property
19
+ def identity(self) -> DaemonInstanceIdentity:
20
+ return DaemonInstanceIdentity(
21
+ pid=self.pid,
22
+ started_at=self.started_at,
23
+ )
@@ -0,0 +1,82 @@
1
+ """Centralized frozen vocabularies for shared contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class RuntimeStatus(str, Enum):
9
+ NEW = "new"
10
+ BOOTSTRAPPING = "bootstrapping"
11
+ CONNECTED = "connected"
12
+ READY = "ready"
13
+ BROKEN = "broken"
14
+ CLOSED = "closed"
15
+
16
+
17
+ class PublicResultCategory(str, Enum):
18
+ OBSERVE = "observe"
19
+ OPEN = "open"
20
+ TRANSITION = "transition"
21
+ WAIT = "wait"
22
+
23
+
24
+ class PublicResultFamily(str, Enum):
25
+ SEMANTIC = "semantic"
26
+ RETAINED = "retained"
27
+ LIST_APPS = "listApps"
28
+
29
+
30
+ class RetainedEnvelopeKind(str, Enum):
31
+ BOOTSTRAP = "bootstrap"
32
+ ARTIFACT = "artifact"
33
+ LIFECYCLE = "lifecycle"
34
+
35
+
36
+ class PayloadMode(str, Enum):
37
+ NONE = "none"
38
+ FULL = "full"
39
+
40
+
41
+ class ExecutionOutcome(str, Enum):
42
+ NOT_APPLICABLE = "notApplicable"
43
+ NOT_ATTEMPTED = "notAttempted"
44
+ DISPATCHED = "dispatched"
45
+ UNKNOWN = "unknown"
46
+
47
+
48
+ class ContinuityStatus(str, Enum):
49
+ NONE = "none"
50
+ STABLE = "stable"
51
+ STALE = "stale"
52
+
53
+
54
+ class ObservationQuality(str, Enum):
55
+ NONE = "none"
56
+ AUTHORITATIVE = "authoritative"
57
+
58
+
59
+ class SemanticResultCode(str, Enum):
60
+ REF_STALE = "REF_STALE"
61
+ WAIT_TIMEOUT = "WAIT_TIMEOUT"
62
+ TARGET_BLOCKED = "TARGET_BLOCKED"
63
+ TARGET_NOT_ACTIONABLE = "TARGET_NOT_ACTIONABLE"
64
+ OPEN_FAILED = "OPEN_FAILED"
65
+ ACTION_NOT_CONFIRMED = "ACTION_NOT_CONFIRMED"
66
+ TYPE_NOT_CONFIRMED = "TYPE_NOT_CONFIRMED"
67
+ SUBMIT_NOT_CONFIRMED = "SUBMIT_NOT_CONFIRMED"
68
+ DEVICE_UNAVAILABLE = "DEVICE_UNAVAILABLE"
69
+ POST_ACTION_OBSERVATION_LOST = "POST_ACTION_OBSERVATION_LOST"
70
+
71
+
72
+ __all__ = [
73
+ "ContinuityStatus",
74
+ "ExecutionOutcome",
75
+ "ObservationQuality",
76
+ "PayloadMode",
77
+ "PublicResultCategory",
78
+ "PublicResultFamily",
79
+ "RetainedEnvelopeKind",
80
+ "RuntimeStatus",
81
+ "SemanticResultCode",
82
+ ]
@@ -0,0 +1,5 @@
1
+ """androidctld host daemon package."""
2
+
3
+ from ._version import __version__ as __version__
4
+
5
+ SERVICE_NAME = "androidctld"