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,353 @@
|
|
|
1
|
+
"""Command run orchestration helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from contextlib import AbstractContextManager, nullcontext
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any, TypeAlias
|
|
12
|
+
|
|
13
|
+
from androidctl_contracts.command_catalog import (
|
|
14
|
+
CommandCatalogEntry,
|
|
15
|
+
entry_for_daemon_kind,
|
|
16
|
+
entry_for_result_command,
|
|
17
|
+
runtime_close_entry,
|
|
18
|
+
)
|
|
19
|
+
from androidctl_contracts.command_results import (
|
|
20
|
+
CommandResultCore,
|
|
21
|
+
ListAppsResult,
|
|
22
|
+
RetainedResultEnvelope,
|
|
23
|
+
dump_canonical_command_result,
|
|
24
|
+
)
|
|
25
|
+
from androidctl_contracts.vocabulary import PublicResultFamily
|
|
26
|
+
from androidctld.commands.command_models import InternalCommand
|
|
27
|
+
from androidctld.commands.models import CommandRecord, CommandStatus
|
|
28
|
+
from androidctld.commands.registry import CommandSpec, resolve_command_spec
|
|
29
|
+
from androidctld.commands.result_models import (
|
|
30
|
+
build_projected_retained_failure_result,
|
|
31
|
+
build_retained_success_result,
|
|
32
|
+
dump_retained_result_envelope,
|
|
33
|
+
)
|
|
34
|
+
from androidctld.commands.results import (
|
|
35
|
+
complete_record_with_error,
|
|
36
|
+
complete_record_with_result,
|
|
37
|
+
)
|
|
38
|
+
from androidctld.errors import DaemonError, DaemonErrorCode
|
|
39
|
+
from androidctld.logging import configure_logging
|
|
40
|
+
from androidctld.protocol import CommandKind
|
|
41
|
+
from androidctld.runtime.models import WorkspaceRuntime
|
|
42
|
+
from androidctld.runtime.store import RuntimeSerialCommandBusyError
|
|
43
|
+
|
|
44
|
+
SerialAdmission: TypeAlias = Callable[[str], AbstractContextManager[None]]
|
|
45
|
+
TimeFn: TypeAlias = Callable[[], float]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class CommandRunContext:
|
|
50
|
+
runtime: WorkspaceRuntime
|
|
51
|
+
command: InternalCommand
|
|
52
|
+
spec: CommandSpec
|
|
53
|
+
catalog_entry: CommandCatalogEntry
|
|
54
|
+
expected_result_command: str
|
|
55
|
+
record: CommandRecord
|
|
56
|
+
started_at: str
|
|
57
|
+
started_monotonic: float
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_CURRENT_CONTEXT: ContextVar[CommandRunContext | None] = ContextVar(
|
|
61
|
+
"androidctld_command_run_context",
|
|
62
|
+
default=None,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CommandRunOrchestrator:
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
*,
|
|
70
|
+
serial_admission: SerialAdmission | None = None,
|
|
71
|
+
time_fn: TimeFn | None = None,
|
|
72
|
+
logger: logging.Logger | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
self._serial_admission = serial_admission
|
|
75
|
+
self._time_fn = time_fn or (lambda: 0.0)
|
|
76
|
+
self._logger = logger or configure_logging()
|
|
77
|
+
|
|
78
|
+
def run(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
runtime: WorkspaceRuntime,
|
|
82
|
+
command: InternalCommand,
|
|
83
|
+
execute: Callable[[], dict[str, Any]],
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
context = self._build_context(runtime=runtime, command=command)
|
|
86
|
+
self._log_command_started(context)
|
|
87
|
+
try:
|
|
88
|
+
with self._admit_serial_command(context):
|
|
89
|
+
token = _CURRENT_CONTEXT.set(context)
|
|
90
|
+
try:
|
|
91
|
+
result = execute()
|
|
92
|
+
finally:
|
|
93
|
+
_CURRENT_CONTEXT.reset(token)
|
|
94
|
+
finalized = self._finalize_result(
|
|
95
|
+
result,
|
|
96
|
+
expected_result_command=context.expected_result_command,
|
|
97
|
+
)
|
|
98
|
+
complete_record_with_result(context.record, finalized)
|
|
99
|
+
except RuntimeSerialCommandBusyError as error:
|
|
100
|
+
if context.catalog_entry.result_family is PublicResultFamily.RETAINED:
|
|
101
|
+
finalized = _retained_busy_result(
|
|
102
|
+
command=context.expected_result_command,
|
|
103
|
+
message=str(error),
|
|
104
|
+
)
|
|
105
|
+
complete_record_with_result(context.record, finalized)
|
|
106
|
+
self._log_command_finished(context)
|
|
107
|
+
return finalized
|
|
108
|
+
daemon_error = _serial_busy_daemon_error(error)
|
|
109
|
+
complete_record_with_error(context.record, daemon_error)
|
|
110
|
+
self._log_command_finished(context)
|
|
111
|
+
raise daemon_error from error
|
|
112
|
+
except DaemonError as error:
|
|
113
|
+
complete_record_with_error(context.record, error)
|
|
114
|
+
self._log_command_finished(context)
|
|
115
|
+
raise
|
|
116
|
+
except Exception as error:
|
|
117
|
+
complete_record_with_error(
|
|
118
|
+
context.record,
|
|
119
|
+
_internal_record_error(error),
|
|
120
|
+
)
|
|
121
|
+
self._log_command_finished(context)
|
|
122
|
+
raise
|
|
123
|
+
self._log_command_finished(context)
|
|
124
|
+
return finalized
|
|
125
|
+
|
|
126
|
+
def close_runtime(
|
|
127
|
+
self,
|
|
128
|
+
*,
|
|
129
|
+
runtime: WorkspaceRuntime,
|
|
130
|
+
close: Callable[[], None],
|
|
131
|
+
) -> dict[str, Any]:
|
|
132
|
+
del runtime
|
|
133
|
+
entry = runtime_close_entry()
|
|
134
|
+
started_at = _now_isoformat()
|
|
135
|
+
record = CommandRecord(
|
|
136
|
+
command_id="semantic-boundary",
|
|
137
|
+
kind=CommandKind.CLOSE,
|
|
138
|
+
status=CommandStatus.RUNNING,
|
|
139
|
+
started_at=started_at,
|
|
140
|
+
result_command=entry.result_command,
|
|
141
|
+
)
|
|
142
|
+
try:
|
|
143
|
+
with self._admit_runtime_close(entry):
|
|
144
|
+
close()
|
|
145
|
+
finalized = self._finalize_result(
|
|
146
|
+
build_retained_success_result(command=entry.result_command),
|
|
147
|
+
expected_result_command=entry.result_command,
|
|
148
|
+
)
|
|
149
|
+
complete_record_with_result(record, finalized)
|
|
150
|
+
return finalized
|
|
151
|
+
except RuntimeSerialCommandBusyError as error:
|
|
152
|
+
finalized = _retained_busy_result(
|
|
153
|
+
command=entry.result_command,
|
|
154
|
+
message=str(error),
|
|
155
|
+
)
|
|
156
|
+
complete_record_with_result(record, finalized)
|
|
157
|
+
return finalized
|
|
158
|
+
except DaemonError as error:
|
|
159
|
+
complete_record_with_error(record, error)
|
|
160
|
+
raise
|
|
161
|
+
except Exception as error:
|
|
162
|
+
complete_record_with_error(record, _internal_record_error(error))
|
|
163
|
+
raise
|
|
164
|
+
|
|
165
|
+
def _build_context(
|
|
166
|
+
self,
|
|
167
|
+
*,
|
|
168
|
+
runtime: WorkspaceRuntime,
|
|
169
|
+
command: InternalCommand,
|
|
170
|
+
) -> CommandRunContext:
|
|
171
|
+
spec = resolve_command_spec(command)
|
|
172
|
+
catalog_entry = entry_for_daemon_kind(spec.daemon_kind)
|
|
173
|
+
if catalog_entry is None:
|
|
174
|
+
raise ValueError(f"unknown daemon command kind: {spec.daemon_kind!r}")
|
|
175
|
+
started_at = _now_isoformat()
|
|
176
|
+
return CommandRunContext(
|
|
177
|
+
runtime=runtime,
|
|
178
|
+
command=command,
|
|
179
|
+
spec=spec,
|
|
180
|
+
catalog_entry=catalog_entry,
|
|
181
|
+
expected_result_command=catalog_entry.result_command,
|
|
182
|
+
record=CommandRecord(
|
|
183
|
+
command_id="semantic-boundary",
|
|
184
|
+
kind=_record_kind_for_context(command=command, spec=spec),
|
|
185
|
+
status=CommandStatus.RUNNING,
|
|
186
|
+
started_at=started_at,
|
|
187
|
+
result_command=catalog_entry.result_command,
|
|
188
|
+
),
|
|
189
|
+
started_at=started_at,
|
|
190
|
+
started_monotonic=self._time_fn(),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _admit_serial_command(
|
|
194
|
+
self,
|
|
195
|
+
context: CommandRunContext,
|
|
196
|
+
) -> AbstractContextManager[None]:
|
|
197
|
+
if self._serial_admission is None:
|
|
198
|
+
return nullcontext()
|
|
199
|
+
return self._serial_admission(context.spec.daemon_kind)
|
|
200
|
+
|
|
201
|
+
def _admit_runtime_close(
|
|
202
|
+
self,
|
|
203
|
+
entry: CommandCatalogEntry,
|
|
204
|
+
) -> AbstractContextManager[None]:
|
|
205
|
+
if self._serial_admission is None:
|
|
206
|
+
return nullcontext()
|
|
207
|
+
return self._serial_admission(entry.result_command)
|
|
208
|
+
|
|
209
|
+
def _finalize_result(
|
|
210
|
+
self,
|
|
211
|
+
payload: (
|
|
212
|
+
CommandResultCore | RetainedResultEnvelope | ListAppsResult | dict[str, Any]
|
|
213
|
+
),
|
|
214
|
+
*,
|
|
215
|
+
expected_result_command: str,
|
|
216
|
+
) -> dict[str, Any]:
|
|
217
|
+
catalog_entry = entry_for_result_command(expected_result_command)
|
|
218
|
+
if catalog_entry is None:
|
|
219
|
+
raise ValueError(f"unknown result command: {expected_result_command!r}")
|
|
220
|
+
result: CommandResultCore | RetainedResultEnvelope | ListAppsResult
|
|
221
|
+
if catalog_entry.result_family is PublicResultFamily.SEMANTIC:
|
|
222
|
+
result = (
|
|
223
|
+
payload
|
|
224
|
+
if isinstance(payload, CommandResultCore)
|
|
225
|
+
else CommandResultCore.model_validate(payload)
|
|
226
|
+
)
|
|
227
|
+
elif catalog_entry.result_family is PublicResultFamily.RETAINED:
|
|
228
|
+
result = (
|
|
229
|
+
payload
|
|
230
|
+
if isinstance(payload, RetainedResultEnvelope)
|
|
231
|
+
else RetainedResultEnvelope.model_validate(payload)
|
|
232
|
+
)
|
|
233
|
+
elif catalog_entry.result_family is PublicResultFamily.LIST_APPS:
|
|
234
|
+
result = (
|
|
235
|
+
payload
|
|
236
|
+
if isinstance(payload, ListAppsResult)
|
|
237
|
+
else ListAppsResult.model_validate(payload)
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
f"unsupported result family: {catalog_entry.result_family!r}"
|
|
242
|
+
)
|
|
243
|
+
if result.command != expected_result_command:
|
|
244
|
+
raise ValueError(
|
|
245
|
+
"result.command must match command catalog result command: "
|
|
246
|
+
f"expected {expected_result_command!r}, got {result.command!r}"
|
|
247
|
+
)
|
|
248
|
+
if result.command != catalog_entry.result_command:
|
|
249
|
+
raise ValueError(f"unknown result command: {result.command!r}")
|
|
250
|
+
if isinstance(result, RetainedResultEnvelope):
|
|
251
|
+
return dump_retained_result_envelope(result)
|
|
252
|
+
if isinstance(result, ListAppsResult):
|
|
253
|
+
return result.model_dump(by_alias=True, mode="json")
|
|
254
|
+
return dump_canonical_command_result(result)
|
|
255
|
+
|
|
256
|
+
def _log_command_started(self, context: CommandRunContext) -> None:
|
|
257
|
+
self._logger.info(
|
|
258
|
+
"command started kind=%s result_command=%s",
|
|
259
|
+
context.record.kind.value,
|
|
260
|
+
context.expected_result_command,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def _log_command_finished(self, context: CommandRunContext) -> None:
|
|
264
|
+
elapsed_ms = max(
|
|
265
|
+
0.0,
|
|
266
|
+
(self._time_fn() - context.started_monotonic) * 1000.0,
|
|
267
|
+
)
|
|
268
|
+
self._logger.info(
|
|
269
|
+
"command finished kind=%s result_command=%s status=%s elapsed_ms=%.3f",
|
|
270
|
+
context.record.kind.value,
|
|
271
|
+
context.expected_result_command,
|
|
272
|
+
context.record.status.value,
|
|
273
|
+
elapsed_ms,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def current_command_record(
|
|
278
|
+
*,
|
|
279
|
+
kind: CommandKind,
|
|
280
|
+
result_command: str,
|
|
281
|
+
) -> CommandRecord:
|
|
282
|
+
context = _CURRENT_CONTEXT.get()
|
|
283
|
+
if (
|
|
284
|
+
context is not None
|
|
285
|
+
and context.record.kind == kind
|
|
286
|
+
and context.record.result_command == result_command
|
|
287
|
+
):
|
|
288
|
+
return context.record
|
|
289
|
+
return CommandRecord(
|
|
290
|
+
command_id="semantic-boundary",
|
|
291
|
+
kind=kind,
|
|
292
|
+
status=CommandStatus.RUNNING,
|
|
293
|
+
started_at=_now_isoformat(),
|
|
294
|
+
result_command=result_command,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _record_kind_for_context(
|
|
299
|
+
*,
|
|
300
|
+
command: InternalCommand,
|
|
301
|
+
spec: CommandSpec,
|
|
302
|
+
) -> CommandKind:
|
|
303
|
+
if spec.family == "global_action":
|
|
304
|
+
return CommandKind.GLOBAL
|
|
305
|
+
return CommandKind(command.kind.value)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _now_isoformat() -> str:
|
|
309
|
+
return (
|
|
310
|
+
datetime.now(timezone.utc)
|
|
311
|
+
.replace(microsecond=0)
|
|
312
|
+
.isoformat()
|
|
313
|
+
.replace("+00:00", "Z")
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _internal_record_error(error: BaseException) -> DaemonError:
|
|
318
|
+
message = str(error) or error.__class__.__name__
|
|
319
|
+
return DaemonError(
|
|
320
|
+
code=DaemonErrorCode.INTERNAL_COMMAND_FAILURE,
|
|
321
|
+
message=message,
|
|
322
|
+
retryable=False,
|
|
323
|
+
details={"exceptionType": error.__class__.__name__},
|
|
324
|
+
http_status=200,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _serial_busy_daemon_error(error: BaseException) -> DaemonError:
|
|
329
|
+
return DaemonError(
|
|
330
|
+
code=DaemonErrorCode.RUNTIME_BUSY,
|
|
331
|
+
message=str(error),
|
|
332
|
+
retryable=True,
|
|
333
|
+
details={"reason": "overlapping_control_request"},
|
|
334
|
+
http_status=200,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _retained_busy_result(*, command: str, message: str) -> dict[str, Any]:
|
|
339
|
+
return dump_retained_result_envelope(
|
|
340
|
+
build_projected_retained_failure_result(
|
|
341
|
+
command=command,
|
|
342
|
+
code=DaemonErrorCode.RUNTIME_BUSY,
|
|
343
|
+
message=message,
|
|
344
|
+
details={"reason": "overlapping_control_request"},
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
__all__ = [
|
|
350
|
+
"CommandRunContext",
|
|
351
|
+
"CommandRunOrchestrator",
|
|
352
|
+
"current_command_record",
|
|
353
|
+
]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Semantic command registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from androidctl_contracts.command_catalog import (
|
|
9
|
+
daemon_command_kinds_for_route,
|
|
10
|
+
)
|
|
11
|
+
from androidctld.commands.command_models import GlobalCommand
|
|
12
|
+
from androidctld.protocol import CommandKind
|
|
13
|
+
|
|
14
|
+
CommandFamily = Literal[
|
|
15
|
+
"connect",
|
|
16
|
+
"observe",
|
|
17
|
+
"list_apps",
|
|
18
|
+
"open",
|
|
19
|
+
"ref_action",
|
|
20
|
+
"type",
|
|
21
|
+
"scroll",
|
|
22
|
+
"global_action",
|
|
23
|
+
"wait",
|
|
24
|
+
"screenshot",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class CommandSpec:
|
|
30
|
+
daemon_kind: str
|
|
31
|
+
family: CommandFamily
|
|
32
|
+
dispatch_method_name: str
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def command_name(self) -> str:
|
|
36
|
+
return self.daemon_kind
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_FAMILY_BY_DAEMON_KIND: dict[str, CommandFamily] = {
|
|
40
|
+
"connect": "connect",
|
|
41
|
+
"observe": "observe",
|
|
42
|
+
"listApps": "list_apps",
|
|
43
|
+
"open": "open",
|
|
44
|
+
"tap": "ref_action",
|
|
45
|
+
"longTap": "ref_action",
|
|
46
|
+
"focus": "ref_action",
|
|
47
|
+
"type": "type",
|
|
48
|
+
"submit": "ref_action",
|
|
49
|
+
"scroll": "scroll",
|
|
50
|
+
"back": "global_action",
|
|
51
|
+
"home": "global_action",
|
|
52
|
+
"recents": "global_action",
|
|
53
|
+
"notifications": "global_action",
|
|
54
|
+
"wait": "wait",
|
|
55
|
+
"screenshot": "screenshot",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_FAMILY_DISPATCH_METHOD_NAMES: dict[CommandFamily, str] = {
|
|
59
|
+
"connect": "execute_connect",
|
|
60
|
+
"observe": "execute_observe",
|
|
61
|
+
"list_apps": "execute_list_apps",
|
|
62
|
+
"open": "execute_open",
|
|
63
|
+
"ref_action": "execute_ref_action",
|
|
64
|
+
"type": "execute_ref_action",
|
|
65
|
+
"scroll": "execute_ref_action",
|
|
66
|
+
"global_action": "execute_global_action",
|
|
67
|
+
"wait": "execute_wait",
|
|
68
|
+
"screenshot": "execute_screenshot",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_commands_run_daemon_kinds = daemon_command_kinds_for_route("commands_run")
|
|
72
|
+
_family_keys = set(_FAMILY_BY_DAEMON_KIND)
|
|
73
|
+
if _family_keys != _commands_run_daemon_kinds:
|
|
74
|
+
missing = sorted(_commands_run_daemon_kinds - _family_keys)
|
|
75
|
+
extra = sorted(_family_keys - _commands_run_daemon_kinds)
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
"daemon command family mapping drifted from shared catalog: "
|
|
78
|
+
f"missing={missing}, extra={extra}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
COMMAND_SPECS: dict[str, CommandSpec] = {}
|
|
82
|
+
for daemon_kind in _FAMILY_BY_DAEMON_KIND:
|
|
83
|
+
family = _FAMILY_BY_DAEMON_KIND[daemon_kind]
|
|
84
|
+
COMMAND_SPECS[daemon_kind] = CommandSpec(
|
|
85
|
+
daemon_kind=daemon_kind,
|
|
86
|
+
family=family,
|
|
87
|
+
dispatch_method_name=_FAMILY_DISPATCH_METHOD_NAMES[family],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_command_spec(command_name: str) -> CommandSpec:
|
|
92
|
+
return COMMAND_SPECS[command_name]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def resolve_command_spec(command_or_name: object) -> CommandSpec:
|
|
96
|
+
if isinstance(command_or_name, str):
|
|
97
|
+
return get_command_spec(command_or_name)
|
|
98
|
+
|
|
99
|
+
if isinstance(command_or_name, GlobalCommand):
|
|
100
|
+
return get_command_spec(command_or_name.action)
|
|
101
|
+
|
|
102
|
+
command_name = getattr(command_or_name, "kind", None)
|
|
103
|
+
if isinstance(command_name, CommandKind):
|
|
104
|
+
command_name = command_name.value
|
|
105
|
+
if not isinstance(command_name, str):
|
|
106
|
+
raise KeyError(command_or_name)
|
|
107
|
+
return get_command_spec(command_name)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = [
|
|
111
|
+
"COMMAND_SPECS",
|
|
112
|
+
"CommandFamily",
|
|
113
|
+
"CommandSpec",
|
|
114
|
+
"get_command_spec",
|
|
115
|
+
"resolve_command_spec",
|
|
116
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Builders for canonical command success results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from androidctld.app_targets import AppTargetMatch
|
|
6
|
+
from androidctld.artifacts.models import ScreenArtifacts
|
|
7
|
+
from androidctld.commands.result_models import (
|
|
8
|
+
CommandAppPayload,
|
|
9
|
+
CommandScreenPayload,
|
|
10
|
+
)
|
|
11
|
+
from androidctld.semantics.public_models import PublicScreen
|
|
12
|
+
from androidctld.snapshots.models import RawSnapshot
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def app_payload(
|
|
16
|
+
snapshot: RawSnapshot,
|
|
17
|
+
*,
|
|
18
|
+
app_match: AppTargetMatch | None = None,
|
|
19
|
+
) -> CommandAppPayload:
|
|
20
|
+
return CommandAppPayload(
|
|
21
|
+
package_name=snapshot.package_name,
|
|
22
|
+
activity_name=snapshot.activity_name,
|
|
23
|
+
requested_package_name=(
|
|
24
|
+
None if app_match is None else app_match.requested_package_name
|
|
25
|
+
),
|
|
26
|
+
resolved_package_name=(
|
|
27
|
+
None if app_match is None else app_match.resolved_package_name
|
|
28
|
+
),
|
|
29
|
+
match_type=None if app_match is None else app_match.match_type,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def screen_payload(
|
|
34
|
+
public_screen: PublicScreen, artifacts: ScreenArtifacts, *, sequence: int
|
|
35
|
+
) -> CommandScreenPayload:
|
|
36
|
+
return CommandScreenPayload(
|
|
37
|
+
screen_id=public_screen.screen_id,
|
|
38
|
+
sequence=sequence,
|
|
39
|
+
path_json=artifacts.screen_json,
|
|
40
|
+
)
|