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,109 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ _WINDOWS_ABS_RE = re.compile(r"^[a-zA-Z]:/")
6
+
7
+
8
+ def normalize_public_path(
9
+ path: str | None,
10
+ *,
11
+ workspace_root: str | None,
12
+ artifact_root: str | None,
13
+ ) -> str | None:
14
+ if path is None:
15
+ return None
16
+ normalized_path = path.replace("\\", "/")
17
+ normalized_workspace = _normalize_slashes(workspace_root)
18
+ normalized_artifact = _normalize_slashes(artifact_root)
19
+ if (
20
+ normalized_artifact is not None
21
+ and normalized_artifact != ""
22
+ and _is_within(normalized_path, normalized_artifact)
23
+ ):
24
+ artifact_relative = _relative_to(normalized_path, normalized_artifact)
25
+ if artifact_relative is not None and _is_internal_screen_artifact(
26
+ artifact_relative
27
+ ):
28
+ return normalized_path
29
+ if (
30
+ normalized_workspace is not None
31
+ and normalized_workspace != ""
32
+ and _is_within(normalized_path, normalized_workspace)
33
+ ):
34
+ relative = _relative_to(normalized_path, normalized_workspace)
35
+ if relative is not None:
36
+ return relative
37
+ return normalized_path
38
+
39
+
40
+ def _normalize_slashes(value: str | None) -> str | None:
41
+ if value is None:
42
+ return None
43
+ return value.replace("\\", "/")
44
+
45
+
46
+ def _is_internal_screen_artifact(relative_path: str) -> bool:
47
+ relative_parts = _parts(relative_path)
48
+ if not relative_parts:
49
+ return False
50
+ return relative_parts[0].lower() == "screens"
51
+
52
+
53
+ def _is_within(path: str, base: str) -> bool:
54
+ path_parts = _parts(path)
55
+ base_parts = _parts(base)
56
+ if len(base_parts) > len(path_parts):
57
+ return False
58
+ if _drive(path_parts) != _drive(base_parts):
59
+ return False
60
+ path_slice = path_parts[: len(base_parts)]
61
+ if _is_windows_path(path_parts) and _is_windows_path(base_parts):
62
+ return _lowered(path_slice) == _lowered(base_parts)
63
+ return path_slice == base_parts
64
+
65
+
66
+ def _relative_to(path: str, base: str) -> str | None:
67
+ path_parts = _parts(path)
68
+ base_parts = _parts(base)
69
+ if not _is_within(path, base):
70
+ return None
71
+ relative_parts = path_parts[len(base_parts) :]
72
+ if not relative_parts:
73
+ return "."
74
+ return "/".join(relative_parts)
75
+
76
+
77
+ def _parts(value: str) -> tuple[str, ...]:
78
+ trimmed = value.rstrip("/")
79
+ if trimmed == "":
80
+ return ()
81
+ if _WINDOWS_ABS_RE.match(trimmed):
82
+ drive, tail = trimmed.split(":/", maxsplit=1)
83
+ head = f"{drive.upper()}:"
84
+ tail_parts = tuple(part for part in tail.split("/") if part)
85
+ return (head, *tail_parts)
86
+ if trimmed.startswith("/"):
87
+ return ("/", *tuple(part for part in trimmed[1:].split("/") if part))
88
+ return tuple(part for part in trimmed.split("/") if part)
89
+
90
+
91
+ def _drive(parts: tuple[str, ...]) -> str | None:
92
+ if not parts:
93
+ return None
94
+ head = parts[0]
95
+ if head == "/":
96
+ return "/"
97
+ if head.endswith(":"):
98
+ return head.lower()
99
+ return None
100
+
101
+
102
+ def _lowered(parts: tuple[str, ...]) -> tuple[str, ...]:
103
+ return tuple(part.lower() for part in parts)
104
+
105
+
106
+ def _is_windows_path(parts: tuple[str, ...]) -> bool:
107
+ if not parts:
108
+ return False
109
+ return parts[0].endswith(":")
@@ -0,0 +1,234 @@
1
+ from __future__ import annotations
2
+
3
+ from xml.etree.ElementTree import Element, SubElement, tostring
4
+
5
+ from androidctl.errors.models import ErrorTier, PublicError
6
+ from androidctl.renderers import RenderPayload
7
+ from androidctl.renderers.xml_projection import (
8
+ XmlActionTargetProjection,
9
+ XmlGroupItemProjection,
10
+ XmlGroupProjection,
11
+ XmlOmittedEntryProjection,
12
+ XmlScalarAttrs,
13
+ XmlScreenProjection,
14
+ XmlSurfaceProjection,
15
+ XmlTransientItemProjection,
16
+ project_xml_payload,
17
+ )
18
+
19
+ _CLOSE_ERROR_FALLBACK_MESSAGE = "close failed"
20
+
21
+
22
+ def render_success_text(
23
+ *,
24
+ payload: dict[str, object],
25
+ ) -> str:
26
+ return render_xml(payload)
27
+
28
+
29
+ def render_error_text(
30
+ error: PublicError,
31
+ *,
32
+ command: str | None = None,
33
+ tier: ErrorTier,
34
+ execution_outcome: str | None = None,
35
+ ) -> str:
36
+ del execution_outcome
37
+ return render_error_xml(
38
+ _normalize_error_for_command(error, command=command),
39
+ command=command,
40
+ tier=tier,
41
+ )
42
+
43
+
44
+ def render_xml(payload: RenderPayload) -> str:
45
+ projection = project_xml_payload(payload)
46
+ if projection["kind"] == "listApps":
47
+ root = Element("listAppsResult", projection["attrs"])
48
+ _append_apps(root, projection["apps"])
49
+ return tostring(root, encoding="unicode", short_empty_elements=True)
50
+
51
+ if projection["kind"] == "retained":
52
+ root = Element("retainedResult", projection["attrs"])
53
+ message = projection["message"]
54
+ if message is not None:
55
+ SubElement(root, "message").text = message
56
+ _append_details(root, projection["details"])
57
+ _append_artifacts(root, projection["artifacts"])
58
+ return tostring(root, encoding="unicode", short_empty_elements=True)
59
+
60
+ root = Element(
61
+ "result",
62
+ projection["attrs"],
63
+ )
64
+
65
+ message = projection["message"]
66
+ if message is not None:
67
+ SubElement(root, "message").text = message
68
+
69
+ _append_truth(root, projection["truth"])
70
+ action_target = projection["actionTarget"]
71
+ if action_target is not None:
72
+ _append_action_target(root, action_target)
73
+ _append_items(root, "uncertainty", projection["uncertainty"])
74
+ _append_items(root, "warnings", projection["warnings"])
75
+
76
+ screen = projection["screen"]
77
+ if screen is not None:
78
+ _append_screen(root, screen)
79
+
80
+ _append_artifacts(root, projection["artifacts"])
81
+ return tostring(root, encoding="unicode", short_empty_elements=True)
82
+
83
+
84
+ def render_error_xml(
85
+ error: PublicError,
86
+ *,
87
+ command: str | None = None,
88
+ tier: ErrorTier,
89
+ ) -> str:
90
+ public_command = command or "observe"
91
+ root = Element(
92
+ "errorResult",
93
+ {
94
+ "ok": "false",
95
+ "code": error.code,
96
+ "exitCode": str(int(error.exit_code)),
97
+ "tier": tier,
98
+ "command": public_command,
99
+ },
100
+ )
101
+ if _has_non_whitespace_text(error.message):
102
+ SubElement(root, "message").text = error.message
103
+ if _has_non_whitespace_text(error.hint):
104
+ SubElement(root, "hint").text = error.hint
105
+ return tostring(root, encoding="unicode", short_empty_elements=True)
106
+
107
+
108
+ def _has_non_whitespace_text(value: str | None) -> bool:
109
+ return value is not None and bool(value.strip())
110
+
111
+
112
+ def _with_close_error_message(error: PublicError) -> PublicError:
113
+ normalized = error.message.strip()
114
+ if normalized:
115
+ return error
116
+ return PublicError(
117
+ code=error.code,
118
+ message=_CLOSE_ERROR_FALLBACK_MESSAGE,
119
+ hint=error.hint,
120
+ exit_code=error.exit_code,
121
+ )
122
+
123
+
124
+ def _normalize_error_for_command(
125
+ error: PublicError,
126
+ *,
127
+ command: str | None,
128
+ ) -> PublicError:
129
+ if command == "close":
130
+ return _with_close_error_message(error)
131
+ return error
132
+
133
+
134
+ def _append_truth(parent: Element, truth_attrs: XmlScalarAttrs) -> None:
135
+ SubElement(parent, "truth", truth_attrs)
136
+
137
+
138
+ def _append_action_target(
139
+ parent: Element,
140
+ action_target: XmlActionTargetProjection,
141
+ ) -> None:
142
+ SubElement(parent, "actionTarget", action_target["attrs"])
143
+
144
+
145
+ def _append_items(parent: Element, tag: str, items: list[str]) -> None:
146
+ container = SubElement(parent, tag)
147
+ for item in items:
148
+ SubElement(container, "item").text = item
149
+
150
+
151
+ def _append_artifacts(parent: Element, artifacts_attrs: XmlScalarAttrs) -> None:
152
+ SubElement(parent, "artifacts", artifacts_attrs)
153
+
154
+
155
+ def _append_details(parent: Element, details_attrs: XmlScalarAttrs) -> None:
156
+ if details_attrs:
157
+ SubElement(parent, "details", details_attrs)
158
+
159
+
160
+ def _append_apps(parent: Element, apps: list[XmlScalarAttrs]) -> None:
161
+ apps_elem = SubElement(parent, "apps")
162
+ for item in apps:
163
+ SubElement(apps_elem, "app", item)
164
+
165
+
166
+ def _append_screen(parent: Element, screen: XmlScreenProjection) -> None:
167
+ screen_elem = SubElement(parent, "screen", screen["attrs"])
168
+ _append_app(screen_elem, screen["app"])
169
+ _append_surface(screen_elem, screen["surface"])
170
+ _append_groups(screen_elem, screen["groups"])
171
+ _append_omitted(screen_elem, screen["omitted"])
172
+ _append_visible_windows(screen_elem, screen["visibleWindows"])
173
+ _append_transient(screen_elem, screen["transient"])
174
+
175
+
176
+ def _append_app(parent: Element, app_attrs: XmlScalarAttrs) -> None:
177
+ SubElement(parent, "app", app_attrs)
178
+
179
+
180
+ def _append_surface(parent: Element, surface: XmlSurfaceProjection) -> None:
181
+ surface_elem = SubElement(parent, "surface", surface["attrs"])
182
+ SubElement(surface_elem, "focus", surface["focus"])
183
+
184
+
185
+ def _append_groups(parent: Element, groups: list[XmlGroupProjection]) -> None:
186
+ groups_elem = SubElement(parent, "groups")
187
+ for group in groups:
188
+ group_elem = SubElement(groups_elem, group["name"])
189
+ for item in group["items"]:
190
+ _append_group_item(group_elem, item)
191
+
192
+
193
+ def _append_group_item(parent: Element, item: XmlGroupItemProjection) -> None:
194
+ if "text" in item:
195
+ text_elem = SubElement(parent, item["tag"], item["attrs"])
196
+ text_elem.text = item["text"]
197
+ return
198
+
199
+ node_elem = SubElement(parent, item["tag"], item["attrs"])
200
+ for child in item["children"]:
201
+ _append_group_item(node_elem, child)
202
+
203
+
204
+ def _append_omitted(
205
+ parent: Element,
206
+ omitted: list[XmlOmittedEntryProjection],
207
+ ) -> None:
208
+ omitted_elem = SubElement(parent, "omitted")
209
+ for item in omitted:
210
+ attrs: XmlScalarAttrs = {
211
+ "group": item["group"],
212
+ "reason": item["reason"],
213
+ }
214
+ if "count" in item:
215
+ attrs["count"] = item["count"]
216
+ SubElement(omitted_elem, "entry", attrs)
217
+
218
+
219
+ def _append_visible_windows(parent: Element, windows: list[XmlScalarAttrs]) -> None:
220
+ windows_elem = SubElement(parent, "visibleWindows")
221
+ for item in windows:
222
+ SubElement(windows_elem, "window", item)
223
+
224
+
225
+ def _append_transient(
226
+ parent: Element,
227
+ transient_items: list[XmlTransientItemProjection],
228
+ ) -> None:
229
+ transient_elem = SubElement(parent, "transient")
230
+ for item in transient_items:
231
+ attrs: XmlScalarAttrs = {}
232
+ if "kind" in item:
233
+ attrs["kind"] = item["kind"]
234
+ SubElement(transient_elem, "item", attrs).text = item["text"]