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.
Files changed (187) hide show
  1. androidctl/__init__.py +5 -0
  2. androidctl/__main__.py +4 -0
  3. androidctl/_version.py +1 -0
  4. androidctl/app.py +73 -0
  5. androidctl/cli_options.py +27 -0
  6. androidctl/command_payloads.py +264 -0
  7. androidctl/command_views.py +157 -0
  8. androidctl/commands/__init__.py +1 -0
  9. androidctl/commands/actions.py +236 -0
  10. androidctl/commands/adb_wireless.py +157 -0
  11. androidctl/commands/close.py +30 -0
  12. androidctl/commands/connect.py +69 -0
  13. androidctl/commands/execute.py +179 -0
  14. androidctl/commands/list_apps.py +26 -0
  15. androidctl/commands/observe.py +26 -0
  16. androidctl/commands/open.py +41 -0
  17. androidctl/commands/plumbing.py +58 -0
  18. androidctl/commands/run_pipeline.py +307 -0
  19. androidctl/commands/screenshot.py +29 -0
  20. androidctl/commands/setup.py +301 -0
  21. androidctl/commands/wait.py +60 -0
  22. androidctl/daemon/__init__.py +1 -0
  23. androidctl/daemon/client.py +348 -0
  24. androidctl/daemon/discovery.py +190 -0
  25. androidctl/daemon/launcher.py +26 -0
  26. androidctl/daemon/owner.py +349 -0
  27. androidctl/errors/__init__.py +1 -0
  28. androidctl/errors/mapping.py +149 -0
  29. androidctl/errors/models.py +16 -0
  30. androidctl/exit_codes.py +8 -0
  31. androidctl/output.py +147 -0
  32. androidctl/parsing/__init__.py +1 -0
  33. androidctl/parsing/duration.py +17 -0
  34. androidctl/parsing/open_target.py +51 -0
  35. androidctl/parsing/refs.py +12 -0
  36. androidctl/parsing/screen_id.py +10 -0
  37. androidctl/parsing/wait.py +70 -0
  38. androidctl/renderers/__init__.py +110 -0
  39. androidctl/renderers/_paths.py +109 -0
  40. androidctl/renderers/xml.py +234 -0
  41. androidctl/renderers/xml_projection.py +732 -0
  42. androidctl/resources/__init__.py +1 -0
  43. androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
  44. androidctl/setup/__init__.py +1 -0
  45. androidctl/setup/accessibility.py +159 -0
  46. androidctl/setup/adb.py +586 -0
  47. androidctl/setup/apk_resource.py +29 -0
  48. androidctl/setup/pairing.py +70 -0
  49. androidctl/setup/verify.py +175 -0
  50. androidctl/workspace/__init__.py +3 -0
  51. androidctl/workspace/resolve.py +27 -0
  52. androidctl-0.1.0.dist-info/METADATA +217 -0
  53. androidctl-0.1.0.dist-info/RECORD +187 -0
  54. androidctl-0.1.0.dist-info/WHEEL +5 -0
  55. androidctl-0.1.0.dist-info/entry_points.txt +3 -0
  56. androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
  57. androidctl-0.1.0.dist-info/top_level.txt +3 -0
  58. androidctl_contracts/__init__.py +55 -0
  59. androidctl_contracts/_version.py +1 -0
  60. androidctl_contracts/_wire_helpers.py +31 -0
  61. androidctl_contracts/base.py +142 -0
  62. androidctl_contracts/command_catalog.py +414 -0
  63. androidctl_contracts/command_results.py +630 -0
  64. androidctl_contracts/daemon_api.py +335 -0
  65. androidctl_contracts/errors.py +44 -0
  66. androidctl_contracts/paths.py +5 -0
  67. androidctl_contracts/public_screen.py +579 -0
  68. androidctl_contracts/user_state.py +23 -0
  69. androidctl_contracts/vocabulary.py +82 -0
  70. androidctld/__init__.py +5 -0
  71. androidctld/__main__.py +63 -0
  72. androidctld/_version.py +1 -0
  73. androidctld/actions/__init__.py +1 -0
  74. androidctld/actions/action_target.py +142 -0
  75. androidctld/actions/capabilities.py +539 -0
  76. androidctld/actions/executor.py +894 -0
  77. androidctld/actions/focus_confirmation.py +177 -0
  78. androidctld/actions/focused_input_admissibility.py +120 -0
  79. androidctld/actions/fresh_current.py +176 -0
  80. androidctld/actions/postconditions.py +473 -0
  81. androidctld/actions/repair.py +101 -0
  82. androidctld/actions/request_builder.py +204 -0
  83. androidctld/actions/settle.py +146 -0
  84. androidctld/actions/submit_confirmation.py +211 -0
  85. androidctld/actions/submit_routing.py +311 -0
  86. androidctld/actions/type_confirmation.py +257 -0
  87. androidctld/app_targets.py +71 -0
  88. androidctld/artifacts/__init__.py +1 -0
  89. androidctld/artifacts/models.py +26 -0
  90. androidctld/artifacts/screen_lookup.py +241 -0
  91. androidctld/artifacts/screen_payloads.py +109 -0
  92. androidctld/artifacts/writer.py +286 -0
  93. androidctld/auth/__init__.py +1 -0
  94. androidctld/auth/active_registry.py +266 -0
  95. androidctld/auth/secret_files.py +52 -0
  96. androidctld/auth/token_store.py +59 -0
  97. androidctld/commands/__init__.py +1 -0
  98. androidctld/commands/assembly.py +231 -0
  99. androidctld/commands/command_models.py +254 -0
  100. androidctld/commands/dispatch.py +99 -0
  101. androidctld/commands/executor.py +31 -0
  102. androidctld/commands/from_boundary.py +175 -0
  103. androidctld/commands/handlers/__init__.py +15 -0
  104. androidctld/commands/handlers/action.py +439 -0
  105. androidctld/commands/handlers/connect.py +94 -0
  106. androidctld/commands/handlers/list_apps.py +215 -0
  107. androidctld/commands/handlers/observe.py +121 -0
  108. androidctld/commands/handlers/screenshot.py +105 -0
  109. androidctld/commands/handlers/wait.py +286 -0
  110. androidctld/commands/models.py +65 -0
  111. androidctld/commands/open_targets.py +56 -0
  112. androidctld/commands/orchestration.py +353 -0
  113. androidctld/commands/registry.py +116 -0
  114. androidctld/commands/result_builders.py +40 -0
  115. androidctld/commands/result_models.py +555 -0
  116. androidctld/commands/results.py +108 -0
  117. androidctld/commands/semantic_command_names.py +17 -0
  118. androidctld/commands/semantic_error_mapping.py +93 -0
  119. androidctld/commands/semantic_truth.py +135 -0
  120. androidctld/commands/service.py +67 -0
  121. androidctld/config.py +75 -0
  122. androidctld/daemon/__init__.py +1 -0
  123. androidctld/daemon/active_slot.py +326 -0
  124. androidctld/daemon/envelope.py +30 -0
  125. androidctld/daemon/http_host.py +123 -0
  126. androidctld/daemon/ingress.py +112 -0
  127. androidctld/daemon/ownership_probe.py +204 -0
  128. androidctld/daemon/server.py +286 -0
  129. androidctld/daemon/service.py +99 -0
  130. androidctld/device/__init__.py +1 -0
  131. androidctld/device/action_models.py +154 -0
  132. androidctld/device/action_serialization.py +121 -0
  133. androidctld/device/adapters.py +220 -0
  134. androidctld/device/bootstrap.py +153 -0
  135. androidctld/device/connectors.py +231 -0
  136. androidctld/device/errors.py +100 -0
  137. androidctld/device/interfaces.py +58 -0
  138. androidctld/device/parsing.py +320 -0
  139. androidctld/device/rpc.py +483 -0
  140. androidctld/device/schema.py +114 -0
  141. androidctld/device/types.py +161 -0
  142. androidctld/errors/__init__.py +94 -0
  143. androidctld/logging/__init__.py +22 -0
  144. androidctld/observation.py +98 -0
  145. androidctld/protocol.py +53 -0
  146. androidctld/refs/__init__.py +1 -0
  147. androidctld/refs/models.py +54 -0
  148. androidctld/refs/repair.py +284 -0
  149. androidctld/refs/service.py +422 -0
  150. androidctld/rendering/__init__.py +1 -0
  151. androidctld/rendering/screen_xml.py +256 -0
  152. androidctld/runtime/__init__.py +21 -0
  153. androidctld/runtime/kernel.py +548 -0
  154. androidctld/runtime/lifecycle.py +19 -0
  155. androidctld/runtime/models.py +48 -0
  156. androidctld/runtime/screen_state.py +117 -0
  157. androidctld/runtime/state_repo.py +70 -0
  158. androidctld/runtime/store.py +76 -0
  159. androidctld/runtime_policy.py +127 -0
  160. androidctld/schema/__init__.py +5 -0
  161. androidctld/schema/base.py +132 -0
  162. androidctld/schema/core.py +35 -0
  163. androidctld/schema/daemon_api.py +108 -0
  164. androidctld/schema/persistence.py +161 -0
  165. androidctld/schema/persistence_io.py +41 -0
  166. androidctld/schema/validation_errors.py +309 -0
  167. androidctld/semantics/__init__.py +1 -0
  168. androidctld/semantics/compiler.py +610 -0
  169. androidctld/semantics/continuity.py +107 -0
  170. androidctld/semantics/labels.py +252 -0
  171. androidctld/semantics/models.py +25 -0
  172. androidctld/semantics/policy.py +23 -0
  173. androidctld/semantics/public_models.py +123 -0
  174. androidctld/semantics/registries.py +13 -0
  175. androidctld/semantics/submit_refs.py +417 -0
  176. androidctld/semantics/surface.py +254 -0
  177. androidctld/semantics/targets.py +167 -0
  178. androidctld/snapshots/__init__.py +1 -0
  179. androidctld/snapshots/models.py +219 -0
  180. androidctld/snapshots/refresh.py +273 -0
  181. androidctld/snapshots/schema.py +74 -0
  182. androidctld/snapshots/service.py +138 -0
  183. androidctld/text_equivalence.py +67 -0
  184. androidctld/waits/__init__.py +1 -0
  185. androidctld/waits/evaluators.py +216 -0
  186. androidctld/waits/loop.py +305 -0
  187. 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