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,58 @@
|
|
|
1
|
+
"""Typed device-client protocols for runtime collaborators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from androidctld.device.action_models import DeviceActionRequest
|
|
8
|
+
from androidctld.device.types import (
|
|
9
|
+
ActionPerformResult,
|
|
10
|
+
EventsPollResult,
|
|
11
|
+
ScreenshotCaptureResult,
|
|
12
|
+
)
|
|
13
|
+
from androidctld.runtime.lifecycle import RuntimeLifecycleLease
|
|
14
|
+
from androidctld.runtime.models import WorkspaceRuntime
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EventPollingClient(Protocol):
|
|
18
|
+
def events_poll(
|
|
19
|
+
self, after_seq: int, wait_ms: int, limit: int, request_id: str
|
|
20
|
+
) -> EventsPollResult: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ActionPerformingClient(Protocol):
|
|
24
|
+
def action_perform(
|
|
25
|
+
self, request: DeviceActionRequest, request_id: str
|
|
26
|
+
) -> ActionPerformResult: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ScreenshotCaptureClient(Protocol):
|
|
30
|
+
def screenshot_capture(self, request_id: str) -> ScreenshotCaptureResult: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DeviceRuntimeClient(
|
|
34
|
+
EventPollingClient,
|
|
35
|
+
ActionPerformingClient,
|
|
36
|
+
ScreenshotCaptureClient,
|
|
37
|
+
Protocol,
|
|
38
|
+
):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@runtime_checkable
|
|
43
|
+
class DeviceClientProvider(Protocol):
|
|
44
|
+
def device_client(
|
|
45
|
+
self,
|
|
46
|
+
session: WorkspaceRuntime,
|
|
47
|
+
*,
|
|
48
|
+
lifecycle_lease: RuntimeLifecycleLease | None = None,
|
|
49
|
+
) -> DeviceRuntimeClient: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DeviceClientFactory(Protocol):
|
|
53
|
+
def __call__(
|
|
54
|
+
self,
|
|
55
|
+
session: WorkspaceRuntime,
|
|
56
|
+
*,
|
|
57
|
+
lifecycle_lease: RuntimeLifecycleLease | None = None,
|
|
58
|
+
) -> DeviceRuntimeClient: ...
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Boundary validation entrypoints for device RPC payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import binascii
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from androidctld.device.adapters import (
|
|
13
|
+
adapt_action_perform_result,
|
|
14
|
+
adapt_events_poll_result,
|
|
15
|
+
adapt_meta_payload,
|
|
16
|
+
adapt_rpc_error_payload,
|
|
17
|
+
adapt_screenshot_capture_result,
|
|
18
|
+
)
|
|
19
|
+
from androidctld.device.errors import DeviceBootstrapError, device_rpc_failed
|
|
20
|
+
from androidctld.device.schema import (
|
|
21
|
+
ActionPerformResultPayload,
|
|
22
|
+
EventsPollResultPayload,
|
|
23
|
+
MetaPayload,
|
|
24
|
+
RpcErrorPayload,
|
|
25
|
+
ScreenshotCaptureResultPayload,
|
|
26
|
+
)
|
|
27
|
+
from androidctld.device.types import (
|
|
28
|
+
ActionPerformResult,
|
|
29
|
+
EventsPollResult,
|
|
30
|
+
MetaInfo,
|
|
31
|
+
ScreenshotCaptureResult,
|
|
32
|
+
)
|
|
33
|
+
from androidctld.runtime_policy import (
|
|
34
|
+
SCREENSHOT_MAX_BASE64_CHARS,
|
|
35
|
+
SCREENSHOT_MAX_BINARY_BYTES,
|
|
36
|
+
SCREENSHOT_MAX_OUTPUT_PIXELS,
|
|
37
|
+
)
|
|
38
|
+
from androidctld.schema import ApiModel
|
|
39
|
+
from androidctld.schema.core import SchemaDecodeError
|
|
40
|
+
from androidctld.schema.validation_errors import (
|
|
41
|
+
validation_error_to_device_bootstrap_error,
|
|
42
|
+
validation_error_to_schema_decode_error,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
ModelT = TypeVar("ModelT", bound=ApiModel)
|
|
46
|
+
ResultT = TypeVar("ResultT")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_meta_payload(payload: object) -> MetaInfo:
|
|
50
|
+
return _adapt_payload(
|
|
51
|
+
lambda item: adapt_meta_payload(item, field_name="result"),
|
|
52
|
+
_validate_payload(MetaPayload, payload, field_name="result"),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_rpc_error_payload(payload: object) -> DeviceBootstrapError:
|
|
57
|
+
return adapt_rpc_error_payload(
|
|
58
|
+
_validate_payload(RpcErrorPayload, payload, field_name="error")
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def parse_action_perform_result(payload: object) -> ActionPerformResult:
|
|
63
|
+
return _adapt_payload(
|
|
64
|
+
lambda item: adapt_action_perform_result(item, field_name="result"),
|
|
65
|
+
_validate_action_perform_result_payload(payload, field_name="result"),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_events_poll_result(payload: object) -> EventsPollResult:
|
|
70
|
+
return _adapt_payload(
|
|
71
|
+
lambda item: adapt_events_poll_result(item, field_name="result"),
|
|
72
|
+
_validate_payload(EventsPollResultPayload, payload, field_name="result"),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_screenshot_capture_result(payload: object) -> ScreenshotCaptureResult:
|
|
77
|
+
screenshot_payload = _validate_payload(
|
|
78
|
+
ScreenshotCaptureResultPayload,
|
|
79
|
+
payload,
|
|
80
|
+
field_name="result",
|
|
81
|
+
)
|
|
82
|
+
_validate_screenshot_capture_payload(screenshot_payload, field_name="result")
|
|
83
|
+
validate_screenshot_body_base64_budget(
|
|
84
|
+
screenshot_payload.body_base64,
|
|
85
|
+
field_name="result.bodyBase64",
|
|
86
|
+
)
|
|
87
|
+
return _adapt_payload(
|
|
88
|
+
lambda item: adapt_screenshot_capture_result(item, field_name="result"),
|
|
89
|
+
screenshot_payload,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def validate_screenshot_body_base64_budget(value: str, *, field_name: str) -> None:
|
|
94
|
+
if len(value) > SCREENSHOT_MAX_BASE64_CHARS:
|
|
95
|
+
raise device_rpc_failed(
|
|
96
|
+
"screenshot bodyBase64 exceeds size budget",
|
|
97
|
+
{
|
|
98
|
+
"field": field_name,
|
|
99
|
+
"reason": "screenshot_base64_too_large",
|
|
100
|
+
"maxChars": SCREENSHOT_MAX_BASE64_CHARS,
|
|
101
|
+
},
|
|
102
|
+
retryable=False,
|
|
103
|
+
)
|
|
104
|
+
estimated_size = _estimated_base64_decoded_size(value)
|
|
105
|
+
if estimated_size > SCREENSHOT_MAX_BINARY_BYTES:
|
|
106
|
+
raise device_rpc_failed(
|
|
107
|
+
"screenshot decoded body exceeds size budget",
|
|
108
|
+
{
|
|
109
|
+
"field": field_name,
|
|
110
|
+
"reason": "screenshot_decoded_too_large",
|
|
111
|
+
"maxBytes": SCREENSHOT_MAX_BINARY_BYTES,
|
|
112
|
+
},
|
|
113
|
+
retryable=False,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def decode_screenshot_body_base64(value: str, *, field_name: str) -> bytes:
|
|
118
|
+
validate_screenshot_body_base64_budget(value, field_name=field_name)
|
|
119
|
+
try:
|
|
120
|
+
decoded = base64.b64decode(value, validate=True)
|
|
121
|
+
except (binascii.Error, ValueError) as error:
|
|
122
|
+
raise device_rpc_failed(
|
|
123
|
+
"screenshot bodyBase64 must be valid base64",
|
|
124
|
+
{
|
|
125
|
+
"field": field_name,
|
|
126
|
+
"reason": "invalid_base64",
|
|
127
|
+
},
|
|
128
|
+
retryable=False,
|
|
129
|
+
) from error
|
|
130
|
+
if len(decoded) > SCREENSHOT_MAX_BINARY_BYTES:
|
|
131
|
+
raise device_rpc_failed(
|
|
132
|
+
"screenshot decoded body exceeds size budget",
|
|
133
|
+
{
|
|
134
|
+
"field": field_name,
|
|
135
|
+
"reason": "screenshot_decoded_too_large",
|
|
136
|
+
"maxBytes": SCREENSHOT_MAX_BINARY_BYTES,
|
|
137
|
+
},
|
|
138
|
+
retryable=False,
|
|
139
|
+
)
|
|
140
|
+
return decoded
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def validate_screenshot_png_bytes(
|
|
144
|
+
value: bytes,
|
|
145
|
+
*,
|
|
146
|
+
field_name: str,
|
|
147
|
+
expected_width_px: int,
|
|
148
|
+
expected_height_px: int,
|
|
149
|
+
) -> None:
|
|
150
|
+
png_signature = b"\x89PNG\r\n\x1a\n"
|
|
151
|
+
minimum_ihdr_bytes = len(png_signature) + 4 + 4 + 13 + 4
|
|
152
|
+
if len(value) < minimum_ihdr_bytes or not value.startswith(png_signature):
|
|
153
|
+
_raise_invalid_png(field_name)
|
|
154
|
+
ihdr_length = int.from_bytes(value[8:12], byteorder="big")
|
|
155
|
+
ihdr_type = value[12:16]
|
|
156
|
+
if ihdr_length != 13 or ihdr_type != b"IHDR":
|
|
157
|
+
_raise_invalid_png(field_name)
|
|
158
|
+
width_px = int.from_bytes(value[16:20], byteorder="big")
|
|
159
|
+
height_px = int.from_bytes(value[20:24], byteorder="big")
|
|
160
|
+
if width_px <= 0 or height_px <= 0:
|
|
161
|
+
_raise_invalid_png(field_name)
|
|
162
|
+
if width_px * height_px > SCREENSHOT_MAX_OUTPUT_PIXELS:
|
|
163
|
+
_raise_screenshot_dimensions_too_large(field_name)
|
|
164
|
+
if width_px != expected_width_px or height_px != expected_height_px:
|
|
165
|
+
raise device_rpc_failed(
|
|
166
|
+
"screenshot PNG IHDR dimensions must match typed metadata",
|
|
167
|
+
{
|
|
168
|
+
"field": field_name,
|
|
169
|
+
"reason": "screenshot_dimensions_mismatch",
|
|
170
|
+
"expectedWidthPx": expected_width_px,
|
|
171
|
+
"expectedHeightPx": expected_height_px,
|
|
172
|
+
"actualWidthPx": width_px,
|
|
173
|
+
"actualHeightPx": height_px,
|
|
174
|
+
},
|
|
175
|
+
retryable=False,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _raise_invalid_png(field_name: str) -> None:
|
|
180
|
+
raise device_rpc_failed(
|
|
181
|
+
"screenshot decoded body must be a PNG with IHDR",
|
|
182
|
+
{
|
|
183
|
+
"field": field_name,
|
|
184
|
+
"reason": "invalid_png",
|
|
185
|
+
},
|
|
186
|
+
retryable=False,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _raise_screenshot_dimensions_too_large(field_name: str) -> None:
|
|
191
|
+
raise device_rpc_failed(
|
|
192
|
+
"screenshot dimensions exceed pixel budget",
|
|
193
|
+
{
|
|
194
|
+
"field": field_name,
|
|
195
|
+
"reason": "screenshot_dimensions_too_large",
|
|
196
|
+
"maxPixels": SCREENSHOT_MAX_OUTPUT_PIXELS,
|
|
197
|
+
},
|
|
198
|
+
retryable=False,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _validate_payload(
|
|
203
|
+
model_type: type[ModelT],
|
|
204
|
+
payload: object,
|
|
205
|
+
*,
|
|
206
|
+
field_name: str,
|
|
207
|
+
) -> ModelT:
|
|
208
|
+
try:
|
|
209
|
+
return model_type.model_validate(payload)
|
|
210
|
+
except ValidationError as error:
|
|
211
|
+
raise validation_error_to_device_bootstrap_error(
|
|
212
|
+
error,
|
|
213
|
+
field_name=field_name,
|
|
214
|
+
retryable=False,
|
|
215
|
+
) from error
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _adapt_payload(
|
|
219
|
+
adapter: Callable[[ModelT], ResultT],
|
|
220
|
+
payload: ModelT,
|
|
221
|
+
) -> ResultT:
|
|
222
|
+
try:
|
|
223
|
+
return adapter(payload)
|
|
224
|
+
except SchemaDecodeError as error:
|
|
225
|
+
raise invalid_device_payload(error.field, error.problem) from error
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _validate_action_perform_result_payload(
|
|
229
|
+
payload: object,
|
|
230
|
+
*,
|
|
231
|
+
field_name: str,
|
|
232
|
+
) -> ActionPerformResultPayload:
|
|
233
|
+
try:
|
|
234
|
+
return ActionPerformResultPayload.model_validate(payload)
|
|
235
|
+
except ValidationError as error:
|
|
236
|
+
schema_error = _translate_action_perform_result_payload_error(
|
|
237
|
+
error,
|
|
238
|
+
field_name=field_name,
|
|
239
|
+
)
|
|
240
|
+
raise invalid_device_payload(
|
|
241
|
+
schema_error.field,
|
|
242
|
+
schema_error.problem,
|
|
243
|
+
) from error
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _translate_action_perform_result_payload_error(
|
|
247
|
+
error: ValidationError,
|
|
248
|
+
*,
|
|
249
|
+
field_name: str,
|
|
250
|
+
) -> SchemaDecodeError:
|
|
251
|
+
first_error = error.errors()[0]
|
|
252
|
+
error_location = tuple(first_error["loc"])
|
|
253
|
+
if str(first_error["type"]) in {"literal_error", "union_tag_invalid"} and (
|
|
254
|
+
error_location
|
|
255
|
+
in {
|
|
256
|
+
("resolvedTarget",),
|
|
257
|
+
("resolved_target",),
|
|
258
|
+
("resolvedTarget", "kind"),
|
|
259
|
+
("resolved_target", "kind"),
|
|
260
|
+
}
|
|
261
|
+
):
|
|
262
|
+
return SchemaDecodeError(
|
|
263
|
+
f"{field_name}.resolvedTarget.kind",
|
|
264
|
+
"must be one of handle|coordinates|none",
|
|
265
|
+
)
|
|
266
|
+
schema_error = validation_error_to_schema_decode_error(error, field_name=field_name)
|
|
267
|
+
return SchemaDecodeError(
|
|
268
|
+
_normalize_action_result_field(schema_error.field),
|
|
269
|
+
schema_error.problem,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _normalize_action_result_field(field: str) -> str:
|
|
274
|
+
if field.endswith(".resolvedTarget.handle.handle"):
|
|
275
|
+
return field.removesuffix(".handle")
|
|
276
|
+
return field.replace(
|
|
277
|
+
".resolvedTarget.handle.handle.",
|
|
278
|
+
".resolvedTarget.handle.",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def invalid_device_payload(field_name: str, problem: str) -> DeviceBootstrapError:
|
|
283
|
+
return device_rpc_failed(
|
|
284
|
+
f"device RPC {field_name} {problem}",
|
|
285
|
+
{
|
|
286
|
+
"field": field_name,
|
|
287
|
+
"reason": "invalid_payload",
|
|
288
|
+
},
|
|
289
|
+
retryable=False,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _validate_screenshot_capture_payload(
|
|
294
|
+
payload: ScreenshotCaptureResultPayload,
|
|
295
|
+
*,
|
|
296
|
+
field_name: str,
|
|
297
|
+
) -> None:
|
|
298
|
+
if payload.content_type != "image/png":
|
|
299
|
+
raise device_rpc_failed(
|
|
300
|
+
"typed screenshot.capture result must be image/png",
|
|
301
|
+
{
|
|
302
|
+
"field": f"{field_name}.contentType",
|
|
303
|
+
"reason": "unsupported_content_type",
|
|
304
|
+
"contentType": payload.content_type,
|
|
305
|
+
"expected": "image/png",
|
|
306
|
+
},
|
|
307
|
+
retryable=False,
|
|
308
|
+
)
|
|
309
|
+
if payload.width_px * payload.height_px > SCREENSHOT_MAX_OUTPUT_PIXELS:
|
|
310
|
+
_raise_screenshot_dimensions_too_large(field_name)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _estimated_base64_decoded_size(value: str) -> int:
|
|
314
|
+
if not value:
|
|
315
|
+
return 0
|
|
316
|
+
padding = 0
|
|
317
|
+
if len(value) % 4 == 0:
|
|
318
|
+
padding = len(value) - len(value.rstrip("="))
|
|
319
|
+
padding = min(padding, 2)
|
|
320
|
+
return ((len(value) + 3) // 4) * 3 - padding
|