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,305 @@
1
+ """Shared wait-loop orchestration for canonical wait commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from typing import TypeAlias
8
+
9
+ from androidctld.commands.command_models import WaitCommand, wait_timeout_ms
10
+ from androidctld.commands.models import CommandRecord
11
+ from androidctld.device.interfaces import (
12
+ DeviceClientFactory,
13
+ EventPollingClient,
14
+ )
15
+ from androidctld.observation import (
16
+ ObservationLoop,
17
+ ObservationPolicy,
18
+ ObservationPollOutcome,
19
+ )
20
+ from androidctld.protocol import CommandKind
21
+ from androidctld.runtime import RuntimeLifecycleLease
22
+ from androidctld.runtime.models import WorkspaceRuntime
23
+ from androidctld.runtime.screen_state import (
24
+ current_compiled_screen,
25
+ current_public_screen,
26
+ )
27
+ from androidctld.runtime_policy import (
28
+ DEVICE_RPC_REQUEST_ID_WAIT,
29
+ TRANSIENT_INVALID_SNAPSHOT_MAX_RETRIES,
30
+ WAIT_EVENT_POLL_SLICE_MS,
31
+ WAIT_IDLE_STABLE_WINDOW_MS,
32
+ WAIT_LOOP_SLEEP_SECONDS,
33
+ WAIT_SNAPSHOT_MAX_INTERVAL_MS,
34
+ default_wait_timeout_ms,
35
+ )
36
+ from androidctld.semantics.compiler import CompiledScreen
37
+ from androidctld.semantics.public_models import PublicScreen
38
+ from androidctld.snapshots.models import RawSnapshot
39
+ from androidctld.snapshots.refresh import ScreenRefreshService, raise_if_stale
40
+ from androidctld.snapshots.service import (
41
+ SnapshotService,
42
+ fetch_with_transient_invalid_snapshot_retry,
43
+ )
44
+ from androidctld.waits.evaluators import (
45
+ WaitEvaluationOutcome,
46
+ WaitIdleEvaluationState,
47
+ WaitIdleTracking,
48
+ WaitMatchData,
49
+ WaitMatched,
50
+ WaitReadyContext,
51
+ evaluate_ready_wait_match,
52
+ )
53
+
54
+ SleepFn = Callable[[float], None]
55
+ TimeFn = Callable[[], float]
56
+ WaitReadyState: TypeAlias = WaitReadyContext
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class WaitLoopTimedOut:
61
+ pass
62
+
63
+
64
+ WaitLoopOutcome: TypeAlias = WaitMatchData | WaitLoopTimedOut
65
+
66
+
67
+ def _wait_state(
68
+ session: WorkspaceRuntime,
69
+ ) -> tuple[
70
+ RawSnapshot | None,
71
+ PublicScreen | None,
72
+ CompiledScreen | None,
73
+ ]:
74
+ return (
75
+ session.latest_snapshot,
76
+ current_public_screen(session),
77
+ current_compiled_screen(session),
78
+ )
79
+
80
+
81
+ def _ready_wait_state(
82
+ snapshot: RawSnapshot | None,
83
+ public_screen: PublicScreen | None,
84
+ compiled_screen: CompiledScreen | None,
85
+ ) -> WaitReadyState | None:
86
+ if snapshot is None or public_screen is None:
87
+ return None
88
+ return WaitReadyContext(
89
+ snapshot=snapshot,
90
+ public_screen=public_screen,
91
+ compiled_screen=compiled_screen,
92
+ )
93
+
94
+
95
+ def _cached_ready_wait_state(
96
+ session: WorkspaceRuntime,
97
+ *,
98
+ lifecycle_lease: RuntimeLifecycleLease,
99
+ command: WaitCommand,
100
+ kind: CommandKind,
101
+ record: CommandRecord,
102
+ ) -> WaitReadyState | None:
103
+ with session.lock:
104
+ ready_state = _ready_wait_state(*_wait_state(session))
105
+ if ready_state is None:
106
+ return None
107
+ raise_if_stale(
108
+ session,
109
+ lifecycle_lease,
110
+ kind=kind,
111
+ wait_kind=command.wait_kind,
112
+ record=record,
113
+ )
114
+ return ready_state
115
+
116
+
117
+ class WaitRuntimeLoop:
118
+ def __init__(
119
+ self,
120
+ *,
121
+ snapshot_service: SnapshotService,
122
+ screen_refresh: ScreenRefreshService,
123
+ device_client_factory: DeviceClientFactory,
124
+ sleep_fn: SleepFn,
125
+ time_fn: TimeFn,
126
+ ) -> None:
127
+ self._snapshot_service = snapshot_service
128
+ self._screen_refresh = screen_refresh
129
+ self._device_client_factory = device_client_factory
130
+ self._sleep_fn = sleep_fn
131
+ self._time_fn = time_fn
132
+
133
+ def run(
134
+ self,
135
+ *,
136
+ session: WorkspaceRuntime,
137
+ record: CommandRecord,
138
+ command: WaitCommand,
139
+ lifecycle_lease: RuntimeLifecycleLease,
140
+ ) -> WaitLoopOutcome:
141
+ kind = command.kind
142
+ timeout_override = wait_timeout_ms(command)
143
+ timeout_ms = (
144
+ timeout_override
145
+ if timeout_override is not None
146
+ else default_wait_timeout_ms(command.wait_kind)
147
+ )
148
+ observation = ObservationLoop.begin(
149
+ ObservationPolicy(
150
+ min_grace_ms=0,
151
+ snapshot_max_interval_ms=WAIT_SNAPSHOT_MAX_INTERVAL_MS,
152
+ stable_window_ms=WAIT_IDLE_STABLE_WINDOW_MS,
153
+ max_total_ms=timeout_ms,
154
+ poll_slice_ms=WAIT_EVENT_POLL_SLICE_MS,
155
+ ),
156
+ started_at=self._time_fn(),
157
+ )
158
+ idle_state = WaitIdleEvaluationState()
159
+
160
+ initial_state = _cached_ready_wait_state(
161
+ session,
162
+ lifecycle_lease=lifecycle_lease,
163
+ command=command,
164
+ kind=kind,
165
+ record=record,
166
+ )
167
+ if initial_state is not None:
168
+ outcome = self._evaluate_ready_state(
169
+ command=command,
170
+ ready_state=initial_state,
171
+ idle_state=idle_state,
172
+ now=observation.started_at,
173
+ )
174
+ if isinstance(outcome, WaitMatched):
175
+ return outcome.match
176
+ if isinstance(outcome, WaitIdleTracking):
177
+ idle_state = outcome.idle_state
178
+
179
+ client = self._device_client_factory(
180
+ session,
181
+ lifecycle_lease=lifecycle_lease,
182
+ )
183
+ while True:
184
+ poll_outcome = self._poll(observation, client)
185
+
186
+ current_state = self._current_ready_state(
187
+ session=session,
188
+ observation=observation,
189
+ poll_outcome=poll_outcome,
190
+ lifecycle_lease=lifecycle_lease,
191
+ command=command,
192
+ kind=kind,
193
+ record=record,
194
+ )
195
+ if current_state is None:
196
+ if observation.timed_out(self._time_fn()):
197
+ return WaitLoopTimedOut()
198
+ self._sleep_fn(WAIT_LOOP_SLEEP_SECONDS)
199
+ continue
200
+
201
+ outcome = self._evaluate_ready_state(
202
+ command=command,
203
+ ready_state=current_state,
204
+ idle_state=idle_state,
205
+ now=self._time_fn(),
206
+ )
207
+ if isinstance(outcome, WaitMatched):
208
+ return outcome.match
209
+ if isinstance(outcome, WaitIdleTracking):
210
+ idle_state = outcome.idle_state
211
+ if observation.timed_out(self._time_fn()):
212
+ return WaitLoopTimedOut()
213
+ self._sleep_fn(WAIT_LOOP_SLEEP_SECONDS)
214
+
215
+ def _poll(
216
+ self,
217
+ observation: ObservationLoop,
218
+ client: EventPollingClient,
219
+ ) -> ObservationPollOutcome:
220
+ poll_wait_ms = observation.poll_wait_ms(self._time_fn())
221
+ poll_outcome = ObservationPollOutcome(
222
+ saw_events=False,
223
+ need_resync=False,
224
+ latest_seq=observation.after_seq,
225
+ )
226
+ if poll_wait_ms > 0:
227
+ polled_at = self._time_fn()
228
+ poll_result = client.events_poll(
229
+ after_seq=observation.after_seq,
230
+ wait_ms=poll_wait_ms,
231
+ limit=1,
232
+ request_id=DEVICE_RPC_REQUEST_ID_WAIT,
233
+ )
234
+ poll_outcome = observation.apply_poll_result(poll_result)
235
+ if not poll_outcome.saw_events and self._time_fn() <= polled_at:
236
+ self._sleep_fn(poll_wait_ms / 1000.0)
237
+ return poll_outcome
238
+
239
+ def _current_ready_state(
240
+ self,
241
+ *,
242
+ session: WorkspaceRuntime,
243
+ observation: ObservationLoop,
244
+ poll_outcome: ObservationPollOutcome,
245
+ lifecycle_lease: RuntimeLifecycleLease,
246
+ command: WaitCommand,
247
+ kind: CommandKind,
248
+ record: CommandRecord,
249
+ ) -> WaitReadyState | None:
250
+ if observation.should_refresh(
251
+ self._time_fn(),
252
+ saw_events=poll_outcome.saw_events,
253
+ need_resync=poll_outcome.need_resync,
254
+ ):
255
+ snapshot = fetch_with_transient_invalid_snapshot_retry(
256
+ self._snapshot_service,
257
+ session=session,
258
+ force_refresh=True,
259
+ lifecycle_lease=lifecycle_lease,
260
+ deadline_at=observation.deadline_at,
261
+ max_retries=TRANSIENT_INVALID_SNAPSHOT_MAX_RETRIES,
262
+ sleep_fn=self._sleep_fn,
263
+ time_fn=self._time_fn,
264
+ )
265
+ observation.mark_refreshed(self._time_fn())
266
+ self._screen_refresh.refresh(
267
+ session,
268
+ snapshot,
269
+ lifecycle_lease=lifecycle_lease,
270
+ command_kind=kind,
271
+ wait_kind=command.wait_kind,
272
+ record=record,
273
+ )
274
+ return _cached_ready_wait_state(
275
+ session,
276
+ lifecycle_lease=lifecycle_lease,
277
+ command=command,
278
+ kind=kind,
279
+ record=record,
280
+ )
281
+
282
+ def _evaluate_ready_state(
283
+ self,
284
+ *,
285
+ command: WaitCommand,
286
+ ready_state: WaitReadyState,
287
+ idle_state: WaitIdleEvaluationState,
288
+ now: float,
289
+ ) -> WaitEvaluationOutcome:
290
+ return evaluate_ready_wait_match(
291
+ command=command,
292
+ ready=ready_state,
293
+ idle_state=idle_state,
294
+ now=now,
295
+ )
296
+
297
+
298
+ __all__ = [
299
+ "SleepFn",
300
+ "TimeFn",
301
+ "WaitLoopOutcome",
302
+ "WaitLoopTimedOut",
303
+ "WaitReadyState",
304
+ "WaitRuntimeLoop",
305
+ ]
@@ -0,0 +1,41 @@
1
+ """Text matching helpers for wait commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from androidctld.snapshots.models import RawSnapshot
8
+ from androidctld.text_equivalence import canonical_text_key, searchable_raw_node_texts
9
+
10
+
11
+ def matches_text(
12
+ snapshot: RawSnapshot,
13
+ query: str,
14
+ ) -> bool:
15
+ normalized_query = normalize_wait_match_text(query)
16
+ if not normalized_query:
17
+ return False
18
+ seen_candidates: set[str] = set()
19
+ for value in text_candidates(snapshot):
20
+ candidate = normalize_wait_match_text(value)
21
+ if not candidate or candidate in seen_candidates:
22
+ continue
23
+ seen_candidates.add(candidate)
24
+ if normalized_query in candidate:
25
+ return True
26
+ return False
27
+
28
+
29
+ def normalize_wait_match_text(value: Any) -> str:
30
+ return canonical_text_key(value)
31
+
32
+
33
+ def text_candidates(
34
+ snapshot: RawSnapshot,
35
+ ) -> list[str]:
36
+ candidates: list[str] = []
37
+ for raw_node in snapshot.nodes:
38
+ if not raw_node.visible_to_user:
39
+ continue
40
+ candidates.extend(searchable_raw_node_texts(raw_node))
41
+ return candidates