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,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from androidctl.commands import run_pipeline
|
|
8
|
+
from androidctl.commands.plumbing import build_and_run_command
|
|
9
|
+
from androidctl_contracts.daemon_api import ObserveCommandPayload
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register(app: typer.Typer) -> None:
|
|
13
|
+
@app.command("observe")
|
|
14
|
+
def observe(
|
|
15
|
+
ctx: typer.Context,
|
|
16
|
+
workspace_root: Path | None = typer.Option(None, "--workspace-root"),
|
|
17
|
+
) -> None:
|
|
18
|
+
build_and_run_command(
|
|
19
|
+
ctx=ctx,
|
|
20
|
+
workspace_root=workspace_root,
|
|
21
|
+
build_request=lambda options: run_pipeline.CliCommandRequest(
|
|
22
|
+
public_command="observe",
|
|
23
|
+
command=ObserveCommandPayload(kind="observe"),
|
|
24
|
+
workspace_root=options.workspace_root,
|
|
25
|
+
),
|
|
26
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from androidctl.cli_options import CliOptions
|
|
8
|
+
from androidctl.commands import run_pipeline
|
|
9
|
+
from androidctl.commands.execute import emit_usage_error
|
|
10
|
+
from androidctl.commands.plumbing import build_and_run_command
|
|
11
|
+
from androidctl.parsing.open_target import parse_open_target
|
|
12
|
+
from androidctl_contracts.daemon_api import OpenCommandPayload
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register(app: typer.Typer) -> None:
|
|
16
|
+
@app.command("open")
|
|
17
|
+
def open_target(
|
|
18
|
+
ctx: typer.Context,
|
|
19
|
+
target: str = typer.Argument(
|
|
20
|
+
...,
|
|
21
|
+
help="Target app:..., url:<target>, or bare http(s)://...",
|
|
22
|
+
),
|
|
23
|
+
workspace_root: Path | None = typer.Option(None, "--workspace-root"),
|
|
24
|
+
) -> None:
|
|
25
|
+
def build_request(options: CliOptions) -> run_pipeline.CliCommandRequest:
|
|
26
|
+
try:
|
|
27
|
+
parsed_target = parse_open_target(target)
|
|
28
|
+
except ValueError as error:
|
|
29
|
+
emit_usage_error(str(error))
|
|
30
|
+
|
|
31
|
+
return run_pipeline.CliCommandRequest(
|
|
32
|
+
public_command="open",
|
|
33
|
+
command=OpenCommandPayload(kind="open", target=parsed_target),
|
|
34
|
+
workspace_root=options.workspace_root,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
build_and_run_command(
|
|
38
|
+
ctx=ctx,
|
|
39
|
+
workspace_root=workspace_root,
|
|
40
|
+
build_request=build_request,
|
|
41
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from androidctl.cli_options import CliOptions, command_cli_options
|
|
10
|
+
from androidctl.commands import run_pipeline
|
|
11
|
+
from androidctl.commands.execute import emit_usage_error, run_and_render
|
|
12
|
+
|
|
13
|
+
RequestBuilder = Callable[[CliOptions], run_pipeline.CliCommandRequest]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def build_and_run_command(
|
|
17
|
+
*,
|
|
18
|
+
ctx: typer.Context,
|
|
19
|
+
workspace_root: Path | None,
|
|
20
|
+
build_request: RequestBuilder,
|
|
21
|
+
public_command: str | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
options = command_cli_options(
|
|
24
|
+
ctx,
|
|
25
|
+
workspace_root=workspace_root,
|
|
26
|
+
)
|
|
27
|
+
try:
|
|
28
|
+
request = build_request(options)
|
|
29
|
+
except ValidationError as error:
|
|
30
|
+
emit_usage_error(
|
|
31
|
+
_validation_error_message(error),
|
|
32
|
+
command=public_command or _context_command_name(ctx),
|
|
33
|
+
)
|
|
34
|
+
run_and_render(
|
|
35
|
+
request,
|
|
36
|
+
public_command=public_command or _context_command_name(ctx),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _context_command_name(ctx: typer.Context) -> str | None:
|
|
41
|
+
command_name = ctx.info_name
|
|
42
|
+
if isinstance(command_name, str) and command_name:
|
|
43
|
+
return command_name
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _validation_error_message(error: ValidationError) -> str:
|
|
48
|
+
details = error.errors(include_url=False)
|
|
49
|
+
if not details:
|
|
50
|
+
return str(error)
|
|
51
|
+
first = details[0]
|
|
52
|
+
location = ".".join(
|
|
53
|
+
str(part) for part in first.get("loc", ()) if part != "__root__"
|
|
54
|
+
).strip(".")
|
|
55
|
+
message = str(first.get("msg", "invalid input"))
|
|
56
|
+
if not location:
|
|
57
|
+
return message
|
|
58
|
+
return f"{location}: {message}"
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Callable, Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import NoReturn, Protocol
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import httpx
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
|
|
13
|
+
from androidctl.command_payloads import (
|
|
14
|
+
CliCommandPayload,
|
|
15
|
+
LateBoundActionCommand,
|
|
16
|
+
LateBoundGlobalActionCommand,
|
|
17
|
+
LateBoundWaitCommand,
|
|
18
|
+
)
|
|
19
|
+
from androidctl.daemon.client import (
|
|
20
|
+
DaemonApiError,
|
|
21
|
+
DaemonProtocolError,
|
|
22
|
+
IncompatibleDaemonError,
|
|
23
|
+
)
|
|
24
|
+
from androidctl.daemon.discovery import (
|
|
25
|
+
discover_existing_daemon_client,
|
|
26
|
+
resolve_daemon_client,
|
|
27
|
+
)
|
|
28
|
+
from androidctl.errors.models import ErrorTier
|
|
29
|
+
from androidctl.workspace.resolve import resolve_workspace_root
|
|
30
|
+
from androidctl_contracts.command_catalog import runtime_close_entry
|
|
31
|
+
from androidctl_contracts.command_results import (
|
|
32
|
+
CommandResultCore,
|
|
33
|
+
ListAppsResult,
|
|
34
|
+
RetainedResultEnvelope,
|
|
35
|
+
dump_canonical_command_result,
|
|
36
|
+
)
|
|
37
|
+
from androidctl_contracts.daemon_api import (
|
|
38
|
+
CommandRunRequest,
|
|
39
|
+
RuntimePayload,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
CommandResultPayload = CommandResultCore | RetainedResultEnvelope | ListAppsResult
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RuntimeCommandClient(Protocol):
|
|
46
|
+
def get_runtime(self) -> RuntimePayload: ...
|
|
47
|
+
|
|
48
|
+
def run_command(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
request: CommandRunRequest,
|
|
52
|
+
) -> CommandResultPayload: ...
|
|
53
|
+
|
|
54
|
+
def close_runtime(self) -> RetainedResultEnvelope: ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PreDispatchCommandError(Exception):
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
cause: Exception,
|
|
61
|
+
*,
|
|
62
|
+
execution_outcome: str | None = None,
|
|
63
|
+
error_tier: ErrorTier | None = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
super().__init__(str(cause))
|
|
66
|
+
self.cause = cause
|
|
67
|
+
self.execution_outcome = execution_outcome
|
|
68
|
+
self.error_tier = error_tier
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class CliCommandRequest:
|
|
73
|
+
public_command: str
|
|
74
|
+
command: CliCommandPayload
|
|
75
|
+
workspace_root: Path | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class CommandOutcome:
|
|
80
|
+
payload: dict[str, object]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class AppContext:
|
|
85
|
+
daemon: RuntimeCommandClient | None
|
|
86
|
+
cwd: Path
|
|
87
|
+
env: Mapping[str, str]
|
|
88
|
+
daemon_discovery: Callable[[Path], RuntimeCommandClient] | None = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def build_context() -> AppContext:
|
|
92
|
+
return AppContext(
|
|
93
|
+
daemon=None,
|
|
94
|
+
cwd=Path.cwd(),
|
|
95
|
+
env=os.environ,
|
|
96
|
+
daemon_discovery=lambda workspace_root: resolve_daemon_client(
|
|
97
|
+
workspace_root=workspace_root,
|
|
98
|
+
cwd=Path.cwd(),
|
|
99
|
+
env=os.environ,
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run_command(cli_request: CliCommandRequest, ctx: AppContext) -> CommandOutcome:
|
|
105
|
+
try:
|
|
106
|
+
workspace_root = resolve_runtime_paths(cli_request.workspace_root, ctx)
|
|
107
|
+
except (OSError, ValueError) as error:
|
|
108
|
+
_raise_pre_dispatch_error(error, cli_request.command)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
daemon = _resolve_command_daemon(ctx, workspace_root)
|
|
112
|
+
except (DaemonApiError, click.ClickException, OSError, ValidationError) as error:
|
|
113
|
+
_raise_pre_dispatch_error(error, cli_request.command)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
runtime_payload = daemon.get_runtime()
|
|
117
|
+
except (
|
|
118
|
+
DaemonApiError,
|
|
119
|
+
DaemonProtocolError,
|
|
120
|
+
OSError,
|
|
121
|
+
ValidationError,
|
|
122
|
+
httpx.HTTPStatusError,
|
|
123
|
+
httpx.RequestError,
|
|
124
|
+
) as error:
|
|
125
|
+
_raise_pre_dispatch_error(error, cli_request.command)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
prepared_request = _prepare_ref_bound_request(cli_request, runtime_payload)
|
|
129
|
+
except DaemonApiError as error:
|
|
130
|
+
_raise_pre_dispatch_error(
|
|
131
|
+
error,
|
|
132
|
+
cli_request.command,
|
|
133
|
+
error_tier=_late_bind_error_tier(error),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
command_request = _build_command_run_request(prepared_request.command)
|
|
138
|
+
except ValidationError as error:
|
|
139
|
+
_raise_pre_dispatch_error(error, cli_request.command)
|
|
140
|
+
|
|
141
|
+
result = daemon.run_command(request=command_request)
|
|
142
|
+
return CommandOutcome(payload=_dump_command_result(result))
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def run_close_command(
|
|
146
|
+
ctx: AppContext,
|
|
147
|
+
workspace_root_override: Path | None,
|
|
148
|
+
) -> CommandOutcome:
|
|
149
|
+
workspace_root = resolve_runtime_paths(workspace_root_override, ctx)
|
|
150
|
+
daemon = ctx.daemon
|
|
151
|
+
if daemon is None:
|
|
152
|
+
daemon = discover_existing_daemon_client(
|
|
153
|
+
workspace_root=workspace_root,
|
|
154
|
+
env=ctx.env,
|
|
155
|
+
)
|
|
156
|
+
result = _close_result(daemon)
|
|
157
|
+
return CommandOutcome(payload=_dump_command_result(result))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def resolve_runtime_paths(
|
|
161
|
+
workspace_root_override: Path | None,
|
|
162
|
+
ctx: AppContext,
|
|
163
|
+
) -> Path:
|
|
164
|
+
return resolve_workspace_root(
|
|
165
|
+
flag_value=workspace_root_override,
|
|
166
|
+
env_value=ctx.env.get("ANDROIDCTL_WORKSPACE_ROOT"),
|
|
167
|
+
cwd=ctx.cwd,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _prepare_ref_bound_request(
|
|
172
|
+
cli_request: CliCommandRequest,
|
|
173
|
+
runtime_payload: RuntimePayload,
|
|
174
|
+
) -> CliCommandRequest:
|
|
175
|
+
bound_command = bind_screen_relative_command(cli_request.command, runtime_payload)
|
|
176
|
+
if bound_command is cli_request.command:
|
|
177
|
+
return cli_request
|
|
178
|
+
return CliCommandRequest(
|
|
179
|
+
public_command=cli_request.public_command,
|
|
180
|
+
command=bound_command,
|
|
181
|
+
workspace_root=cli_request.workspace_root,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def bind_screen_relative_command(
|
|
186
|
+
command: CliCommandPayload,
|
|
187
|
+
runtime_payload: RuntimePayload,
|
|
188
|
+
) -> CliCommandPayload:
|
|
189
|
+
if not isinstance(
|
|
190
|
+
command,
|
|
191
|
+
(
|
|
192
|
+
LateBoundActionCommand,
|
|
193
|
+
LateBoundGlobalActionCommand,
|
|
194
|
+
LateBoundWaitCommand,
|
|
195
|
+
),
|
|
196
|
+
):
|
|
197
|
+
return command
|
|
198
|
+
|
|
199
|
+
if isinstance(command, LateBoundGlobalActionCommand):
|
|
200
|
+
return command.bind(_live_screen_id(runtime_payload))
|
|
201
|
+
|
|
202
|
+
return command.bind(_required_live_screen_id(runtime_payload))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _required_live_screen_id(runtime_payload: RuntimePayload) -> str:
|
|
206
|
+
live_screen_id = _live_screen_id(runtime_payload)
|
|
207
|
+
if live_screen_id is not None:
|
|
208
|
+
return live_screen_id
|
|
209
|
+
status = runtime_payload.status.strip().lower()
|
|
210
|
+
if status in {"ready", "connected"}:
|
|
211
|
+
raise DaemonApiError(
|
|
212
|
+
code="SCREEN_NOT_READY",
|
|
213
|
+
message="screen is not ready yet",
|
|
214
|
+
details={},
|
|
215
|
+
)
|
|
216
|
+
raise DaemonApiError(
|
|
217
|
+
code="RUNTIME_NOT_CONNECTED",
|
|
218
|
+
message="runtime is not connected to a device",
|
|
219
|
+
details={},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _live_screen_id(runtime_payload: RuntimePayload) -> str | None:
|
|
224
|
+
status = runtime_payload.status.strip().lower()
|
|
225
|
+
current_screen_id = runtime_payload.current_screen_id
|
|
226
|
+
if status != "ready" or not isinstance(current_screen_id, str):
|
|
227
|
+
return None
|
|
228
|
+
normalized_screen_id = current_screen_id.strip()
|
|
229
|
+
return normalized_screen_id or None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _resolve_command_daemon(
|
|
233
|
+
ctx: AppContext,
|
|
234
|
+
workspace_root: Path,
|
|
235
|
+
) -> RuntimeCommandClient:
|
|
236
|
+
if ctx.daemon is not None:
|
|
237
|
+
return ctx.daemon
|
|
238
|
+
if ctx.daemon_discovery is None:
|
|
239
|
+
raise click.ClickException("unable to start or discover androidctld daemon")
|
|
240
|
+
try:
|
|
241
|
+
return ctx.daemon_discovery(workspace_root)
|
|
242
|
+
except (DaemonApiError, IncompatibleDaemonError):
|
|
243
|
+
raise
|
|
244
|
+
except (FileNotFoundError, OSError, RuntimeError, ValidationError) as error:
|
|
245
|
+
raise click.ClickException(
|
|
246
|
+
f"unable to start or discover androidctld daemon: {error}"
|
|
247
|
+
) from error
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _build_command_run_request(command: CliCommandPayload) -> CommandRunRequest:
|
|
251
|
+
if isinstance(
|
|
252
|
+
command,
|
|
253
|
+
(
|
|
254
|
+
LateBoundActionCommand,
|
|
255
|
+
LateBoundGlobalActionCommand,
|
|
256
|
+
LateBoundWaitCommand,
|
|
257
|
+
),
|
|
258
|
+
):
|
|
259
|
+
raise RuntimeError("prepared command was not bound to a live screen")
|
|
260
|
+
return CommandRunRequest.model_validate(
|
|
261
|
+
{"command": command.model_dump(exclude_none=True, exclude_defaults=True)}
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _raise_pre_dispatch_error(
|
|
266
|
+
error: Exception,
|
|
267
|
+
command: CliCommandPayload,
|
|
268
|
+
*,
|
|
269
|
+
error_tier: ErrorTier | None = None,
|
|
270
|
+
) -> NoReturn:
|
|
271
|
+
del command
|
|
272
|
+
raise PreDispatchCommandError(
|
|
273
|
+
error,
|
|
274
|
+
execution_outcome=None,
|
|
275
|
+
error_tier=error_tier,
|
|
276
|
+
) from error
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _late_bind_error_tier(error: DaemonApiError) -> ErrorTier | None:
|
|
280
|
+
if error.code in {"RUNTIME_NOT_CONNECTED", "SCREEN_NOT_READY"}:
|
|
281
|
+
return "preDispatch"
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _dump_command_result(result: CommandResultPayload) -> dict[str, object]:
|
|
286
|
+
if isinstance(result, CommandResultCore):
|
|
287
|
+
return dump_canonical_command_result(result)
|
|
288
|
+
return result.model_dump(by_alias=True, mode="json", exclude_none=True)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _close_result(
|
|
292
|
+
daemon: RuntimeCommandClient | None,
|
|
293
|
+
) -> RetainedResultEnvelope:
|
|
294
|
+
if daemon is not None:
|
|
295
|
+
return daemon.close_runtime()
|
|
296
|
+
close_entry = runtime_close_entry()
|
|
297
|
+
if close_entry.retained_envelope_kind is None:
|
|
298
|
+
raise RuntimeError("runtime close command must use a retained envelope")
|
|
299
|
+
return RetainedResultEnvelope.model_validate(
|
|
300
|
+
{
|
|
301
|
+
"ok": True,
|
|
302
|
+
"command": close_entry.result_command,
|
|
303
|
+
"envelope": close_entry.retained_envelope_kind.value,
|
|
304
|
+
"artifacts": {},
|
|
305
|
+
"details": {},
|
|
306
|
+
}
|
|
307
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from androidctl.commands import run_pipeline
|
|
8
|
+
from androidctl.commands.plumbing import build_and_run_command
|
|
9
|
+
from androidctl_contracts.daemon_api import ScreenshotCommandPayload
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register(app: typer.Typer) -> None:
|
|
13
|
+
@app.command(
|
|
14
|
+
"screenshot",
|
|
15
|
+
help="Retained support route that captures an explicit screenshot artifact.",
|
|
16
|
+
)
|
|
17
|
+
def screenshot(
|
|
18
|
+
ctx: typer.Context,
|
|
19
|
+
workspace_root: Path | None = typer.Option(None, "--workspace-root"),
|
|
20
|
+
) -> None:
|
|
21
|
+
build_and_run_command(
|
|
22
|
+
ctx=ctx,
|
|
23
|
+
workspace_root=workspace_root,
|
|
24
|
+
build_request=lambda options: run_pipeline.CliCommandRequest(
|
|
25
|
+
public_command="screenshot",
|
|
26
|
+
command=ScreenshotCommandPayload(kind="screenshot"),
|
|
27
|
+
workspace_root=options.workspace_root,
|
|
28
|
+
),
|
|
29
|
+
)
|