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,286 @@
1
+ """Semantic wait command handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from androidctld.commands.command_models import (
6
+ GoneWaitPredicate,
7
+ ScreenChangeWaitPredicate,
8
+ WaitCommand,
9
+ )
10
+ from androidctld.commands.orchestration import current_command_record
11
+ from androidctld.commands.result_builders import app_payload
12
+ from androidctld.commands.result_models import (
13
+ SemanticResultAssemblyInput,
14
+ build_semantic_failure_result,
15
+ build_semantic_success_result,
16
+ )
17
+ from androidctld.commands.semantic_error_mapping import (
18
+ map_daemon_error_to_semantic_failure,
19
+ )
20
+ from androidctld.commands.semantic_truth import (
21
+ resolve_runtime_continuity,
22
+ )
23
+ from androidctld.errors import DaemonError, DaemonErrorCode
24
+ from androidctld.runtime import RuntimeKernel
25
+ from androidctld.runtime.models import WorkspaceRuntime
26
+ from androidctld.runtime.screen_state import (
27
+ current_artifacts,
28
+ current_public_screen,
29
+ get_authoritative_current_basis,
30
+ )
31
+ from androidctld.semantics.compiler import CompiledScreen
32
+ from androidctld.waits.evaluators import WaitMatchData
33
+ from androidctld.waits.loop import (
34
+ WaitLoopOutcome,
35
+ WaitRuntimeLoop,
36
+ )
37
+
38
+
39
+ class WaitCommandHandler:
40
+ def __init__(
41
+ self,
42
+ *,
43
+ runtime_kernel: RuntimeKernel,
44
+ wait_runtime_loop: WaitRuntimeLoop,
45
+ ) -> None:
46
+ self._runtime_kernel = runtime_kernel
47
+ self._wait_runtime_loop = wait_runtime_loop
48
+
49
+ def handle_service_wait(
50
+ self,
51
+ *,
52
+ command: WaitCommand,
53
+ ) -> dict[str, object]:
54
+ runtime = self._runtime_kernel.ensure_runtime()
55
+ source_screen_id = _wait_basis_screen_id(command)
56
+ source_compiled_screen = None
57
+ try:
58
+ source_compiled_screen = _validate_relative_wait_entry(
59
+ runtime=runtime,
60
+ command=command,
61
+ )
62
+ _ensure_wait_runtime_connected(runtime)
63
+ lifecycle_lease = self._runtime_kernel.capture_lifecycle_lease(runtime)
64
+ outcome = self._wait_runtime_loop.run(
65
+ session=runtime,
66
+ record=current_command_record(
67
+ kind=command.kind,
68
+ result_command="wait",
69
+ ),
70
+ command=command,
71
+ lifecycle_lease=lifecycle_lease,
72
+ )
73
+ assembly_input = _wait_loop_result(command=command, outcome=outcome)
74
+ continuity_status, changed = _wait_success_overrides(
75
+ command=command,
76
+ runtime=runtime,
77
+ )
78
+ return _wait_success_payload(
79
+ runtime,
80
+ source_screen_id=source_screen_id,
81
+ source_compiled_screen=source_compiled_screen,
82
+ assembly_input=assembly_input,
83
+ continuity_status=continuity_status,
84
+ changed=changed,
85
+ )
86
+ except DaemonError as error:
87
+ return _wait_failure_payload(
88
+ runtime,
89
+ source_screen_id=source_screen_id,
90
+ source_compiled_screen=source_compiled_screen,
91
+ error=error,
92
+ )
93
+
94
+
95
+ def _ensure_wait_runtime_connected(runtime: WorkspaceRuntime) -> None:
96
+ if runtime.connection is None or runtime.device_token is None:
97
+ raise DaemonError(
98
+ code=DaemonErrorCode.RUNTIME_NOT_CONNECTED,
99
+ message="runtime is not connected to a device",
100
+ retryable=False,
101
+ details={"workspaceRoot": runtime.workspace_root.as_posix()},
102
+ http_status=200,
103
+ )
104
+
105
+
106
+ def _wait_loop_result(
107
+ *,
108
+ command: WaitCommand,
109
+ outcome: WaitLoopOutcome,
110
+ ) -> SemanticResultAssemblyInput:
111
+ if isinstance(outcome, WaitMatchData):
112
+ return SemanticResultAssemblyInput(
113
+ app_payload=app_payload(
114
+ outcome.snapshot,
115
+ app_match=outcome.app_match,
116
+ ),
117
+ )
118
+ raise _wait_timeout_error(command)
119
+
120
+
121
+ def _wait_timeout_error(command: WaitCommand) -> DaemonError:
122
+ return DaemonError(
123
+ code=DaemonErrorCode.WAIT_TIMEOUT,
124
+ message=f"wait {command.wait_kind.value} timed out",
125
+ retryable=True,
126
+ details={"kind": command.kind.value, "waitKind": command.wait_kind.value},
127
+ http_status=200,
128
+ )
129
+
130
+
131
+ def _wait_success_payload(
132
+ runtime: WorkspaceRuntime,
133
+ *,
134
+ source_screen_id: str | None,
135
+ source_compiled_screen: CompiledScreen | None,
136
+ assembly_input: SemanticResultAssemblyInput | None = None,
137
+ continuity_status: str | None = None,
138
+ changed: bool | None = None,
139
+ ) -> dict[str, object]:
140
+ current_screen = current_public_screen(runtime)
141
+ artifacts = current_artifacts(runtime)
142
+ resolved_continuity, resolved_changed = _resolve_wait_continuity(
143
+ runtime=runtime,
144
+ source_screen_id=source_screen_id,
145
+ source_compiled_screen=source_compiled_screen,
146
+ continuity_status=continuity_status,
147
+ changed=changed,
148
+ )
149
+ return build_semantic_success_result(
150
+ command="wait",
151
+ category="wait",
152
+ source_screen_id=source_screen_id,
153
+ next_screen=current_screen,
154
+ app_payload=(None if assembly_input is None else assembly_input.app_payload),
155
+ artifacts=artifacts,
156
+ continuity_status=resolved_continuity,
157
+ execution_outcome="notApplicable",
158
+ changed=resolved_changed,
159
+ warnings=([] if assembly_input is None else list(assembly_input.warnings)),
160
+ ).model_dump(by_alias=True, mode="json")
161
+
162
+
163
+ def _wait_failure_payload(
164
+ runtime: WorkspaceRuntime,
165
+ *,
166
+ source_screen_id: str | None,
167
+ source_compiled_screen: CompiledScreen | None,
168
+ error: DaemonError,
169
+ ) -> dict[str, object]:
170
+ current_screen = current_public_screen(runtime)
171
+ mapped = map_daemon_error_to_semantic_failure(
172
+ command_name="wait",
173
+ error=error,
174
+ )
175
+ if mapped is None:
176
+ raise error
177
+ continuity_status, changed = _resolve_wait_continuity(
178
+ runtime=runtime,
179
+ source_screen_id=source_screen_id,
180
+ source_compiled_screen=source_compiled_screen,
181
+ )
182
+ return build_semantic_failure_result(
183
+ command="wait",
184
+ category="wait",
185
+ code=mapped.code,
186
+ message=mapped.message,
187
+ source_screen_id=source_screen_id,
188
+ current_screen=current_screen,
189
+ artifacts=current_artifacts(runtime),
190
+ continuity_status=continuity_status,
191
+ observation_quality="authoritative" if current_screen is not None else "none",
192
+ changed=changed,
193
+ ).model_dump(by_alias=True, mode="json")
194
+
195
+
196
+ def _resolve_wait_continuity(
197
+ *,
198
+ runtime: WorkspaceRuntime,
199
+ source_screen_id: str | None,
200
+ source_compiled_screen: CompiledScreen | None,
201
+ continuity_status: str | None = None,
202
+ changed: bool | None = None,
203
+ ) -> tuple[str, bool | None]:
204
+ continuity = resolve_runtime_continuity(
205
+ runtime=runtime,
206
+ source_screen_id=source_screen_id,
207
+ source_compiled_screen=source_compiled_screen,
208
+ )
209
+ return (
210
+ (
211
+ continuity.continuity_status
212
+ if continuity_status is None
213
+ else continuity_status
214
+ ),
215
+ continuity.changed if changed is None else changed,
216
+ )
217
+
218
+
219
+ def _wait_success_overrides(
220
+ *,
221
+ command: WaitCommand,
222
+ runtime: WorkspaceRuntime,
223
+ ) -> tuple[str | None, bool | None]:
224
+ current_screen = current_public_screen(runtime)
225
+ if current_screen is None:
226
+ return None, None
227
+ predicate = command.predicate
228
+ if isinstance(predicate, GoneWaitPredicate):
229
+ return "stale", True
230
+ if (
231
+ isinstance(predicate, ScreenChangeWaitPredicate)
232
+ and current_screen.screen_id != predicate.source_screen_id
233
+ ):
234
+ return None, True
235
+ return None, None
236
+
237
+
238
+ def _wait_basis_screen_id(command: WaitCommand) -> str | None:
239
+ predicate = _relative_wait_predicate(command)
240
+ if predicate is not None:
241
+ return predicate.source_screen_id
242
+ return None
243
+
244
+
245
+ def _relative_wait_predicate(
246
+ command: WaitCommand,
247
+ ) -> ScreenChangeWaitPredicate | GoneWaitPredicate | None:
248
+ predicate = command.predicate
249
+ if isinstance(predicate, (ScreenChangeWaitPredicate, GoneWaitPredicate)):
250
+ return predicate
251
+ return None
252
+
253
+
254
+ def _validate_relative_wait_entry(
255
+ *,
256
+ runtime: WorkspaceRuntime,
257
+ command: WaitCommand,
258
+ ) -> CompiledScreen | None:
259
+ predicate = _relative_wait_predicate(command)
260
+ if predicate is None:
261
+ return None
262
+ basis = get_authoritative_current_basis(runtime)
263
+ if basis is None or basis.screen_id != predicate.source_screen_id:
264
+ raise _relative_wait_entry_unavailable(command)
265
+ if (
266
+ isinstance(predicate, GoneWaitPredicate)
267
+ and predicate.ref not in basis.public_refs
268
+ ):
269
+ raise _relative_wait_entry_unavailable(command)
270
+ return basis.compiled_screen
271
+
272
+
273
+ def _relative_wait_entry_unavailable(command: WaitCommand) -> DaemonError:
274
+ details: dict[str, object] = {"waitKind": command.wait_kind.value}
275
+ predicate = _relative_wait_predicate(command)
276
+ if predicate is not None:
277
+ details["sourceScreenId"] = predicate.source_screen_id
278
+ if isinstance(predicate, GoneWaitPredicate):
279
+ details["ref"] = predicate.ref
280
+ return DaemonError(
281
+ code=DaemonErrorCode.SCREEN_NOT_READY,
282
+ message="No current device observation is available.",
283
+ retryable=True,
284
+ details=details,
285
+ http_status=200,
286
+ )
@@ -0,0 +1,65 @@
1
+ """Typed command ledger models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+ from androidctl_contracts.command_results import (
10
+ CommandResultCore,
11
+ ListAppsResult,
12
+ RetainedResultEnvelope,
13
+ )
14
+ from androidctld.commands.semantic_command_names import (
15
+ semantic_result_command_for_daemon_kind,
16
+ )
17
+ from androidctld.errors import DaemonError, DaemonErrorCode
18
+ from androidctld.protocol import CommandKind
19
+
20
+
21
+ class CommandStatus(str, Enum):
22
+ RUNNING = "running"
23
+ SUCCEEDED = "succeeded"
24
+ FAILED = "failed"
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class CachedCommandError:
29
+ code: DaemonErrorCode
30
+ message: str
31
+ retryable: bool
32
+ details: dict[str, Any]
33
+
34
+ def __post_init__(self) -> None:
35
+ object.__setattr__(self, "code", DaemonErrorCode(self.code))
36
+
37
+ @classmethod
38
+ def from_daemon_error(cls, error: DaemonError) -> CachedCommandError:
39
+ return cls(
40
+ code=error.code,
41
+ message=error.message,
42
+ retryable=error.retryable,
43
+ details=dict(error.details),
44
+ )
45
+
46
+
47
+ @dataclass
48
+ class CommandRecord:
49
+ command_id: str
50
+ kind: CommandKind
51
+ status: CommandStatus
52
+ started_at: str
53
+ result_command: str | None = None
54
+ completed_at: str | None = None
55
+ result: CommandResultCore | RetainedResultEnvelope | ListAppsResult | None = None
56
+ error: CachedCommandError | None = None
57
+
58
+ def __post_init__(self) -> None:
59
+ if self.result_command is None:
60
+ self.result_command = semantic_result_command_for_daemon_kind(self.kind)
61
+ return
62
+ normalized_result_command = self.result_command.strip()
63
+ if not normalized_result_command:
64
+ raise ValueError("result_command must be a non-empty string")
65
+ self.result_command = normalized_result_command
@@ -0,0 +1,56 @@
1
+ """Typed runtime open-target ADTs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ from androidctld.errors import bad_request
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class OpenAppTarget:
12
+ package_name: str
13
+ kind: str = field(default="app", init=False)
14
+
15
+ @property
16
+ def value(self) -> str:
17
+ return self.package_name
18
+
19
+ @property
20
+ def required_action_kind(self) -> str:
21
+ return "launchApp"
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class OpenUrlTarget:
26
+ url: str
27
+ kind: str = field(default="url", init=False)
28
+
29
+ @property
30
+ def value(self) -> str:
31
+ return self.url
32
+
33
+ @property
34
+ def required_action_kind(self) -> str:
35
+ return "openUrl"
36
+
37
+
38
+ def validate_open_target(
39
+ target: OpenAppTarget | OpenUrlTarget,
40
+ ) -> OpenAppTarget | OpenUrlTarget:
41
+ if isinstance(target, OpenAppTarget):
42
+ if not target.package_name:
43
+ raise bad_request("open requires target.kind app|url and target.value")
44
+ return target
45
+ if isinstance(target, OpenUrlTarget):
46
+ if not target.url:
47
+ raise bad_request("open requires target.kind app|url and target.value")
48
+ return target
49
+ raise bad_request("open requires target.kind app|url and target.value")
50
+
51
+
52
+ __all__ = [
53
+ "OpenAppTarget",
54
+ "OpenUrlTarget",
55
+ "validate_open_target",
56
+ ]