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,286 @@
|
|
|
1
|
+
"""Semantic wait command handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from androidctld.commands.command_models import (
|
|
6
|
+
GoneWaitPredicate,
|
|
7
|
+
ScreenChangeWaitPredicate,
|
|
8
|
+
WaitCommand,
|
|
9
|
+
)
|
|
10
|
+
from androidctld.commands.orchestration import current_command_record
|
|
11
|
+
from androidctld.commands.result_builders import app_payload
|
|
12
|
+
from androidctld.commands.result_models import (
|
|
13
|
+
SemanticResultAssemblyInput,
|
|
14
|
+
build_semantic_failure_result,
|
|
15
|
+
build_semantic_success_result,
|
|
16
|
+
)
|
|
17
|
+
from androidctld.commands.semantic_error_mapping import (
|
|
18
|
+
map_daemon_error_to_semantic_failure,
|
|
19
|
+
)
|
|
20
|
+
from androidctld.commands.semantic_truth import (
|
|
21
|
+
resolve_runtime_continuity,
|
|
22
|
+
)
|
|
23
|
+
from androidctld.errors import DaemonError, DaemonErrorCode
|
|
24
|
+
from androidctld.runtime import RuntimeKernel
|
|
25
|
+
from androidctld.runtime.models import WorkspaceRuntime
|
|
26
|
+
from androidctld.runtime.screen_state import (
|
|
27
|
+
current_artifacts,
|
|
28
|
+
current_public_screen,
|
|
29
|
+
get_authoritative_current_basis,
|
|
30
|
+
)
|
|
31
|
+
from androidctld.semantics.compiler import CompiledScreen
|
|
32
|
+
from androidctld.waits.evaluators import WaitMatchData
|
|
33
|
+
from androidctld.waits.loop import (
|
|
34
|
+
WaitLoopOutcome,
|
|
35
|
+
WaitRuntimeLoop,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class WaitCommandHandler:
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
*,
|
|
43
|
+
runtime_kernel: RuntimeKernel,
|
|
44
|
+
wait_runtime_loop: WaitRuntimeLoop,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._runtime_kernel = runtime_kernel
|
|
47
|
+
self._wait_runtime_loop = wait_runtime_loop
|
|
48
|
+
|
|
49
|
+
def handle_service_wait(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
command: WaitCommand,
|
|
53
|
+
) -> dict[str, object]:
|
|
54
|
+
runtime = self._runtime_kernel.ensure_runtime()
|
|
55
|
+
source_screen_id = _wait_basis_screen_id(command)
|
|
56
|
+
source_compiled_screen = None
|
|
57
|
+
try:
|
|
58
|
+
source_compiled_screen = _validate_relative_wait_entry(
|
|
59
|
+
runtime=runtime,
|
|
60
|
+
command=command,
|
|
61
|
+
)
|
|
62
|
+
_ensure_wait_runtime_connected(runtime)
|
|
63
|
+
lifecycle_lease = self._runtime_kernel.capture_lifecycle_lease(runtime)
|
|
64
|
+
outcome = self._wait_runtime_loop.run(
|
|
65
|
+
session=runtime,
|
|
66
|
+
record=current_command_record(
|
|
67
|
+
kind=command.kind,
|
|
68
|
+
result_command="wait",
|
|
69
|
+
),
|
|
70
|
+
command=command,
|
|
71
|
+
lifecycle_lease=lifecycle_lease,
|
|
72
|
+
)
|
|
73
|
+
assembly_input = _wait_loop_result(command=command, outcome=outcome)
|
|
74
|
+
continuity_status, changed = _wait_success_overrides(
|
|
75
|
+
command=command,
|
|
76
|
+
runtime=runtime,
|
|
77
|
+
)
|
|
78
|
+
return _wait_success_payload(
|
|
79
|
+
runtime,
|
|
80
|
+
source_screen_id=source_screen_id,
|
|
81
|
+
source_compiled_screen=source_compiled_screen,
|
|
82
|
+
assembly_input=assembly_input,
|
|
83
|
+
continuity_status=continuity_status,
|
|
84
|
+
changed=changed,
|
|
85
|
+
)
|
|
86
|
+
except DaemonError as error:
|
|
87
|
+
return _wait_failure_payload(
|
|
88
|
+
runtime,
|
|
89
|
+
source_screen_id=source_screen_id,
|
|
90
|
+
source_compiled_screen=source_compiled_screen,
|
|
91
|
+
error=error,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _ensure_wait_runtime_connected(runtime: WorkspaceRuntime) -> None:
|
|
96
|
+
if runtime.connection is None or runtime.device_token is None:
|
|
97
|
+
raise DaemonError(
|
|
98
|
+
code=DaemonErrorCode.RUNTIME_NOT_CONNECTED,
|
|
99
|
+
message="runtime is not connected to a device",
|
|
100
|
+
retryable=False,
|
|
101
|
+
details={"workspaceRoot": runtime.workspace_root.as_posix()},
|
|
102
|
+
http_status=200,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _wait_loop_result(
|
|
107
|
+
*,
|
|
108
|
+
command: WaitCommand,
|
|
109
|
+
outcome: WaitLoopOutcome,
|
|
110
|
+
) -> SemanticResultAssemblyInput:
|
|
111
|
+
if isinstance(outcome, WaitMatchData):
|
|
112
|
+
return SemanticResultAssemblyInput(
|
|
113
|
+
app_payload=app_payload(
|
|
114
|
+
outcome.snapshot,
|
|
115
|
+
app_match=outcome.app_match,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
raise _wait_timeout_error(command)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _wait_timeout_error(command: WaitCommand) -> DaemonError:
|
|
122
|
+
return DaemonError(
|
|
123
|
+
code=DaemonErrorCode.WAIT_TIMEOUT,
|
|
124
|
+
message=f"wait {command.wait_kind.value} timed out",
|
|
125
|
+
retryable=True,
|
|
126
|
+
details={"kind": command.kind.value, "waitKind": command.wait_kind.value},
|
|
127
|
+
http_status=200,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _wait_success_payload(
|
|
132
|
+
runtime: WorkspaceRuntime,
|
|
133
|
+
*,
|
|
134
|
+
source_screen_id: str | None,
|
|
135
|
+
source_compiled_screen: CompiledScreen | None,
|
|
136
|
+
assembly_input: SemanticResultAssemblyInput | None = None,
|
|
137
|
+
continuity_status: str | None = None,
|
|
138
|
+
changed: bool | None = None,
|
|
139
|
+
) -> dict[str, object]:
|
|
140
|
+
current_screen = current_public_screen(runtime)
|
|
141
|
+
artifacts = current_artifacts(runtime)
|
|
142
|
+
resolved_continuity, resolved_changed = _resolve_wait_continuity(
|
|
143
|
+
runtime=runtime,
|
|
144
|
+
source_screen_id=source_screen_id,
|
|
145
|
+
source_compiled_screen=source_compiled_screen,
|
|
146
|
+
continuity_status=continuity_status,
|
|
147
|
+
changed=changed,
|
|
148
|
+
)
|
|
149
|
+
return build_semantic_success_result(
|
|
150
|
+
command="wait",
|
|
151
|
+
category="wait",
|
|
152
|
+
source_screen_id=source_screen_id,
|
|
153
|
+
next_screen=current_screen,
|
|
154
|
+
app_payload=(None if assembly_input is None else assembly_input.app_payload),
|
|
155
|
+
artifacts=artifacts,
|
|
156
|
+
continuity_status=resolved_continuity,
|
|
157
|
+
execution_outcome="notApplicable",
|
|
158
|
+
changed=resolved_changed,
|
|
159
|
+
warnings=([] if assembly_input is None else list(assembly_input.warnings)),
|
|
160
|
+
).model_dump(by_alias=True, mode="json")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _wait_failure_payload(
|
|
164
|
+
runtime: WorkspaceRuntime,
|
|
165
|
+
*,
|
|
166
|
+
source_screen_id: str | None,
|
|
167
|
+
source_compiled_screen: CompiledScreen | None,
|
|
168
|
+
error: DaemonError,
|
|
169
|
+
) -> dict[str, object]:
|
|
170
|
+
current_screen = current_public_screen(runtime)
|
|
171
|
+
mapped = map_daemon_error_to_semantic_failure(
|
|
172
|
+
command_name="wait",
|
|
173
|
+
error=error,
|
|
174
|
+
)
|
|
175
|
+
if mapped is None:
|
|
176
|
+
raise error
|
|
177
|
+
continuity_status, changed = _resolve_wait_continuity(
|
|
178
|
+
runtime=runtime,
|
|
179
|
+
source_screen_id=source_screen_id,
|
|
180
|
+
source_compiled_screen=source_compiled_screen,
|
|
181
|
+
)
|
|
182
|
+
return build_semantic_failure_result(
|
|
183
|
+
command="wait",
|
|
184
|
+
category="wait",
|
|
185
|
+
code=mapped.code,
|
|
186
|
+
message=mapped.message,
|
|
187
|
+
source_screen_id=source_screen_id,
|
|
188
|
+
current_screen=current_screen,
|
|
189
|
+
artifacts=current_artifacts(runtime),
|
|
190
|
+
continuity_status=continuity_status,
|
|
191
|
+
observation_quality="authoritative" if current_screen is not None else "none",
|
|
192
|
+
changed=changed,
|
|
193
|
+
).model_dump(by_alias=True, mode="json")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _resolve_wait_continuity(
|
|
197
|
+
*,
|
|
198
|
+
runtime: WorkspaceRuntime,
|
|
199
|
+
source_screen_id: str | None,
|
|
200
|
+
source_compiled_screen: CompiledScreen | None,
|
|
201
|
+
continuity_status: str | None = None,
|
|
202
|
+
changed: bool | None = None,
|
|
203
|
+
) -> tuple[str, bool | None]:
|
|
204
|
+
continuity = resolve_runtime_continuity(
|
|
205
|
+
runtime=runtime,
|
|
206
|
+
source_screen_id=source_screen_id,
|
|
207
|
+
source_compiled_screen=source_compiled_screen,
|
|
208
|
+
)
|
|
209
|
+
return (
|
|
210
|
+
(
|
|
211
|
+
continuity.continuity_status
|
|
212
|
+
if continuity_status is None
|
|
213
|
+
else continuity_status
|
|
214
|
+
),
|
|
215
|
+
continuity.changed if changed is None else changed,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _wait_success_overrides(
|
|
220
|
+
*,
|
|
221
|
+
command: WaitCommand,
|
|
222
|
+
runtime: WorkspaceRuntime,
|
|
223
|
+
) -> tuple[str | None, bool | None]:
|
|
224
|
+
current_screen = current_public_screen(runtime)
|
|
225
|
+
if current_screen is None:
|
|
226
|
+
return None, None
|
|
227
|
+
predicate = command.predicate
|
|
228
|
+
if isinstance(predicate, GoneWaitPredicate):
|
|
229
|
+
return "stale", True
|
|
230
|
+
if (
|
|
231
|
+
isinstance(predicate, ScreenChangeWaitPredicate)
|
|
232
|
+
and current_screen.screen_id != predicate.source_screen_id
|
|
233
|
+
):
|
|
234
|
+
return None, True
|
|
235
|
+
return None, None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _wait_basis_screen_id(command: WaitCommand) -> str | None:
|
|
239
|
+
predicate = _relative_wait_predicate(command)
|
|
240
|
+
if predicate is not None:
|
|
241
|
+
return predicate.source_screen_id
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _relative_wait_predicate(
|
|
246
|
+
command: WaitCommand,
|
|
247
|
+
) -> ScreenChangeWaitPredicate | GoneWaitPredicate | None:
|
|
248
|
+
predicate = command.predicate
|
|
249
|
+
if isinstance(predicate, (ScreenChangeWaitPredicate, GoneWaitPredicate)):
|
|
250
|
+
return predicate
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _validate_relative_wait_entry(
|
|
255
|
+
*,
|
|
256
|
+
runtime: WorkspaceRuntime,
|
|
257
|
+
command: WaitCommand,
|
|
258
|
+
) -> CompiledScreen | None:
|
|
259
|
+
predicate = _relative_wait_predicate(command)
|
|
260
|
+
if predicate is None:
|
|
261
|
+
return None
|
|
262
|
+
basis = get_authoritative_current_basis(runtime)
|
|
263
|
+
if basis is None or basis.screen_id != predicate.source_screen_id:
|
|
264
|
+
raise _relative_wait_entry_unavailable(command)
|
|
265
|
+
if (
|
|
266
|
+
isinstance(predicate, GoneWaitPredicate)
|
|
267
|
+
and predicate.ref not in basis.public_refs
|
|
268
|
+
):
|
|
269
|
+
raise _relative_wait_entry_unavailable(command)
|
|
270
|
+
return basis.compiled_screen
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _relative_wait_entry_unavailable(command: WaitCommand) -> DaemonError:
|
|
274
|
+
details: dict[str, object] = {"waitKind": command.wait_kind.value}
|
|
275
|
+
predicate = _relative_wait_predicate(command)
|
|
276
|
+
if predicate is not None:
|
|
277
|
+
details["sourceScreenId"] = predicate.source_screen_id
|
|
278
|
+
if isinstance(predicate, GoneWaitPredicate):
|
|
279
|
+
details["ref"] = predicate.ref
|
|
280
|
+
return DaemonError(
|
|
281
|
+
code=DaemonErrorCode.SCREEN_NOT_READY,
|
|
282
|
+
message="No current device observation is available.",
|
|
283
|
+
retryable=True,
|
|
284
|
+
details=details,
|
|
285
|
+
http_status=200,
|
|
286
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Typed command ledger models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from androidctl_contracts.command_results import (
|
|
10
|
+
CommandResultCore,
|
|
11
|
+
ListAppsResult,
|
|
12
|
+
RetainedResultEnvelope,
|
|
13
|
+
)
|
|
14
|
+
from androidctld.commands.semantic_command_names import (
|
|
15
|
+
semantic_result_command_for_daemon_kind,
|
|
16
|
+
)
|
|
17
|
+
from androidctld.errors import DaemonError, DaemonErrorCode
|
|
18
|
+
from androidctld.protocol import CommandKind
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CommandStatus(str, Enum):
|
|
22
|
+
RUNNING = "running"
|
|
23
|
+
SUCCEEDED = "succeeded"
|
|
24
|
+
FAILED = "failed"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class CachedCommandError:
|
|
29
|
+
code: DaemonErrorCode
|
|
30
|
+
message: str
|
|
31
|
+
retryable: bool
|
|
32
|
+
details: dict[str, Any]
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
object.__setattr__(self, "code", DaemonErrorCode(self.code))
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_daemon_error(cls, error: DaemonError) -> CachedCommandError:
|
|
39
|
+
return cls(
|
|
40
|
+
code=error.code,
|
|
41
|
+
message=error.message,
|
|
42
|
+
retryable=error.retryable,
|
|
43
|
+
details=dict(error.details),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class CommandRecord:
|
|
49
|
+
command_id: str
|
|
50
|
+
kind: CommandKind
|
|
51
|
+
status: CommandStatus
|
|
52
|
+
started_at: str
|
|
53
|
+
result_command: str | None = None
|
|
54
|
+
completed_at: str | None = None
|
|
55
|
+
result: CommandResultCore | RetainedResultEnvelope | ListAppsResult | None = None
|
|
56
|
+
error: CachedCommandError | None = None
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
if self.result_command is None:
|
|
60
|
+
self.result_command = semantic_result_command_for_daemon_kind(self.kind)
|
|
61
|
+
return
|
|
62
|
+
normalized_result_command = self.result_command.strip()
|
|
63
|
+
if not normalized_result_command:
|
|
64
|
+
raise ValueError("result_command must be a non-empty string")
|
|
65
|
+
self.result_command = normalized_result_command
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Typed runtime open-target ADTs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
from androidctld.errors import bad_request
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class OpenAppTarget:
|
|
12
|
+
package_name: str
|
|
13
|
+
kind: str = field(default="app", init=False)
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def value(self) -> str:
|
|
17
|
+
return self.package_name
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def required_action_kind(self) -> str:
|
|
21
|
+
return "launchApp"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class OpenUrlTarget:
|
|
26
|
+
url: str
|
|
27
|
+
kind: str = field(default="url", init=False)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def value(self) -> str:
|
|
31
|
+
return self.url
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def required_action_kind(self) -> str:
|
|
35
|
+
return "openUrl"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def validate_open_target(
|
|
39
|
+
target: OpenAppTarget | OpenUrlTarget,
|
|
40
|
+
) -> OpenAppTarget | OpenUrlTarget:
|
|
41
|
+
if isinstance(target, OpenAppTarget):
|
|
42
|
+
if not target.package_name:
|
|
43
|
+
raise bad_request("open requires target.kind app|url and target.value")
|
|
44
|
+
return target
|
|
45
|
+
if isinstance(target, OpenUrlTarget):
|
|
46
|
+
if not target.url:
|
|
47
|
+
raise bad_request("open requires target.kind app|url and target.value")
|
|
48
|
+
return target
|
|
49
|
+
raise bad_request("open requires target.kind app|url and target.value")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"OpenAppTarget",
|
|
54
|
+
"OpenUrlTarget",
|
|
55
|
+
"validate_open_target",
|
|
56
|
+
]
|