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.
- androidctl/__init__.py +5 -0
- androidctl/__main__.py +4 -0
- androidctl/_version.py +1 -0
- androidctl/app.py +73 -0
- androidctl/cli_options.py +27 -0
- androidctl/command_payloads.py +264 -0
- androidctl/command_views.py +157 -0
- androidctl/commands/__init__.py +1 -0
- androidctl/commands/actions.py +236 -0
- androidctl/commands/adb_wireless.py +157 -0
- androidctl/commands/close.py +30 -0
- androidctl/commands/connect.py +69 -0
- androidctl/commands/execute.py +179 -0
- androidctl/commands/list_apps.py +26 -0
- androidctl/commands/observe.py +26 -0
- androidctl/commands/open.py +41 -0
- androidctl/commands/plumbing.py +58 -0
- androidctl/commands/run_pipeline.py +307 -0
- androidctl/commands/screenshot.py +29 -0
- androidctl/commands/setup.py +301 -0
- androidctl/commands/wait.py +60 -0
- androidctl/daemon/__init__.py +1 -0
- androidctl/daemon/client.py +348 -0
- androidctl/daemon/discovery.py +190 -0
- androidctl/daemon/launcher.py +26 -0
- androidctl/daemon/owner.py +349 -0
- androidctl/errors/__init__.py +1 -0
- androidctl/errors/mapping.py +149 -0
- androidctl/errors/models.py +16 -0
- androidctl/exit_codes.py +8 -0
- androidctl/output.py +147 -0
- androidctl/parsing/__init__.py +1 -0
- androidctl/parsing/duration.py +17 -0
- androidctl/parsing/open_target.py +51 -0
- androidctl/parsing/refs.py +12 -0
- androidctl/parsing/screen_id.py +10 -0
- androidctl/parsing/wait.py +70 -0
- androidctl/renderers/__init__.py +110 -0
- androidctl/renderers/_paths.py +109 -0
- androidctl/renderers/xml.py +234 -0
- androidctl/renderers/xml_projection.py +732 -0
- androidctl/resources/__init__.py +1 -0
- androidctl/resources/androidctl-agent-0.1.0-release.apk +0 -0
- androidctl/setup/__init__.py +1 -0
- androidctl/setup/accessibility.py +159 -0
- androidctl/setup/adb.py +586 -0
- androidctl/setup/apk_resource.py +29 -0
- androidctl/setup/pairing.py +70 -0
- androidctl/setup/verify.py +175 -0
- androidctl/workspace/__init__.py +3 -0
- androidctl/workspace/resolve.py +27 -0
- androidctl-0.1.0.dist-info/METADATA +217 -0
- androidctl-0.1.0.dist-info/RECORD +187 -0
- androidctl-0.1.0.dist-info/WHEEL +5 -0
- androidctl-0.1.0.dist-info/entry_points.txt +3 -0
- androidctl-0.1.0.dist-info/licenses/LICENSE +674 -0
- androidctl-0.1.0.dist-info/top_level.txt +3 -0
- androidctl_contracts/__init__.py +55 -0
- androidctl_contracts/_version.py +1 -0
- androidctl_contracts/_wire_helpers.py +31 -0
- androidctl_contracts/base.py +142 -0
- androidctl_contracts/command_catalog.py +414 -0
- androidctl_contracts/command_results.py +630 -0
- androidctl_contracts/daemon_api.py +335 -0
- androidctl_contracts/errors.py +44 -0
- androidctl_contracts/paths.py +5 -0
- androidctl_contracts/public_screen.py +579 -0
- androidctl_contracts/user_state.py +23 -0
- androidctl_contracts/vocabulary.py +82 -0
- androidctld/__init__.py +5 -0
- androidctld/__main__.py +63 -0
- androidctld/_version.py +1 -0
- androidctld/actions/__init__.py +1 -0
- androidctld/actions/action_target.py +142 -0
- androidctld/actions/capabilities.py +539 -0
- androidctld/actions/executor.py +894 -0
- androidctld/actions/focus_confirmation.py +177 -0
- androidctld/actions/focused_input_admissibility.py +120 -0
- androidctld/actions/fresh_current.py +176 -0
- androidctld/actions/postconditions.py +473 -0
- androidctld/actions/repair.py +101 -0
- androidctld/actions/request_builder.py +204 -0
- androidctld/actions/settle.py +146 -0
- androidctld/actions/submit_confirmation.py +211 -0
- androidctld/actions/submit_routing.py +311 -0
- androidctld/actions/type_confirmation.py +257 -0
- androidctld/app_targets.py +71 -0
- androidctld/artifacts/__init__.py +1 -0
- androidctld/artifacts/models.py +26 -0
- androidctld/artifacts/screen_lookup.py +241 -0
- androidctld/artifacts/screen_payloads.py +109 -0
- androidctld/artifacts/writer.py +286 -0
- androidctld/auth/__init__.py +1 -0
- androidctld/auth/active_registry.py +266 -0
- androidctld/auth/secret_files.py +52 -0
- androidctld/auth/token_store.py +59 -0
- androidctld/commands/__init__.py +1 -0
- androidctld/commands/assembly.py +231 -0
- androidctld/commands/command_models.py +254 -0
- androidctld/commands/dispatch.py +99 -0
- androidctld/commands/executor.py +31 -0
- androidctld/commands/from_boundary.py +175 -0
- androidctld/commands/handlers/__init__.py +15 -0
- androidctld/commands/handlers/action.py +439 -0
- androidctld/commands/handlers/connect.py +94 -0
- androidctld/commands/handlers/list_apps.py +215 -0
- androidctld/commands/handlers/observe.py +121 -0
- androidctld/commands/handlers/screenshot.py +105 -0
- androidctld/commands/handlers/wait.py +286 -0
- androidctld/commands/models.py +65 -0
- androidctld/commands/open_targets.py +56 -0
- androidctld/commands/orchestration.py +353 -0
- androidctld/commands/registry.py +116 -0
- androidctld/commands/result_builders.py +40 -0
- androidctld/commands/result_models.py +555 -0
- androidctld/commands/results.py +108 -0
- androidctld/commands/semantic_command_names.py +17 -0
- androidctld/commands/semantic_error_mapping.py +93 -0
- androidctld/commands/semantic_truth.py +135 -0
- androidctld/commands/service.py +67 -0
- androidctld/config.py +75 -0
- androidctld/daemon/__init__.py +1 -0
- androidctld/daemon/active_slot.py +326 -0
- androidctld/daemon/envelope.py +30 -0
- androidctld/daemon/http_host.py +123 -0
- androidctld/daemon/ingress.py +112 -0
- androidctld/daemon/ownership_probe.py +204 -0
- androidctld/daemon/server.py +286 -0
- androidctld/daemon/service.py +99 -0
- androidctld/device/__init__.py +1 -0
- androidctld/device/action_models.py +154 -0
- androidctld/device/action_serialization.py +121 -0
- androidctld/device/adapters.py +220 -0
- androidctld/device/bootstrap.py +153 -0
- androidctld/device/connectors.py +231 -0
- androidctld/device/errors.py +100 -0
- androidctld/device/interfaces.py +58 -0
- androidctld/device/parsing.py +320 -0
- androidctld/device/rpc.py +483 -0
- androidctld/device/schema.py +114 -0
- androidctld/device/types.py +161 -0
- androidctld/errors/__init__.py +94 -0
- androidctld/logging/__init__.py +22 -0
- androidctld/observation.py +98 -0
- androidctld/protocol.py +53 -0
- androidctld/refs/__init__.py +1 -0
- androidctld/refs/models.py +54 -0
- androidctld/refs/repair.py +284 -0
- androidctld/refs/service.py +422 -0
- androidctld/rendering/__init__.py +1 -0
- androidctld/rendering/screen_xml.py +256 -0
- androidctld/runtime/__init__.py +21 -0
- androidctld/runtime/kernel.py +548 -0
- androidctld/runtime/lifecycle.py +19 -0
- androidctld/runtime/models.py +48 -0
- androidctld/runtime/screen_state.py +117 -0
- androidctld/runtime/state_repo.py +70 -0
- androidctld/runtime/store.py +76 -0
- androidctld/runtime_policy.py +127 -0
- androidctld/schema/__init__.py +5 -0
- androidctld/schema/base.py +132 -0
- androidctld/schema/core.py +35 -0
- androidctld/schema/daemon_api.py +108 -0
- androidctld/schema/persistence.py +161 -0
- androidctld/schema/persistence_io.py +41 -0
- androidctld/schema/validation_errors.py +309 -0
- androidctld/semantics/__init__.py +1 -0
- androidctld/semantics/compiler.py +610 -0
- androidctld/semantics/continuity.py +107 -0
- androidctld/semantics/labels.py +252 -0
- androidctld/semantics/models.py +25 -0
- androidctld/semantics/policy.py +23 -0
- androidctld/semantics/public_models.py +123 -0
- androidctld/semantics/registries.py +13 -0
- androidctld/semantics/submit_refs.py +417 -0
- androidctld/semantics/surface.py +254 -0
- androidctld/semantics/targets.py +167 -0
- androidctld/snapshots/__init__.py +1 -0
- androidctld/snapshots/models.py +219 -0
- androidctld/snapshots/refresh.py +273 -0
- androidctld/snapshots/schema.py +74 -0
- androidctld/snapshots/service.py +138 -0
- androidctld/text_equivalence.py +67 -0
- androidctld/waits/__init__.py +1 -0
- androidctld/waits/evaluators.py +216 -0
- androidctld/waits/loop.py +305 -0
- 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"]
|