termwright 0.2.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.
- termwright/__init__.py +125 -0
- termwright/client.py +708 -0
- termwright/debug.py +169 -0
- termwright/diffing.py +118 -0
- termwright/errors.py +20 -0
- termwright/framing.py +196 -0
- termwright/limits.py +82 -0
- termwright/logging_bridge.py +108 -0
- termwright/logs.py +207 -0
- termwright/marker.py +128 -0
- termwright/messages.py +417 -0
- termwright/roles.py +57 -0
- termwright/textual.py +249 -0
- termwright/tree.py +282 -0
- termwright/validate.py +846 -0
- termwright-0.2.0.dist-info/METADATA +336 -0
- termwright-0.2.0.dist-info/RECORD +25 -0
- termwright-0.2.0.dist-info/WHEEL +4 -0
- termwright_probe/__init__.py +73 -0
- termwright_probe/__main__.py +45 -0
- termwright_probe/bootstrap.py +207 -0
- termwright_probe/defer.py +130 -0
- termwright_probe/session.py +197 -0
- termwright_probe/textual_probe.py +239 -0
- termwright_probe/textual_tree.py +634 -0
termwright/messages.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""Wire messages: builders for what an adapter sends, parsers for what it receives.
|
|
2
|
+
|
|
3
|
+
Transport is length-prefixed JSON frames (see :mod:`termwright.framing`). The
|
|
4
|
+
adapter pushes commits; the driver issues requests; either side may send an
|
|
5
|
+
error and close. Everything is validated against the active limits before it is
|
|
6
|
+
retained, and failures are returned as typed results rather than raised.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Dict, Mapping, Optional, Sequence
|
|
13
|
+
|
|
14
|
+
from .errors import ProtocolViolation
|
|
15
|
+
from .framing import project_dto
|
|
16
|
+
from .limits import DEFAULT_LIMITS, LIMIT_FIELDS, ProtocolLimits
|
|
17
|
+
from .logs import LogRecord, validate_log_record
|
|
18
|
+
from .roles import CAPABILITY_SET
|
|
19
|
+
from .validate import validate_snapshot, validate_tree_delta
|
|
20
|
+
|
|
21
|
+
PROTOCOL_ID = "termwright/1"
|
|
22
|
+
PROTOCOL_V2_ID = "termwright/2"
|
|
23
|
+
PROTOCOL_VERSION = 1
|
|
24
|
+
|
|
25
|
+
ERROR_CODES = ("bad-token", "bad-version", "malformed", "limit-exceeded", "internal")
|
|
26
|
+
SUBSCRIBE_MODES = ("snapshots", "revisions", "diffs")
|
|
27
|
+
|
|
28
|
+
MAX_IDENTIFIER_LENGTH = 1024
|
|
29
|
+
_MAX_SAFE_INTEGER = 2**53 - 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ParseResult:
|
|
34
|
+
"""Outcome of parsing one wire message."""
|
|
35
|
+
|
|
36
|
+
ok: bool
|
|
37
|
+
message: Optional[Dict[str, Any]] = None
|
|
38
|
+
code: Optional[str] = None
|
|
39
|
+
detail: str = ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _malformed(detail: str) -> ParseResult:
|
|
43
|
+
return ParseResult(ok=False, code="malformed", detail=detail)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --------------------------------------------------------------------------
|
|
47
|
+
# Builders (adapter → driver)
|
|
48
|
+
# --------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def hello(
|
|
52
|
+
token: str,
|
|
53
|
+
adapter_name: str,
|
|
54
|
+
adapter_version: str,
|
|
55
|
+
capabilities: Sequence[str],
|
|
56
|
+
probe: Optional[Mapping[str, Any]] = None,
|
|
57
|
+
protocol: str = PROTOCOL_ID,
|
|
58
|
+
) -> Dict[str, Any]:
|
|
59
|
+
"""Build the handshake message. Unknown capabilities are refused locally.
|
|
60
|
+
|
|
61
|
+
``probe`` is present when the sender is a probe rather than a hand-written
|
|
62
|
+
adapter, and carries what it can actually observe — framework and
|
|
63
|
+
versions, the best identity it can offer, and its optional abilities — so
|
|
64
|
+
the driver negotiates against measured capability instead of a floor.
|
|
65
|
+
"""
|
|
66
|
+
unknown = [item for item in capabilities if item not in CAPABILITY_SET]
|
|
67
|
+
if unknown:
|
|
68
|
+
raise ProtocolViolation("marker-argument", f"unknown capabilities: {', '.join(unknown)}")
|
|
69
|
+
message: Dict[str, Any] = {
|
|
70
|
+
"type": "hello",
|
|
71
|
+
"protocol": protocol,
|
|
72
|
+
"token": token,
|
|
73
|
+
"adapter": {"name": adapter_name, "version": adapter_version},
|
|
74
|
+
"capabilities": list(capabilities),
|
|
75
|
+
}
|
|
76
|
+
if probe is not None:
|
|
77
|
+
message["probe"] = dict(probe)
|
|
78
|
+
return message
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def snapshot_message(snapshot: Mapping[str, Any]) -> Dict[str, Any]:
|
|
82
|
+
"""Wrap a wire-form snapshot in its envelope."""
|
|
83
|
+
return {"type": "snapshot", "snapshot": dict(snapshot)}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def revision_commit(revision: int) -> Dict[str, Any]:
|
|
87
|
+
"""Announce that ``revision`` has been committed to the terminal."""
|
|
88
|
+
return {"type": "revision-commit", "revision": revision}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_tree_result(request_id: int, snapshot: Optional[Mapping[str, Any]] = None, error: Optional[str] = None) -> Dict[str, Any]:
|
|
92
|
+
"""Answer a ``get-tree`` request with exactly one of a snapshot or an error."""
|
|
93
|
+
if (snapshot is None) == (error is None):
|
|
94
|
+
raise ProtocolViolation("marker-argument", "exactly one of snapshot or error must be given")
|
|
95
|
+
if snapshot is not None:
|
|
96
|
+
return {"type": "get-tree-result", "requestId": request_id, "snapshot": dict(snapshot)}
|
|
97
|
+
return {"type": "get-tree-result", "requestId": request_id, "error": error}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def log_message(record: LogRecord) -> Dict[str, Any]:
|
|
101
|
+
"""Wrap a log record in its envelope."""
|
|
102
|
+
return {"type": "log", "record": record.to_wire()}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def protocol_error(code: str, message: str) -> Dict[str, Any]:
|
|
106
|
+
"""Build a terminal error message; the sender closes after emitting it."""
|
|
107
|
+
if code not in ERROR_CODES:
|
|
108
|
+
raise ProtocolViolation("marker-argument", f"unknown error code {code}")
|
|
109
|
+
return {"type": "error", "code": code, "message": message}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# --------------------------------------------------------------------------
|
|
113
|
+
# Parsers
|
|
114
|
+
# --------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _project(value: Any, limits: ProtocolLimits) -> ParseResult:
|
|
118
|
+
try:
|
|
119
|
+
return ParseResult(ok=True, message=project_dto(value, limits.maxDepth))
|
|
120
|
+
except ProtocolViolation as error:
|
|
121
|
+
if error.code == "dto-depth":
|
|
122
|
+
return ParseResult(ok=False, code="limit-exceeded", detail=str(error))
|
|
123
|
+
return _malformed(str(error))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _identifier(value: Any, field: str, allow_empty: bool = False) -> Optional[str]:
|
|
127
|
+
if not isinstance(value, str):
|
|
128
|
+
return f"{field}: expected a string"
|
|
129
|
+
if len(value) > MAX_IDENTIFIER_LENGTH:
|
|
130
|
+
return f"{field}: expected at most {MAX_IDENTIFIER_LENGTH} characters"
|
|
131
|
+
if not allow_empty and not value:
|
|
132
|
+
return f"{field}: expected a non-empty string"
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _index(value: Any, field: str) -> Optional[str]:
|
|
137
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0 or value > _MAX_SAFE_INTEGER:
|
|
138
|
+
return f"{field}: expected a non-negative safe integer"
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _revision(value: Any, field: str) -> Optional[str]:
|
|
143
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0 or value > _MAX_SAFE_INTEGER:
|
|
144
|
+
return f"{field}: expected a positive safe integer"
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _required_keys(message: Mapping[str, Any], required: Sequence[str]) -> Optional[str]:
|
|
149
|
+
"""Check that every required key is present, tolerating unknown ones."""
|
|
150
|
+
missing = [key for key in required if key not in message]
|
|
151
|
+
if missing:
|
|
152
|
+
return f"missing field(s): {', '.join(missing)}"
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _exact_keys(message: Mapping[str, Any], required: Sequence[str], optional: Sequence[str] = ()) -> Optional[str]:
|
|
157
|
+
missing = [key for key in required if key not in message]
|
|
158
|
+
if missing:
|
|
159
|
+
return f"missing field(s): {', '.join(missing)}"
|
|
160
|
+
allowed = set(required) | set(optional)
|
|
161
|
+
unknown = [key for key in message if key not in allowed]
|
|
162
|
+
if unknown:
|
|
163
|
+
return f"unrecognized key(s): {', '.join(unknown)}"
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _check_tree_delta(value: Any, limits: ProtocolLimits) -> Optional[ParseResult]:
|
|
168
|
+
"""Map a delta shape failure onto the wire taxonomy."""
|
|
169
|
+
result = validate_tree_delta(value, limits)
|
|
170
|
+
if result.ok:
|
|
171
|
+
return None
|
|
172
|
+
over_capacity = result.code in ("bytes", "count", "depth", "string-bytes")
|
|
173
|
+
return ParseResult(
|
|
174
|
+
ok=False,
|
|
175
|
+
code="limit-exceeded" if over_capacity else "malformed",
|
|
176
|
+
detail=f"tree-delta {result.code}: {result.detail}",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _check_log_record(value: Any, limits: ProtocolLimits) -> Optional[ParseResult]:
|
|
181
|
+
"""Map a record failure onto the wire taxonomy, as the reference does."""
|
|
182
|
+
result = validate_log_record(value, limits)
|
|
183
|
+
if result.ok:
|
|
184
|
+
return None
|
|
185
|
+
over_capacity = result.code in ("bytes", "count", "depth", "string-bytes")
|
|
186
|
+
return ParseResult(
|
|
187
|
+
ok=False,
|
|
188
|
+
code="limit-exceeded" if over_capacity else "malformed",
|
|
189
|
+
detail=f"log record {result.code}: {result.detail}",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _check_log_budget(value: Any) -> Optional[str]:
|
|
194
|
+
"""Validate the optional log-channel budget carried by ``hello-ack``.
|
|
195
|
+
|
|
196
|
+
The field is absent unless the adapter announced the ``logs`` capability,
|
|
197
|
+
and absent means logs are disabled.
|
|
198
|
+
"""
|
|
199
|
+
if not isinstance(value, dict):
|
|
200
|
+
return "logs: expected an object"
|
|
201
|
+
issue = _required_keys(value, ("enabled", "maxRecordsPerSecond", "burst"))
|
|
202
|
+
if issue:
|
|
203
|
+
return f"logs: {issue}"
|
|
204
|
+
if not isinstance(value["enabled"], bool):
|
|
205
|
+
return "logs.enabled: expected a boolean"
|
|
206
|
+
if _revision(value["maxRecordsPerSecond"], "logs.maxRecordsPerSecond"):
|
|
207
|
+
return "logs.maxRecordsPerSecond: expected a positive safe integer"
|
|
208
|
+
if _index(value["burst"], "logs.burst"):
|
|
209
|
+
return "logs.burst: expected a non-negative safe integer"
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _check_snapshot(value: Any, limits: ProtocolLimits) -> Optional[ParseResult]:
|
|
214
|
+
result = validate_snapshot(value, limits)
|
|
215
|
+
if result.ok:
|
|
216
|
+
return None
|
|
217
|
+
over_capacity = result.code in ("bytes", "count", "depth", "string-bytes")
|
|
218
|
+
return ParseResult(
|
|
219
|
+
ok=False,
|
|
220
|
+
code="limit-exceeded" if over_capacity else "malformed",
|
|
221
|
+
detail=f"snapshot {result.code}: {result.detail}",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _check_error(message: Mapping[str, Any], strict: bool = True) -> Optional[str]:
|
|
226
|
+
issue = (
|
|
227
|
+
_exact_keys(message, ("type", "code", "message"))
|
|
228
|
+
if strict
|
|
229
|
+
else _required_keys(message, ("type", "code", "message"))
|
|
230
|
+
)
|
|
231
|
+
if issue:
|
|
232
|
+
return issue
|
|
233
|
+
if message["code"] not in ERROR_CODES:
|
|
234
|
+
return f"code: expected one of {', '.join(ERROR_CODES)}"
|
|
235
|
+
return _identifier(message["message"], "message", allow_empty=True)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def parse_adapter_message(value: Any, limits: ProtocolLimits = DEFAULT_LIMITS) -> ParseResult:
|
|
239
|
+
"""Parse one adapter → driver message. Never raises.
|
|
240
|
+
|
|
241
|
+
Strict: an unknown field from an adapter is a protocol error, not an
|
|
242
|
+
extension. See :func:`parse_driver_message` for the other direction.
|
|
243
|
+
"""
|
|
244
|
+
projected = _project(value, limits)
|
|
245
|
+
if not projected.ok:
|
|
246
|
+
return projected
|
|
247
|
+
message = projected.message
|
|
248
|
+
if not isinstance(message, dict) or not isinstance(message.get("type"), str):
|
|
249
|
+
return _malformed("unknown or missing message type")
|
|
250
|
+
|
|
251
|
+
kind = message["type"]
|
|
252
|
+
if kind == "hello":
|
|
253
|
+
protocol = message.get("protocol")
|
|
254
|
+
if isinstance(protocol, str) and protocol not in (PROTOCOL_ID, PROTOCOL_V2_ID):
|
|
255
|
+
return ParseResult(ok=False, code="bad-version", detail=f"unsupported protocol {protocol}")
|
|
256
|
+
issue = _exact_keys(message, ("type", "protocol", "token", "adapter", "capabilities"))
|
|
257
|
+
if issue:
|
|
258
|
+
return _malformed(issue)
|
|
259
|
+
if message["protocol"] not in (PROTOCOL_ID, PROTOCOL_V2_ID):
|
|
260
|
+
return _malformed("protocol: expected termwright/1 or termwright/2")
|
|
261
|
+
issue = _identifier(message["token"], "token")
|
|
262
|
+
if issue:
|
|
263
|
+
return _malformed(issue)
|
|
264
|
+
adapter = message["adapter"]
|
|
265
|
+
if not isinstance(adapter, dict):
|
|
266
|
+
return _malformed("adapter: expected an object")
|
|
267
|
+
issue = _exact_keys(adapter, ("name", "version"))
|
|
268
|
+
if issue:
|
|
269
|
+
return _malformed(f"adapter: {issue}")
|
|
270
|
+
for field in ("name", "version"):
|
|
271
|
+
issue = _identifier(adapter[field], f"adapter.{field}")
|
|
272
|
+
if issue:
|
|
273
|
+
return _malformed(issue)
|
|
274
|
+
capabilities = message["capabilities"]
|
|
275
|
+
if not isinstance(capabilities, list) or len(capabilities) > len(CAPABILITY_SET):
|
|
276
|
+
return _malformed("capabilities: expected a bounded array")
|
|
277
|
+
for item in capabilities:
|
|
278
|
+
if item not in CAPABILITY_SET:
|
|
279
|
+
return _malformed(f"capabilities: unknown capability {item!r}")
|
|
280
|
+
qualified = "qualified-observations" in capabilities
|
|
281
|
+
if (message["protocol"] == PROTOCOL_V2_ID) != qualified:
|
|
282
|
+
return _malformed("termwright/2 and qualified-observations must be negotiated together")
|
|
283
|
+
if "pointer-hit-grid" in capabilities and not qualified:
|
|
284
|
+
return _malformed("pointer-hit-grid requires qualified-observations")
|
|
285
|
+
return ParseResult(ok=True, message=message)
|
|
286
|
+
|
|
287
|
+
if kind == "revision-commit":
|
|
288
|
+
issue = _exact_keys(message, ("type", "revision")) or _revision(message.get("revision"), "revision")
|
|
289
|
+
return _malformed(issue) if issue else ParseResult(ok=True, message=message)
|
|
290
|
+
|
|
291
|
+
if kind == "snapshot":
|
|
292
|
+
issue = _exact_keys(message, ("type", "snapshot"))
|
|
293
|
+
if issue:
|
|
294
|
+
return _malformed(issue)
|
|
295
|
+
bad = _check_snapshot(message["snapshot"], limits)
|
|
296
|
+
return bad if bad is not None else ParseResult(ok=True, message=message)
|
|
297
|
+
|
|
298
|
+
if kind == "get-tree-result":
|
|
299
|
+
issue = _exact_keys(message, ("type", "requestId"), ("snapshot", "error"))
|
|
300
|
+
if issue:
|
|
301
|
+
return _malformed(issue)
|
|
302
|
+
issue = _index(message["requestId"], "requestId")
|
|
303
|
+
if issue:
|
|
304
|
+
return _malformed(issue)
|
|
305
|
+
has_snapshot = "snapshot" in message
|
|
306
|
+
has_error = "error" in message
|
|
307
|
+
if has_snapshot == has_error:
|
|
308
|
+
return _malformed("exactly one of snapshot or error must be present")
|
|
309
|
+
if has_error:
|
|
310
|
+
issue = _identifier(message["error"], "error", allow_empty=True)
|
|
311
|
+
return _malformed(issue) if issue else ParseResult(ok=True, message=message)
|
|
312
|
+
bad = _check_snapshot(message["snapshot"], limits)
|
|
313
|
+
return bad if bad is not None else ParseResult(ok=True, message=message)
|
|
314
|
+
|
|
315
|
+
if kind == "tree-delta":
|
|
316
|
+
bad = _check_tree_delta(message, limits)
|
|
317
|
+
return bad if bad is not None else ParseResult(ok=True, message=message)
|
|
318
|
+
|
|
319
|
+
if kind == "log":
|
|
320
|
+
issue = _exact_keys(message, ("type", "record"))
|
|
321
|
+
if issue:
|
|
322
|
+
return _malformed(issue)
|
|
323
|
+
bad = _check_log_record(message["record"], limits)
|
|
324
|
+
return bad if bad is not None else ParseResult(ok=True, message=message)
|
|
325
|
+
|
|
326
|
+
if kind == "error":
|
|
327
|
+
issue = _check_error(message)
|
|
328
|
+
return _malformed(issue) if issue else ParseResult(ok=True, message=message)
|
|
329
|
+
|
|
330
|
+
return _malformed("unknown or missing message type")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def parse_driver_message(value: Any, limits: ProtocolLimits = DEFAULT_LIMITS) -> ParseResult:
|
|
334
|
+
"""Parse one driver → adapter message. Never raises.
|
|
335
|
+
|
|
336
|
+
Driver traffic is read tolerantly: unknown fields in the envelope and in
|
|
337
|
+
the driver's nested objects (``marker``, ``logs``, ``limits``) are ignored
|
|
338
|
+
and passed through to the caller, so a newer driver can add a field without
|
|
339
|
+
breaking an adapter that was published before it existed.
|
|
340
|
+
|
|
341
|
+
The asymmetry is about who is speaking, not about the message. Adapter
|
|
342
|
+
traffic crosses an untrusted boundary, where an unknown field is a signal
|
|
343
|
+
rather than an extension, so :func:`parse_adapter_message` stays strict.
|
|
344
|
+
Tolerance is not leniency either: known fields keep their types, and the
|
|
345
|
+
closed sets (message types, error codes, ``subscribe``, roles, actions)
|
|
346
|
+
stay closed in both directions.
|
|
347
|
+
"""
|
|
348
|
+
projected = _project(value, limits)
|
|
349
|
+
if not projected.ok:
|
|
350
|
+
return projected
|
|
351
|
+
message = projected.message
|
|
352
|
+
if not isinstance(message, dict) or not isinstance(message.get("type"), str):
|
|
353
|
+
return _malformed("unknown or missing message type")
|
|
354
|
+
|
|
355
|
+
kind = message["type"]
|
|
356
|
+
if kind == "hello-ack":
|
|
357
|
+
protocol = message.get("protocol")
|
|
358
|
+
if isinstance(protocol, str) and protocol not in (PROTOCOL_ID, PROTOCOL_V2_ID):
|
|
359
|
+
return ParseResult(ok=False, code="bad-version", detail=f"unsupported protocol {protocol}")
|
|
360
|
+
issue = _required_keys(
|
|
361
|
+
message, ("type", "protocol", "sessionId", "limits", "subscribe", "marker")
|
|
362
|
+
)
|
|
363
|
+
if issue:
|
|
364
|
+
return _malformed(issue)
|
|
365
|
+
if message["protocol"] not in (PROTOCOL_ID, PROTOCOL_V2_ID):
|
|
366
|
+
return _malformed("protocol: expected termwright/1 or termwright/2")
|
|
367
|
+
issue = _identifier(message["sessionId"], "sessionId")
|
|
368
|
+
if issue:
|
|
369
|
+
return _malformed(issue)
|
|
370
|
+
limits_value = message["limits"]
|
|
371
|
+
if not isinstance(limits_value, dict):
|
|
372
|
+
return _malformed("limits: expected an object")
|
|
373
|
+
# Required keys must all be present, but unknown ones are ignored:
|
|
374
|
+
# `limits` is the one object on the wire that grows between versions,
|
|
375
|
+
# and a client that rejected a ceiling it had never heard of would
|
|
376
|
+
# drop the channel every time the protocol gained one.
|
|
377
|
+
issue = _required_keys(limits_value, LIMIT_FIELDS)
|
|
378
|
+
if issue:
|
|
379
|
+
return _malformed(f"limits: {issue}")
|
|
380
|
+
for field in LIMIT_FIELDS:
|
|
381
|
+
issue = _revision(limits_value[field], f"limits.{field}")
|
|
382
|
+
if issue:
|
|
383
|
+
return _malformed(issue)
|
|
384
|
+
if message["subscribe"] not in SUBSCRIBE_MODES:
|
|
385
|
+
return _malformed("subscribe: expected 'snapshots', 'revisions' or 'diffs'")
|
|
386
|
+
marker = message["marker"]
|
|
387
|
+
if not isinstance(marker, dict):
|
|
388
|
+
return _malformed("marker: expected an object")
|
|
389
|
+
issue = _required_keys(marker, ("enabled",))
|
|
390
|
+
if issue:
|
|
391
|
+
return _malformed(f"marker: {issue}")
|
|
392
|
+
if not isinstance(marker["enabled"], bool):
|
|
393
|
+
return _malformed("marker.enabled: expected a boolean")
|
|
394
|
+
if "logs" in message:
|
|
395
|
+
issue = _check_log_budget(message["logs"])
|
|
396
|
+
if issue:
|
|
397
|
+
return _malformed(issue)
|
|
398
|
+
return ParseResult(ok=True, message=message)
|
|
399
|
+
|
|
400
|
+
if kind == "get-tree":
|
|
401
|
+
issue = _required_keys(message, ("type", "requestId"))
|
|
402
|
+
if issue:
|
|
403
|
+
return _malformed(issue)
|
|
404
|
+
issue = _index(message["requestId"], "requestId")
|
|
405
|
+
if issue:
|
|
406
|
+
return _malformed(issue)
|
|
407
|
+
if "revision" in message:
|
|
408
|
+
issue = _revision(message["revision"], "revision")
|
|
409
|
+
if issue:
|
|
410
|
+
return _malformed(issue)
|
|
411
|
+
return ParseResult(ok=True, message=message)
|
|
412
|
+
|
|
413
|
+
if kind == "error":
|
|
414
|
+
issue = _check_error(message, strict=False)
|
|
415
|
+
return _malformed(issue) if issue else ParseResult(ok=True, message=message)
|
|
416
|
+
|
|
417
|
+
return _malformed("unknown or missing message type")
|
termwright/roles.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Closed vocabularies. Unknown members are rejected, never passed through."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
SEMANTIC_ROLES = (
|
|
6
|
+
"application",
|
|
7
|
+
"region",
|
|
8
|
+
"dialog",
|
|
9
|
+
"alert",
|
|
10
|
+
"status",
|
|
11
|
+
"list",
|
|
12
|
+
"listitem",
|
|
13
|
+
"menu",
|
|
14
|
+
"menuitem",
|
|
15
|
+
"button",
|
|
16
|
+
"checkbox",
|
|
17
|
+
"radio",
|
|
18
|
+
"tab",
|
|
19
|
+
"textbox",
|
|
20
|
+
"heading",
|
|
21
|
+
"text",
|
|
22
|
+
"progressbar",
|
|
23
|
+
"separator",
|
|
24
|
+
"scrollbar",
|
|
25
|
+
"table",
|
|
26
|
+
"row",
|
|
27
|
+
"cell",
|
|
28
|
+
"generic",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
SEMANTIC_ACTIONS = (
|
|
32
|
+
"focus",
|
|
33
|
+
"activate",
|
|
34
|
+
"toggle",
|
|
35
|
+
"setValue",
|
|
36
|
+
"scroll",
|
|
37
|
+
"select",
|
|
38
|
+
"expand",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
ADAPTER_CAPABILITIES = (
|
|
42
|
+
"tree",
|
|
43
|
+
"bounds",
|
|
44
|
+
"absolute-bounds",
|
|
45
|
+
"states",
|
|
46
|
+
"actions",
|
|
47
|
+
"text-ranges",
|
|
48
|
+
"render-revisions",
|
|
49
|
+
"tree-diffs",
|
|
50
|
+
"logs",
|
|
51
|
+
"qualified-observations",
|
|
52
|
+
"pointer-hit-grid",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
ROLE_SET = frozenset(SEMANTIC_ROLES)
|
|
56
|
+
ACTION_SET = frozenset(SEMANTIC_ACTIONS)
|
|
57
|
+
CAPABILITY_SET = frozenset(ADAPTER_CAPABILITIES)
|