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,254 @@
1
+ """Runtime command models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import TypeAlias, TypeGuard
8
+
9
+ from androidctld.commands.open_targets import OpenAppTarget, OpenUrlTarget
10
+ from androidctld.device.types import ConnectionConfig
11
+ from androidctld.protocol import CommandKind
12
+
13
+
14
+ class WaitKind(str, Enum):
15
+ TEXT = "text"
16
+ SCREEN_CHANGE = "screen-change"
17
+ GONE = "gone"
18
+ APP = "app"
19
+ IDLE = "idle"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class ConnectCommand:
24
+ connection: ConnectionConfig
25
+ kind: CommandKind = field(default=CommandKind.CONNECT, init=False)
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class ObserveCommand:
30
+ kind: CommandKind = field(default=CommandKind.OBSERVE, init=False)
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ScreenshotCommand:
35
+ kind: CommandKind = field(default=CommandKind.SCREENSHOT, init=False)
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class ListAppsCommand:
40
+ kind: CommandKind = field(default=CommandKind.LIST_APPS, init=False)
41
+
42
+
43
+ class _TypedActionCommandMixin:
44
+ kind: CommandKind
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class OpenCommand(_TypedActionCommandMixin):
49
+ target: OpenAppTarget | OpenUrlTarget
50
+ kind: CommandKind = field(default=CommandKind.OPEN, init=False)
51
+
52
+ def __post_init__(self) -> None:
53
+ if not isinstance(self.target, (OpenAppTarget, OpenUrlTarget)):
54
+ raise ValueError("open requires typed target")
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class TapCommand(_TypedActionCommandMixin):
59
+ ref: str
60
+ source_screen_id: str
61
+ kind: CommandKind = field(default=CommandKind.TAP, init=False)
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class LongTapCommand(_TypedActionCommandMixin):
66
+ ref: str
67
+ source_screen_id: str
68
+ kind: CommandKind = field(default=CommandKind.LONG_TAP, init=False)
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class TypeCommand(_TypedActionCommandMixin):
73
+ ref: str
74
+ source_screen_id: str
75
+ text: str
76
+ kind: CommandKind = field(default=CommandKind.TYPE, init=False)
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class FocusCommand(_TypedActionCommandMixin):
81
+ ref: str
82
+ source_screen_id: str
83
+ kind: CommandKind = field(default=CommandKind.FOCUS, init=False)
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class SubmitCommand(_TypedActionCommandMixin):
88
+ ref: str
89
+ source_screen_id: str
90
+ kind: CommandKind = field(default=CommandKind.SUBMIT, init=False)
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class ScrollCommand(_TypedActionCommandMixin):
95
+ ref: str
96
+ source_screen_id: str
97
+ direction: str
98
+ kind: CommandKind = field(default=CommandKind.SCROLL, init=False)
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class GlobalCommand(_TypedActionCommandMixin):
103
+ action: str
104
+ source_screen_id: str | None = None
105
+ kind: CommandKind = field(default=CommandKind.GLOBAL, init=False)
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class TextWaitPredicate:
110
+ text: str
111
+ wait_kind: WaitKind = field(default=WaitKind.TEXT, init=False)
112
+
113
+
114
+ @dataclass(frozen=True, slots=True)
115
+ class ScreenChangeWaitPredicate:
116
+ source_screen_id: str
117
+ wait_kind: WaitKind = field(default=WaitKind.SCREEN_CHANGE, init=False)
118
+
119
+
120
+ @dataclass(frozen=True, slots=True)
121
+ class GoneWaitPredicate:
122
+ source_screen_id: str
123
+ ref: str
124
+ wait_kind: WaitKind = field(default=WaitKind.GONE, init=False)
125
+
126
+
127
+ @dataclass(frozen=True, slots=True)
128
+ class AppWaitPredicate:
129
+ package_name: str
130
+ wait_kind: WaitKind = field(default=WaitKind.APP, init=False)
131
+
132
+
133
+ @dataclass(frozen=True, slots=True)
134
+ class IdleWaitPredicate:
135
+ wait_kind: WaitKind = field(default=WaitKind.IDLE, init=False)
136
+
137
+
138
+ WaitPredicate: TypeAlias = (
139
+ TextWaitPredicate
140
+ | ScreenChangeWaitPredicate
141
+ | GoneWaitPredicate
142
+ | AppWaitPredicate
143
+ | IdleWaitPredicate
144
+ )
145
+
146
+ WAIT_PREDICATE_TYPES = (
147
+ TextWaitPredicate,
148
+ ScreenChangeWaitPredicate,
149
+ GoneWaitPredicate,
150
+ AppWaitPredicate,
151
+ IdleWaitPredicate,
152
+ )
153
+
154
+
155
+ @dataclass(frozen=True)
156
+ class WaitCommand:
157
+ predicate: WaitPredicate
158
+ timeout_ms: int | None = None
159
+ kind: CommandKind = field(default=CommandKind.WAIT, init=False)
160
+
161
+ def __post_init__(self) -> None:
162
+ if not is_wait_predicate(self.predicate):
163
+ raise ValueError("wait requires typed predicate")
164
+
165
+ @property
166
+ def wait_kind(self) -> WaitKind:
167
+ return self.predicate.wait_kind
168
+
169
+
170
+ TypedActionCommand: TypeAlias = (
171
+ OpenCommand
172
+ | TapCommand
173
+ | LongTapCommand
174
+ | TypeCommand
175
+ | FocusCommand
176
+ | SubmitCommand
177
+ | ScrollCommand
178
+ | GlobalCommand
179
+ )
180
+
181
+ ActionCommand: TypeAlias = TypedActionCommand
182
+
183
+ REF_BOUND_ACTION_COMMAND_TYPES = (
184
+ TapCommand,
185
+ LongTapCommand,
186
+ TypeCommand,
187
+ FocusCommand,
188
+ SubmitCommand,
189
+ ScrollCommand,
190
+ )
191
+
192
+ RefBoundActionCommand: TypeAlias = (
193
+ TapCommand
194
+ | LongTapCommand
195
+ | TypeCommand
196
+ | FocusCommand
197
+ | SubmitCommand
198
+ | ScrollCommand
199
+ )
200
+
201
+ InternalCommand: TypeAlias = (
202
+ ConnectCommand
203
+ | ObserveCommand
204
+ | ListAppsCommand
205
+ | ActionCommand
206
+ | WaitCommand
207
+ | ScreenshotCommand
208
+ )
209
+
210
+
211
+ def wait_timeout_ms(command: WaitCommand) -> int | None:
212
+ return command.timeout_ms
213
+
214
+
215
+ def is_wait_predicate(value: object) -> TypeGuard[WaitPredicate]:
216
+ return isinstance(value, WAIT_PREDICATE_TYPES)
217
+
218
+
219
+ def is_ref_bound_action_command(command: object) -> TypeGuard[RefBoundActionCommand]:
220
+ return isinstance(command, REF_BOUND_ACTION_COMMAND_TYPES)
221
+
222
+
223
+ __all__ = [
224
+ "REF_BOUND_ACTION_COMMAND_TYPES",
225
+ "ActionCommand",
226
+ "AppWaitPredicate",
227
+ "ConnectCommand",
228
+ "FocusCommand",
229
+ "GlobalCommand",
230
+ "GoneWaitPredicate",
231
+ "IdleWaitPredicate",
232
+ "InternalCommand",
233
+ "ListAppsCommand",
234
+ "LongTapCommand",
235
+ "ObserveCommand",
236
+ "OpenAppTarget",
237
+ "OpenCommand",
238
+ "OpenUrlTarget",
239
+ "RefBoundActionCommand",
240
+ "ScreenChangeWaitPredicate",
241
+ "ScreenshotCommand",
242
+ "ScrollCommand",
243
+ "SubmitCommand",
244
+ "TapCommand",
245
+ "TextWaitPredicate",
246
+ "TypeCommand",
247
+ "TypedActionCommand",
248
+ "WaitCommand",
249
+ "WaitKind",
250
+ "WaitPredicate",
251
+ "is_ref_bound_action_command",
252
+ "is_wait_predicate",
253
+ "wait_timeout_ms",
254
+ ]
@@ -0,0 +1,99 @@
1
+ """Per-command semantic boundary dispatch helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import cast
6
+
7
+ from androidctld.commands.command_models import (
8
+ ConnectCommand,
9
+ GlobalCommand,
10
+ InternalCommand,
11
+ ListAppsCommand,
12
+ ObserveCommand,
13
+ OpenCommand,
14
+ ScreenshotCommand,
15
+ WaitCommand,
16
+ is_ref_bound_action_command,
17
+ )
18
+ from androidctld.commands.executor import CommandHandler, CommandResult
19
+ from androidctld.commands.handlers.action import ActionCommandHandler
20
+ from androidctld.commands.handlers.connect import ConnectCommandHandler
21
+ from androidctld.commands.handlers.list_apps import ListAppsCommandHandler
22
+ from androidctld.commands.handlers.observe import ObserveCommandHandler
23
+ from androidctld.commands.handlers.screenshot import ScreenshotCommandHandler
24
+ from androidctld.commands.handlers.wait import WaitCommandHandler
25
+ from androidctld.commands.registry import COMMAND_SPECS
26
+
27
+ __all__ = ["CommandDispatch"]
28
+
29
+
30
+ class CommandDispatch:
31
+ def __init__(
32
+ self,
33
+ *,
34
+ connect_handler: ConnectCommandHandler,
35
+ observe_handler: ObserveCommandHandler,
36
+ list_apps_handler: ListAppsCommandHandler,
37
+ action_handler: ActionCommandHandler,
38
+ wait_handler: WaitCommandHandler,
39
+ screenshot_handler: ScreenshotCommandHandler,
40
+ ) -> None:
41
+ self._connect_handler = connect_handler
42
+ self._observe_handler = observe_handler
43
+ self._list_apps_handler = list_apps_handler
44
+ self._action_handler = action_handler
45
+ self._wait_handler = wait_handler
46
+ self._screenshot_handler = screenshot_handler
47
+
48
+ def build_handlers(self) -> dict[str, CommandHandler]:
49
+ return {
50
+ spec.daemon_kind: cast(
51
+ CommandHandler,
52
+ getattr(self, spec.dispatch_method_name),
53
+ )
54
+ for spec in COMMAND_SPECS.values()
55
+ }
56
+
57
+ def execute_connect(self, *, command: InternalCommand) -> CommandResult:
58
+ if not isinstance(command, ConnectCommand):
59
+ raise TypeError(f"connect handler received {command.kind!r} command")
60
+ return self._connect_handler.handle(command=command)
61
+
62
+ def execute_observe(self, *, command: InternalCommand) -> CommandResult:
63
+ if not isinstance(command, ObserveCommand):
64
+ raise TypeError(f"observe handler received {command.kind!r} command")
65
+ return self._observe_handler.handle(command=command)
66
+
67
+ def execute_list_apps(self, *, command: InternalCommand) -> CommandResult:
68
+ if not isinstance(command, ListAppsCommand):
69
+ raise TypeError(f"list-apps handler received {command.kind!r} command")
70
+ return self._list_apps_handler.handle(command=command)
71
+
72
+ def execute_open(self, *, command: InternalCommand) -> CommandResult:
73
+ if not isinstance(command, OpenCommand):
74
+ raise TypeError(f"open handler received {command.kind!r} command")
75
+ return self._action_handler.handle_open(command=command)
76
+
77
+ def execute_ref_action(self, *, command: InternalCommand) -> CommandResult:
78
+ if not is_ref_bound_action_command(command):
79
+ raise TypeError(f"ref action handler received {command.kind!r} command")
80
+ return self._action_handler.handle_ref_action(command=command)
81
+
82
+ def execute_global_action(
83
+ self,
84
+ *,
85
+ command: InternalCommand,
86
+ ) -> CommandResult:
87
+ if not isinstance(command, GlobalCommand):
88
+ raise TypeError(f"global action handler received {command.kind!r} command")
89
+ return self._action_handler.handle_global_action(command=command)
90
+
91
+ def execute_wait(self, *, command: InternalCommand) -> CommandResult:
92
+ if isinstance(command, WaitCommand):
93
+ return self._wait_handler.handle_service_wait(command=command)
94
+ raise TypeError(f"wait handler received {command.kind!r} command")
95
+
96
+ def execute_screenshot(self, *, command: InternalCommand) -> CommandResult:
97
+ if not isinstance(command, ScreenshotCommand):
98
+ raise TypeError(f"screenshot handler received {command.kind!r} command")
99
+ return self._screenshot_handler.handle(command=command)
@@ -0,0 +1,31 @@
1
+ """Thin semantic command executor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Protocol
6
+
7
+ from androidctld.commands.command_models import InternalCommand
8
+ from androidctld.commands.registry import resolve_command_spec
9
+
10
+ CommandResult = dict[str, object]
11
+
12
+
13
+ class CommandHandler(Protocol):
14
+ def __call__(self, *, command: InternalCommand) -> CommandResult: ...
15
+
16
+
17
+ class CommandExecutor:
18
+ def __init__(self, *, handlers: dict[str, CommandHandler]) -> None:
19
+ self._handlers = handlers
20
+
21
+ def run(
22
+ self,
23
+ *,
24
+ command: InternalCommand,
25
+ ) -> CommandResult:
26
+ spec = resolve_command_spec(command)
27
+ handler = self._handlers[spec.command_name]
28
+ return handler(command=command)
29
+
30
+
31
+ __all__ = ["CommandExecutor", "CommandHandler", "CommandResult"]
@@ -0,0 +1,175 @@
1
+ """Pure compilers from validated shared wire payloads to executable commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from androidctl_contracts import daemon_api as wire_api
6
+ from androidctld.commands.command_models import (
7
+ AppWaitPredicate,
8
+ ConnectCommand,
9
+ FocusCommand,
10
+ GlobalCommand,
11
+ GoneWaitPredicate,
12
+ IdleWaitPredicate,
13
+ ListAppsCommand,
14
+ LongTapCommand,
15
+ ObserveCommand,
16
+ OpenAppTarget,
17
+ OpenCommand,
18
+ OpenUrlTarget,
19
+ RefBoundActionCommand,
20
+ ScreenChangeWaitPredicate,
21
+ ScreenshotCommand,
22
+ ScrollCommand,
23
+ SubmitCommand,
24
+ TapCommand,
25
+ TextWaitPredicate,
26
+ TypeCommand,
27
+ WaitCommand,
28
+ )
29
+ from androidctld.device.types import ConnectionConfig
30
+ from androidctld.protocol import ConnectionMode
31
+
32
+
33
+ def compile_connect_command(payload: wire_api.ConnectCommandPayload) -> ConnectCommand:
34
+ connection = payload.connection
35
+ mode = ConnectionMode(connection.mode)
36
+ if mode is ConnectionMode.LAN:
37
+ if connection.host is None or connection.port is None:
38
+ raise TypeError("lan connect wire payload requires host and port")
39
+ return ConnectCommand(
40
+ connection=ConnectionConfig(
41
+ mode=mode,
42
+ token=connection.token,
43
+ host=connection.host,
44
+ port=connection.port,
45
+ )
46
+ )
47
+ return ConnectCommand(
48
+ connection=ConnectionConfig(
49
+ mode=mode,
50
+ token=connection.token,
51
+ serial=connection.serial,
52
+ )
53
+ )
54
+
55
+
56
+ def compile_observe_command(payload: wire_api.ObserveCommandPayload) -> ObserveCommand:
57
+ del payload
58
+ return ObserveCommand()
59
+
60
+
61
+ def compile_list_apps_command(
62
+ payload: wire_api.ListAppsCommandPayload,
63
+ ) -> ListAppsCommand:
64
+ del payload
65
+ return ListAppsCommand()
66
+
67
+
68
+ def compile_open_command(payload: wire_api.OpenCommandPayload) -> OpenCommand:
69
+ target = payload.target
70
+ if isinstance(target, wire_api.OpenAppTargetPayload):
71
+ return OpenCommand(target=OpenAppTarget(package_name=target.value))
72
+ if isinstance(target, wire_api.OpenUrlTargetPayload):
73
+ return OpenCommand(target=OpenUrlTarget(url=target.value))
74
+ raise TypeError(f"unsupported open target payload: {type(target)!r}")
75
+
76
+
77
+ def compile_ref_action_command(
78
+ payload: (
79
+ wire_api.RefActionCommandPayload
80
+ | wire_api.TypeCommandPayload
81
+ | wire_api.ScrollCommandPayload
82
+ ),
83
+ ) -> RefBoundActionCommand:
84
+ if isinstance(payload, wire_api.TypeCommandPayload):
85
+ return TypeCommand(
86
+ ref=payload.ref,
87
+ source_screen_id=payload.source_screen_id,
88
+ text=payload.text,
89
+ )
90
+ if isinstance(payload, wire_api.ScrollCommandPayload):
91
+ return ScrollCommand(
92
+ ref=payload.ref,
93
+ source_screen_id=payload.source_screen_id,
94
+ direction=payload.direction,
95
+ )
96
+ if payload.kind == "tap":
97
+ return TapCommand(ref=payload.ref, source_screen_id=payload.source_screen_id)
98
+ if payload.kind == "longTap":
99
+ return LongTapCommand(
100
+ ref=payload.ref,
101
+ source_screen_id=payload.source_screen_id,
102
+ )
103
+ if payload.kind == "focus":
104
+ return FocusCommand(ref=payload.ref, source_screen_id=payload.source_screen_id)
105
+ if payload.kind == "submit":
106
+ return SubmitCommand(
107
+ ref=payload.ref,
108
+ source_screen_id=payload.source_screen_id,
109
+ )
110
+ raise TypeError(f"unsupported ref action payload kind: {payload.kind!r}")
111
+
112
+
113
+ def compile_global_action_command(
114
+ payload: wire_api.GlobalActionCommandPayload,
115
+ ) -> GlobalCommand:
116
+ return GlobalCommand(
117
+ action=payload.kind,
118
+ source_screen_id=payload.source_screen_id,
119
+ )
120
+
121
+
122
+ def compile_service_wait_command(
123
+ payload: wire_api.WaitCommandPayload,
124
+ ) -> WaitCommand:
125
+ predicate = payload.predicate
126
+ if isinstance(predicate, wire_api.TextPresentPredicatePayload):
127
+ return WaitCommand(
128
+ predicate=TextWaitPredicate(text=predicate.text),
129
+ timeout_ms=payload.timeout_ms,
130
+ )
131
+ if isinstance(predicate, wire_api.ScreenChangePredicatePayload):
132
+ return WaitCommand(
133
+ predicate=ScreenChangeWaitPredicate(
134
+ source_screen_id=predicate.source_screen_id,
135
+ ),
136
+ timeout_ms=payload.timeout_ms,
137
+ )
138
+ if isinstance(predicate, wire_api.GonePredicatePayload):
139
+ return WaitCommand(
140
+ predicate=GoneWaitPredicate(
141
+ source_screen_id=predicate.source_screen_id,
142
+ ref=predicate.ref,
143
+ ),
144
+ timeout_ms=payload.timeout_ms,
145
+ )
146
+ if isinstance(predicate, wire_api.AppPredicatePayload):
147
+ return WaitCommand(
148
+ predicate=AppWaitPredicate(package_name=predicate.package_name),
149
+ timeout_ms=payload.timeout_ms,
150
+ )
151
+ if isinstance(predicate, wire_api.IdlePredicatePayload):
152
+ return WaitCommand(
153
+ predicate=IdleWaitPredicate(),
154
+ timeout_ms=payload.timeout_ms,
155
+ )
156
+ raise TypeError(f"unsupported wait predicate payload: {type(predicate)!r}")
157
+
158
+
159
+ def compile_screenshot_command(
160
+ payload: wire_api.ScreenshotCommandPayload,
161
+ ) -> ScreenshotCommand:
162
+ del payload
163
+ return ScreenshotCommand()
164
+
165
+
166
+ __all__ = [
167
+ "compile_connect_command",
168
+ "compile_global_action_command",
169
+ "compile_list_apps_command",
170
+ "compile_observe_command",
171
+ "compile_open_command",
172
+ "compile_ref_action_command",
173
+ "compile_screenshot_command",
174
+ "compile_service_wait_command",
175
+ ]
@@ -0,0 +1,15 @@
1
+ """Command handler modules."""
2
+
3
+ from androidctld.commands.handlers.action import ActionCommandHandler
4
+ from androidctld.commands.handlers.connect import ConnectCommandHandler
5
+ from androidctld.commands.handlers.observe import ObserveCommandHandler
6
+ from androidctld.commands.handlers.screenshot import ScreenshotCommandHandler
7
+ from androidctld.commands.handlers.wait import WaitCommandHandler
8
+
9
+ __all__ = [
10
+ "ActionCommandHandler",
11
+ "ConnectCommandHandler",
12
+ "ObserveCommandHandler",
13
+ "ScreenshotCommandHandler",
14
+ "WaitCommandHandler",
15
+ ]