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.
- androidctl/__init__.py +5 -0
- androidctl/__main__.py +4 -0
- androidctl/_version.py +1 -0
- androidctl/app.py +73 -0
- androidctl/cli_options.py +27 -0
- androidctl/command_payloads.py +264 -0
- androidctl/command_views.py +157 -0
- androidctl/commands/__init__.py +1 -0
- androidctl/commands/actions.py +236 -0
- androidctl/commands/adb_wireless.py +157 -0
- androidctl/commands/close.py +30 -0
- androidctl/commands/connect.py +69 -0
- androidctl/commands/execute.py +179 -0
- androidctl/commands/list_apps.py +26 -0
- androidctl/commands/observe.py +26 -0
- androidctl/commands/open.py +41 -0
- androidctl/commands/plumbing.py +58 -0
- androidctl/commands/run_pipeline.py +307 -0
- androidctl/commands/screenshot.py +29 -0
- androidctl/commands/setup.py +301 -0
- androidctl/commands/wait.py +60 -0
- androidctl/daemon/__init__.py +1 -0
- androidctl/daemon/client.py +348 -0
- androidctl/daemon/discovery.py +190 -0
- androidctl/daemon/launcher.py +26 -0
- androidctl/daemon/owner.py +349 -0
- androidctl/errors/__init__.py +1 -0
- androidctl/errors/mapping.py +149 -0
- androidctl/errors/models.py +16 -0
- androidctl/exit_codes.py +8 -0
- androidctl/output.py +147 -0
- androidctl/parsing/__init__.py +1 -0
- androidctl/parsing/duration.py +17 -0
- androidctl/parsing/open_target.py +51 -0
- androidctl/parsing/refs.py +12 -0
- androidctl/parsing/screen_id.py +10 -0
- androidctl/parsing/wait.py +70 -0
- androidctl/renderers/__init__.py +110 -0
- androidctl/renderers/_paths.py +109 -0
- androidctl/renderers/xml.py +234 -0
- androidctl/renderers/xml_projection.py +732 -0
- androidctl/resources/__init__.py +1 -0
- androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
- androidctl/setup/__init__.py +1 -0
- androidctl/setup/accessibility.py +159 -0
- androidctl/setup/adb.py +586 -0
- androidctl/setup/apk_resource.py +29 -0
- androidctl/setup/pairing.py +70 -0
- androidctl/setup/verify.py +175 -0
- androidctl/workspace/__init__.py +3 -0
- androidctl/workspace/resolve.py +27 -0
- androidctl-0.1.0.dist-info/METADATA +217 -0
- androidctl-0.1.0.dist-info/RECORD +187 -0
- androidctl-0.1.0.dist-info/WHEEL +5 -0
- androidctl-0.1.0.dist-info/entry_points.txt +3 -0
- androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
- androidctl-0.1.0.dist-info/top_level.txt +3 -0
- androidctl_contracts/__init__.py +55 -0
- androidctl_contracts/_version.py +1 -0
- androidctl_contracts/_wire_helpers.py +31 -0
- androidctl_contracts/base.py +142 -0
- androidctl_contracts/command_catalog.py +414 -0
- androidctl_contracts/command_results.py +630 -0
- androidctl_contracts/daemon_api.py +335 -0
- androidctl_contracts/errors.py +44 -0
- androidctl_contracts/paths.py +5 -0
- androidctl_contracts/public_screen.py +579 -0
- androidctl_contracts/user_state.py +23 -0
- androidctl_contracts/vocabulary.py +82 -0
- androidctld/__init__.py +5 -0
- androidctld/__main__.py +63 -0
- androidctld/_version.py +1 -0
- androidctld/actions/__init__.py +1 -0
- androidctld/actions/action_target.py +142 -0
- androidctld/actions/capabilities.py +539 -0
- androidctld/actions/executor.py +894 -0
- androidctld/actions/focus_confirmation.py +177 -0
- androidctld/actions/focused_input_admissibility.py +120 -0
- androidctld/actions/fresh_current.py +176 -0
- androidctld/actions/postconditions.py +473 -0
- androidctld/actions/repair.py +101 -0
- androidctld/actions/request_builder.py +204 -0
- androidctld/actions/settle.py +146 -0
- androidctld/actions/submit_confirmation.py +211 -0
- androidctld/actions/submit_routing.py +311 -0
- androidctld/actions/type_confirmation.py +257 -0
- androidctld/app_targets.py +71 -0
- androidctld/artifacts/__init__.py +1 -0
- androidctld/artifacts/models.py +26 -0
- androidctld/artifacts/screen_lookup.py +241 -0
- androidctld/artifacts/screen_payloads.py +109 -0
- androidctld/artifacts/writer.py +286 -0
- androidctld/auth/__init__.py +1 -0
- androidctld/auth/active_registry.py +266 -0
- androidctld/auth/secret_files.py +52 -0
- androidctld/auth/token_store.py +59 -0
- androidctld/commands/__init__.py +1 -0
- androidctld/commands/assembly.py +231 -0
- androidctld/commands/command_models.py +254 -0
- androidctld/commands/dispatch.py +99 -0
- androidctld/commands/executor.py +31 -0
- androidctld/commands/from_boundary.py +175 -0
- androidctld/commands/handlers/__init__.py +15 -0
- androidctld/commands/handlers/action.py +439 -0
- androidctld/commands/handlers/connect.py +94 -0
- androidctld/commands/handlers/list_apps.py +215 -0
- androidctld/commands/handlers/observe.py +121 -0
- androidctld/commands/handlers/screenshot.py +105 -0
- androidctld/commands/handlers/wait.py +286 -0
- androidctld/commands/models.py +65 -0
- androidctld/commands/open_targets.py +56 -0
- androidctld/commands/orchestration.py +353 -0
- androidctld/commands/registry.py +116 -0
- androidctld/commands/result_builders.py +40 -0
- androidctld/commands/result_models.py +555 -0
- androidctld/commands/results.py +108 -0
- androidctld/commands/semantic_command_names.py +17 -0
- androidctld/commands/semantic_error_mapping.py +93 -0
- androidctld/commands/semantic_truth.py +135 -0
- androidctld/commands/service.py +67 -0
- androidctld/config.py +75 -0
- androidctld/daemon/__init__.py +1 -0
- androidctld/daemon/active_slot.py +326 -0
- androidctld/daemon/envelope.py +30 -0
- androidctld/daemon/http_host.py +123 -0
- androidctld/daemon/ingress.py +112 -0
- androidctld/daemon/ownership_probe.py +204 -0
- androidctld/daemon/server.py +286 -0
- androidctld/daemon/service.py +99 -0
- androidctld/device/__init__.py +1 -0
- androidctld/device/action_models.py +154 -0
- androidctld/device/action_serialization.py +121 -0
- androidctld/device/adapters.py +220 -0
- androidctld/device/bootstrap.py +153 -0
- androidctld/device/connectors.py +231 -0
- androidctld/device/errors.py +100 -0
- androidctld/device/interfaces.py +58 -0
- androidctld/device/parsing.py +320 -0
- androidctld/device/rpc.py +483 -0
- androidctld/device/schema.py +114 -0
- androidctld/device/types.py +161 -0
- androidctld/errors/__init__.py +94 -0
- androidctld/logging/__init__.py +22 -0
- androidctld/observation.py +98 -0
- androidctld/protocol.py +53 -0
- androidctld/refs/__init__.py +1 -0
- androidctld/refs/models.py +54 -0
- androidctld/refs/repair.py +284 -0
- androidctld/refs/service.py +422 -0
- androidctld/rendering/__init__.py +1 -0
- androidctld/rendering/screen_xml.py +256 -0
- androidctld/runtime/__init__.py +21 -0
- androidctld/runtime/kernel.py +548 -0
- androidctld/runtime/lifecycle.py +19 -0
- androidctld/runtime/models.py +48 -0
- androidctld/runtime/screen_state.py +117 -0
- androidctld/runtime/state_repo.py +70 -0
- androidctld/runtime/store.py +76 -0
- androidctld/runtime_policy.py +127 -0
- androidctld/schema/__init__.py +5 -0
- androidctld/schema/base.py +132 -0
- androidctld/schema/core.py +35 -0
- androidctld/schema/daemon_api.py +108 -0
- androidctld/schema/persistence.py +161 -0
- androidctld/schema/persistence_io.py +41 -0
- androidctld/schema/validation_errors.py +309 -0
- androidctld/semantics/__init__.py +1 -0
- androidctld/semantics/compiler.py +610 -0
- androidctld/semantics/continuity.py +107 -0
- androidctld/semantics/labels.py +252 -0
- androidctld/semantics/models.py +25 -0
- androidctld/semantics/policy.py +23 -0
- androidctld/semantics/public_models.py +123 -0
- androidctld/semantics/registries.py +13 -0
- androidctld/semantics/submit_refs.py +417 -0
- androidctld/semantics/surface.py +254 -0
- androidctld/semantics/targets.py +167 -0
- androidctld/snapshots/__init__.py +1 -0
- androidctld/snapshots/models.py +219 -0
- androidctld/snapshots/refresh.py +273 -0
- androidctld/snapshots/schema.py +74 -0
- androidctld/snapshots/service.py +138 -0
- androidctld/text_equivalence.py +67 -0
- androidctld/waits/__init__.py +1 -0
- androidctld/waits/evaluators.py +216 -0
- androidctld/waits/loop.py +305 -0
- 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
|
+
]
|