elsewindow 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.
@@ -0,0 +1,429 @@
1
+ # Copyright (c) 2026 kogeler
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """Strictly load the Xpra live configuration mirrored from the maintained fork."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import math
10
+ import re
11
+ from dataclasses import dataclass
12
+ from decimal import Decimal, InvalidOperation
13
+ from functools import cache
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ PACKAGE_ROOT = Path(__file__).resolve().parent
18
+ NETWORK_PROFILES_PATH = PACKAGE_ROOT / "profiles.yml"
19
+ LIVE_CLI_PATH = PACKAGE_ROOT / "live-cli.yml"
20
+ FORK_NETWORK_PROFILES_URL = (
21
+ "https://github.com/kogeler/xpra/blob/develop/fork-maintenance/profiles.yml"
22
+ )
23
+ FORK_LIVE_CLI_URL = (
24
+ "https://github.com/kogeler/xpra/blob/develop/fork-maintenance/live-cli.yml"
25
+ )
26
+ CONFIG_BYTES_LIMIT = 128 * 1024
27
+ KEY_RE = re.compile(r"[a-z][a-z0-9_-]*")
28
+ PROFILE_RE = re.compile(r"[a-z][a-z0-9_]*")
29
+ PROFILE_FIELDS = frozenset(
30
+ {
31
+ "auto_refresh_delay_seconds",
32
+ "bandwidth_limit",
33
+ "min_quality",
34
+ "min_speed",
35
+ "refresh_rate_hz",
36
+ }
37
+ )
38
+ ROLE_FIELDS = {
39
+ "server": frozenset({"base", "commands", "diagnostics", "lifecycle", "transports"}),
40
+ "client": frozenset({"base", "commands", "diagnostics", "transports"}),
41
+ }
42
+ ROLE_COMMANDS = {
43
+ "server": frozenset({"info", "version"}),
44
+ "client": frozenset({"detach", "version"}),
45
+ }
46
+ PROFILE_OPTION_PREFIXES = (
47
+ "--auto-refresh-delay=",
48
+ "--bandwidth-limit=",
49
+ "--min-quality=",
50
+ "--min-speed=",
51
+ "--quality=",
52
+ "--refresh-rate=",
53
+ "--speed=",
54
+ )
55
+ ENCODING_PROFILE_POLICIES = {
56
+ "rgb": ("rgb", "strict"),
57
+ "h264": ("h264", "adaptive-alpha"),
58
+ }
59
+ DEFAULT_ENCODING_PROFILE = next(iter(ENCODING_PROFILE_POLICIES))
60
+
61
+
62
+ class LiveConfigError(ValueError):
63
+ """Raised when mirrored live configuration is malformed or ambiguous."""
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class NetworkProfile:
68
+ """One reviewed set of client-side quality and network controls."""
69
+
70
+ name: str
71
+ min_quality: int
72
+ min_speed: int
73
+ auto_refresh_delay_seconds: Decimal
74
+ refresh_rate_hz: int
75
+ bandwidth_limit: str
76
+
77
+ def client_options(self) -> tuple[str, ...]:
78
+ return (
79
+ f"--min-quality={self.min_quality}",
80
+ f"--min-speed={self.min_speed}",
81
+ f"--auto-refresh-delay={self.auto_refresh_delay_seconds:.2f}",
82
+ f"--refresh-rate={self.refresh_rate_hz}",
83
+ f"--bandwidth-limit={self.bandwidth_limit}",
84
+ )
85
+
86
+
87
+ def _yaml_scalar(value: str, path: Path, line_number: int) -> object:
88
+ try:
89
+ parsed = json.loads(value)
90
+ except json.JSONDecodeError as error:
91
+ raise LiveConfigError(
92
+ f"{path}:{line_number}: values must use JSON-compatible YAML scalars"
93
+ ) from error
94
+ if isinstance(parsed, (dict, list)):
95
+ raise LiveConfigError(f"{path}:{line_number}: flow collections are forbidden")
96
+ return parsed
97
+
98
+
99
+ def _yaml_tokens(path: Path) -> list[tuple[int, str, int]]:
100
+ if path.is_symlink() or not path.is_file():
101
+ raise LiveConfigError(f"live configuration is not a regular file: {path}")
102
+ try:
103
+ source_bytes = path.read_bytes()
104
+ source = source_bytes.decode("utf-8")
105
+ except (OSError, UnicodeDecodeError) as error:
106
+ raise LiveConfigError(
107
+ f"cannot read live configuration {path}: {error}"
108
+ ) from error
109
+ if len(source_bytes) > CONFIG_BYTES_LIMIT:
110
+ raise LiveConfigError(
111
+ f"live configuration exceeds {CONFIG_BYTES_LIMIT} bytes: {path}"
112
+ )
113
+ tokens: list[tuple[int, str, int]] = []
114
+ for line_number, raw in enumerate(source.splitlines(), 1):
115
+ if "\t" in raw or raw.rstrip() != raw:
116
+ raise LiveConfigError(f"{path}:{line_number}: unsafe whitespace")
117
+ if not raw or raw.lstrip().startswith("#"):
118
+ continue
119
+ indent = len(raw) - len(raw.lstrip(" "))
120
+ if indent % 2:
121
+ raise LiveConfigError(
122
+ f"{path}:{line_number}: indentation must use two spaces"
123
+ )
124
+ tokens.append((indent, raw[indent:], line_number))
125
+ if not tokens:
126
+ raise LiveConfigError(f"live configuration is empty: {path}")
127
+ return tokens
128
+
129
+
130
+ def _parse_yaml_block(
131
+ tokens: list[tuple[int, str, int]],
132
+ index: int,
133
+ indent: int,
134
+ path: Path,
135
+ ) -> tuple[object, int]:
136
+ if index >= len(tokens) or tokens[index][0] != indent:
137
+ raise LiveConfigError(f"{path}: invalid YAML block indentation")
138
+ list_block = tokens[index][1].startswith("- ")
139
+ value: list[object] | dict[str, object] = [] if list_block else {}
140
+ while index < len(tokens):
141
+ current_indent, content, line_number = tokens[index]
142
+ if current_indent < indent:
143
+ break
144
+ if current_indent != indent:
145
+ raise LiveConfigError(f"{path}:{line_number}: unexpected indentation")
146
+ if list_block:
147
+ if not content.startswith("- ") or not content[2:]:
148
+ raise LiveConfigError(
149
+ f"{path}:{line_number}: invalid scalar list entry"
150
+ )
151
+ assert isinstance(value, list)
152
+ value.append(_yaml_scalar(content[2:], path, line_number))
153
+ index += 1
154
+ continue
155
+ if content.startswith("- "):
156
+ raise LiveConfigError(f"{path}:{line_number}: mixed YAML collection types")
157
+ key, separator, payload = content.partition(":")
158
+ if not separator or not KEY_RE.fullmatch(key):
159
+ raise LiveConfigError(f"{path}:{line_number}: invalid mapping key")
160
+ assert isinstance(value, dict)
161
+ if key in value:
162
+ raise LiveConfigError(f"{path}:{line_number}: duplicate mapping key: {key}")
163
+ index += 1
164
+ payload = payload.lstrip(" ")
165
+ if payload:
166
+ value[key] = _yaml_scalar(payload, path, line_number)
167
+ continue
168
+ if index >= len(tokens) or tokens[index][0] != indent + 2:
169
+ raise LiveConfigError(f"{path}:{line_number}: mapping value is missing")
170
+ value[key], index = _parse_yaml_block(tokens, index, indent + 2, path)
171
+ return value, index
172
+
173
+
174
+ def load_strict_yaml(path: Path) -> dict[str, object]:
175
+ """Parse the small deterministic YAML subset used by the fork."""
176
+ tokens = _yaml_tokens(path)
177
+ if tokens[0][0] != 0:
178
+ raise LiveConfigError(f"{path}: top-level YAML must start at column zero")
179
+ payload, index = _parse_yaml_block(tokens, 0, 0, path)
180
+ if index != len(tokens) or not isinstance(payload, dict):
181
+ raise LiveConfigError(f"{path}: top-level YAML must be one mapping")
182
+ return payload
183
+
184
+
185
+ def _integer(value: object, *, name: str, minimum: int, maximum: int) -> int:
186
+ if isinstance(value, bool) or not isinstance(value, int):
187
+ raise LiveConfigError(f"network profile {name} must be an integer")
188
+ if not minimum <= value <= maximum:
189
+ raise LiveConfigError(
190
+ f"network profile {name} must be between {minimum} and {maximum}"
191
+ )
192
+ return value
193
+
194
+
195
+ def _network_profile(name: str, payload: object) -> NetworkProfile:
196
+ if not PROFILE_RE.fullmatch(name) or not isinstance(payload, dict):
197
+ raise LiveConfigError(f"invalid network profile: {name!r}")
198
+ if set(payload) != PROFILE_FIELDS:
199
+ raise LiveConfigError(f"network profile {name} fields are inconsistent")
200
+ delay_value = payload["auto_refresh_delay_seconds"]
201
+ if isinstance(delay_value, bool) or not isinstance(delay_value, (int, float)):
202
+ raise LiveConfigError(f"network profile {name} delay must be numeric")
203
+ try:
204
+ delay = Decimal(str(delay_value))
205
+ except InvalidOperation as error:
206
+ raise LiveConfigError(f"network profile {name} delay is invalid") from error
207
+ if not math.isfinite(float(delay)) or not Decimal("0.01") <= delay <= Decimal(60):
208
+ raise LiveConfigError(f"network profile {name} delay is out of range")
209
+ bandwidth = payload["bandwidth_limit"]
210
+ if not isinstance(bandwidth, str) or not re.fullmatch(
211
+ r"(?:0|[1-9][0-9]*(?:K|M|G)bps)", bandwidth
212
+ ):
213
+ raise LiveConfigError(f"network profile {name} bandwidth limit is invalid")
214
+ return NetworkProfile(
215
+ name=name,
216
+ min_quality=_integer(
217
+ payload["min_quality"], name=f"{name}.min_quality", minimum=0, maximum=100
218
+ ),
219
+ min_speed=_integer(
220
+ payload["min_speed"], name=f"{name}.min_speed", minimum=0, maximum=100
221
+ ),
222
+ auto_refresh_delay_seconds=delay,
223
+ refresh_rate_hz=_integer(
224
+ payload["refresh_rate_hz"],
225
+ name=f"{name}.refresh_rate_hz",
226
+ minimum=1,
227
+ maximum=240,
228
+ ),
229
+ bandwidth_limit=bandwidth,
230
+ )
231
+
232
+
233
+ @cache
234
+ def load_network_profiles(
235
+ path: Path = NETWORK_PROFILES_PATH,
236
+ ) -> tuple[str, dict[str, NetworkProfile]]:
237
+ payload = load_strict_yaml(path)
238
+ if (
239
+ set(payload) != {"schema", "default_profile", "profiles"}
240
+ or payload.get("schema") != 1
241
+ ):
242
+ raise LiveConfigError("network profile configuration schema is inconsistent")
243
+ profiles_payload = payload.get("profiles")
244
+ if not isinstance(profiles_payload, dict) or not profiles_payload:
245
+ raise LiveConfigError("network profile configuration has no profiles")
246
+ profiles = {
247
+ name: _network_profile(name, value) for name, value in profiles_payload.items()
248
+ }
249
+ default = payload.get("default_profile")
250
+ if not isinstance(default, str) or default not in profiles:
251
+ raise LiveConfigError("default network profile is unavailable")
252
+ return default, profiles
253
+
254
+
255
+ def network_profile_names(path: Path = NETWORK_PROFILES_PATH) -> tuple[str, ...]:
256
+ return tuple(load_network_profiles(path)[1])
257
+
258
+
259
+ def network_profile(name: str, path: Path = NETWORK_PROFILES_PATH) -> NetworkProfile:
260
+ profiles = load_network_profiles(path)[1]
261
+ try:
262
+ return profiles[name]
263
+ except KeyError as error:
264
+ raise LiveConfigError(f"unsupported live network profile: {name}") from error
265
+
266
+
267
+ def _option_list(value: object, *, label: str) -> tuple[str, ...]:
268
+ if (
269
+ not isinstance(value, list)
270
+ or not value
271
+ or any(
272
+ not isinstance(option, str)
273
+ or not option
274
+ or option.strip() != option
275
+ or "\x00" in option
276
+ for option in value
277
+ )
278
+ ):
279
+ raise LiveConfigError(f"{label} must be a non-empty list of exact arguments")
280
+ return tuple(value)
281
+
282
+
283
+ @cache
284
+ def load_live_cli(path: Path = LIVE_CLI_PATH) -> dict[str, dict[str, Any]]:
285
+ payload = load_strict_yaml(path)
286
+ if set(payload) != {"schema", "server", "client"} or payload.get("schema") != 1:
287
+ raise LiveConfigError("live CLI configuration schema is inconsistent")
288
+ result: dict[str, dict[str, Any]] = {}
289
+ all_options: list[str] = []
290
+ for role, expected_fields in ROLE_FIELDS.items():
291
+ role_payload = payload.get(role)
292
+ if not isinstance(role_payload, dict) or set(role_payload) != expected_fields:
293
+ raise LiveConfigError(f"live CLI {role} fields are inconsistent")
294
+ role_result: dict[str, Any] = {}
295
+ for block in expected_fields - {"commands", "transports"}:
296
+ options = _option_list(role_payload[block], label=f"{role}.{block}")
297
+ role_result[block] = options
298
+ all_options.extend(options)
299
+ commands = role_payload.get("commands")
300
+ if not isinstance(commands, dict) or set(commands) != ROLE_COMMANDS[role]:
301
+ raise LiveConfigError(f"live CLI {role} commands are inconsistent")
302
+ command_result = {
303
+ command: _option_list(options, label=f"{role}.commands.{command}")
304
+ for command, options in commands.items()
305
+ }
306
+ role_result["commands"] = command_result
307
+ for options in command_result.values():
308
+ all_options.extend(options)
309
+ transports = role_payload.get("transports")
310
+ if not isinstance(transports, dict) or not transports:
311
+ raise LiveConfigError(f"live CLI {role} transports are inconsistent")
312
+ transport_result: dict[str, dict[str, Any]] = {}
313
+ for encoding, transport in transports.items():
314
+ if not KEY_RE.fullmatch(encoding):
315
+ raise LiveConfigError(f"live CLI {role} transport name is invalid")
316
+ if not isinstance(transport, dict) or set(transport) != {
317
+ "common",
318
+ "policies",
319
+ }:
320
+ raise LiveConfigError(f"live CLI {role}.{encoding} is inconsistent")
321
+ common = _option_list(
322
+ transport["common"], label=f"{role}.{encoding}.common"
323
+ )
324
+ policies = transport.get("policies")
325
+ if (
326
+ not isinstance(policies, dict)
327
+ or not policies
328
+ or any(not KEY_RE.fullmatch(policy) for policy in policies)
329
+ ):
330
+ raise LiveConfigError(
331
+ f"live CLI {role}.{encoding} policies are inconsistent"
332
+ )
333
+ policy_result = {
334
+ policy: _option_list(options, label=f"{role}.{encoding}.{policy}")
335
+ for policy, options in policies.items()
336
+ }
337
+ transport_result[encoding] = {
338
+ "common": common,
339
+ "policies": policy_result,
340
+ }
341
+ all_options.extend(common)
342
+ for options in policy_result.values():
343
+ all_options.extend(options)
344
+ role_result["transports"] = transport_result
345
+ result[role] = role_result
346
+ server_transports = result["server"]["transports"]
347
+ client_transports = result["client"]["transports"]
348
+ if set(server_transports) != set(client_transports) or any(
349
+ set(server_transports[encoding]["policies"])
350
+ != set(client_transports[encoding]["policies"])
351
+ for encoding in server_transports
352
+ ):
353
+ raise LiveConfigError("server and client transport policies differ")
354
+ if any(option.startswith(PROFILE_OPTION_PREFIXES) for option in all_options):
355
+ raise LiveConfigError(
356
+ "profile-managed client arguments are forbidden in live CLI blocks"
357
+ )
358
+ return result
359
+
360
+
361
+ def static_cli_options(
362
+ role: str, block: str, path: Path = LIVE_CLI_PATH
363
+ ) -> tuple[str, ...]:
364
+ try:
365
+ value = load_live_cli(path)[role][block]
366
+ except KeyError as error:
367
+ raise LiveConfigError(
368
+ f"unsupported static live CLI block: {role}.{block}"
369
+ ) from error
370
+ if not isinstance(value, tuple):
371
+ raise LiveConfigError(f"live CLI block is not static: {role}.{block}")
372
+ return value
373
+
374
+
375
+ def command_cli_options(
376
+ role: str, command: str, path: Path = LIVE_CLI_PATH
377
+ ) -> tuple[str, ...]:
378
+ try:
379
+ value = load_live_cli(path)[role]["commands"][command]
380
+ except KeyError as error:
381
+ raise LiveConfigError(
382
+ f"unsupported live CLI command block: {role}.{command}"
383
+ ) from error
384
+ if not isinstance(value, tuple) or any(
385
+ not isinstance(option, str) for option in value
386
+ ):
387
+ raise LiveConfigError(f"live CLI command is invalid: {role}.{command}")
388
+ return value
389
+
390
+
391
+ def transport_options(
392
+ role: str,
393
+ encoding: str,
394
+ policy: str,
395
+ path: Path = LIVE_CLI_PATH,
396
+ ) -> tuple[str, ...]:
397
+ try:
398
+ transport = load_live_cli(path)[role]["transports"][encoding]
399
+ return (*transport["common"], *transport["policies"][policy])
400
+ except KeyError as error:
401
+ raise LiveConfigError(
402
+ f"unsupported live CLI transport: {role}.{encoding}.{policy}"
403
+ ) from error
404
+
405
+
406
+ def encoding_profile_names() -> tuple[str, ...]:
407
+ """Return public production profile names in stable order."""
408
+ return tuple(ENCODING_PROFILE_POLICIES)
409
+
410
+
411
+ def production_encoding(profile: str) -> str:
412
+ """Return the fork transport selected by one public production profile."""
413
+ try:
414
+ return ENCODING_PROFILE_POLICIES[profile][0]
415
+ except KeyError as error:
416
+ raise LiveConfigError(
417
+ f"unsupported production encoding profile: {profile}"
418
+ ) from error
419
+
420
+
421
+ def production_transport_options(role: str, profile: str) -> tuple[str, ...]:
422
+ """Render the reviewed strict RGB or universal adaptive-alpha H.264 profile."""
423
+ try:
424
+ encoding, policy = ENCODING_PROFILE_POLICIES[profile]
425
+ except KeyError as error:
426
+ raise LiveConfigError(
427
+ f"unsupported production encoding profile: {profile}"
428
+ ) from error
429
+ return transport_options(role, encoding, policy)
@@ -0,0 +1,41 @@
1
+ # Client-side network and quality profiles for every named live acceptance run.
2
+ # Select one with `NETWORK_PROFILE=<name>`; `default_profile` is used otherwise.
3
+ # Loader: infra/live/live_config.py. Integration tests: infra/live/test_job.py.
4
+ # Contract and usage: CONTRACT.md and docs/runbooks/live-tests.md.
5
+ schema: 1
6
+ default_profile: "gigabit_lan"
7
+ profiles:
8
+ gigabit_lan:
9
+ min_quality: 90
10
+ min_speed: 90
11
+ auto_refresh_delay_seconds: 0.10
12
+ refresh_rate_hz: 60
13
+ bandwidth_limit: "0"
14
+
15
+ fast_wired:
16
+ min_quality: 82
17
+ min_speed: 80
18
+ auto_refresh_delay_seconds: 0.20
19
+ refresh_rate_hz: 60
20
+ bandwidth_limit: "50Mbps"
21
+
22
+ mobile_5g:
23
+ min_quality: 78
24
+ min_speed: 75
25
+ auto_refresh_delay_seconds: 0.30
26
+ refresh_rate_hz: 45
27
+ bandwidth_limit: "25Mbps"
28
+
29
+ mobile_4g:
30
+ min_quality: 68
31
+ min_speed: 70
32
+ auto_refresh_delay_seconds: 0.45
33
+ refresh_rate_hz: 30
34
+ bandwidth_limit: "8Mbps"
35
+
36
+ power_saving:
37
+ min_quality: 72
38
+ min_speed: 70
39
+ auto_refresh_delay_seconds: 0.50
40
+ refresh_rate_hz: 30
41
+ bandwidth_limit: "8Mbps"
elsewindow/py.typed ADDED
@@ -0,0 +1 @@
1
+