bounded-embodiment 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 (41) hide show
  1. bounded_embodiment/__init__.py +2 -0
  2. bounded_embodiment/canonical.py +311 -0
  3. bounded_embodiment/cli.py +399 -0
  4. bounded_embodiment/commandspace.py +354 -0
  5. bounded_embodiment/envelope/__init__.py +11 -0
  6. bounded_embodiment/envelope/convert.py +221 -0
  7. bounded_embodiment/envelope/geometry.py +261 -0
  8. bounded_embodiment/envelope/load.py +384 -0
  9. bounded_embodiment/envelope/model.py +226 -0
  10. bounded_embodiment/findings.py +28 -0
  11. bounded_embodiment/generator.py +689 -0
  12. bounded_embodiment/profiles/ev03-illustrative-0.1.json +31 -0
  13. bounded_embodiment/profiles/ev10-illustrative-0.1.json +23 -0
  14. bounded_embodiment/profiles.py +62 -0
  15. bounded_embodiment/rawdata.py +543 -0
  16. bounded_embodiment/report/__init__.py +67 -0
  17. bounded_embodiment/report/build.py +253 -0
  18. bounded_embodiment/report/cli.py +150 -0
  19. bounded_embodiment/report/derive.py +805 -0
  20. bounded_embodiment/report/files.py +339 -0
  21. bounded_embodiment/report/streams.py +326 -0
  22. bounded_embodiment/report/verify.py +549 -0
  23. bounded_embodiment/report/waypoints.py +164 -0
  24. bounded_embodiment/schema.py +203 -0
  25. bounded_embodiment/schemas/command-space-0.1.json +95 -0
  26. bounded_embodiment/schemas/envelope-0.1.json +151 -0
  27. bounded_embodiment/schemas/envelope-0.2.json +273 -0
  28. bounded_embodiment/schemas/gate-decision-0.1.json +54 -0
  29. bounded_embodiment/schemas/key-0.1.json +63 -0
  30. bounded_embodiment/schemas/test-report-0.1.json +951 -0
  31. bounded_embodiment/schemas/waypoints-0.1.json +59 -0
  32. bounded_embodiment/scoring/__init__.py +49 -0
  33. bounded_embodiment/scoring/common.py +500 -0
  34. bounded_embodiment/scoring/ev03.py +641 -0
  35. bounded_embodiment/scoring/ev10.py +246 -0
  36. bounded_embodiment/scoring/verdict.py +490 -0
  37. bounded_embodiment/signing.py +430 -0
  38. bounded_embodiment-0.1.0.dist-info/METADATA +34 -0
  39. bounded_embodiment-0.1.0.dist-info/RECORD +41 -0
  40. bounded_embodiment-0.1.0.dist-info/WHEEL +4 -0
  41. bounded_embodiment-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,2 @@
1
+ """The Bounded Embodiment test method, executable."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,311 @@
1
+ """RFC 8785 JSON Canonicalization Scheme (JCS), and a strict JSON loader.
2
+
3
+ ``canonicalize(obj)`` returns the canonical UTF-8 bytes of a JSON value:
4
+
5
+ * no whitespace;
6
+ * object members sorted by the UTF-16 code units of their names;
7
+ * strings escaped exactly as ECMAScript ``JSON.stringify`` escapes them
8
+ (``\\" \\\\ \\b \\f \\n \\r \\t``, ``\\u00xx`` in lower-case hex for the
9
+ other C0 controls, everything else as literal UTF-8);
10
+ * numbers serialized as ECMAScript ``Number.prototype.toString`` does
11
+ (RFC 8785 section 3.2.2.3), so ``5e-05`` becomes ``0.00005``, ``1e21``
12
+ becomes ``1e+21`` and ``-0.0`` becomes ``0``.
13
+
14
+ Python ``int`` values are exact, so an integer above 2**53 in magnitude cannot
15
+ be carried by an IEEE 754 double without possibly changing it; it raises
16
+ ``CanonicalError`` instead of being rounded. A ``float`` is already a double and
17
+ is serialized whatever its magnitude. NaN, the infinities, lone surrogates and
18
+ non-string object keys all raise ``CanonicalError``.
19
+
20
+ ``loads_strict(data)`` parses JSON text and rejects what would make two
21
+ readers disagree about a document: duplicate object member names at any
22
+ depth, the non-JSON literals ``NaN``/``Infinity``/``-Infinity``, numbers that
23
+ overflow a double, lone surrogates, invalid UTF-8 and a leading byte order mark.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import hashlib
29
+ import json
30
+ import math
31
+ import re
32
+ import sys
33
+ from collections.abc import Mapping
34
+ from decimal import ROUND_CEILING, ROUND_FLOOR, Decimal, localcontext
35
+
36
+ __all__ = ["MAX_EXACT_INT", "CanonicalError", "canonicalize", "loads_strict", "sha256_jcs"]
37
+
38
+ MAX_EXACT_INT = 2**53
39
+ """Largest integer magnitude ``canonicalize`` accepts for a Python ``int``."""
40
+
41
+
42
+ class CanonicalError(ValueError):
43
+ """A value cannot be canonicalized, or JSON text is not acceptable."""
44
+
45
+
46
+ # --------------------------------------------------------------------------- strings
47
+
48
+ _SURROGATE = re.compile("[\ud800-\udfff]")
49
+ _NEEDS_ESCAPE = re.compile('[\\x00-\\x1f"\\\\]')
50
+ _ESCAPES = {
51
+ '"': '\\"',
52
+ "\\": "\\\\",
53
+ "\b": "\\b",
54
+ "\f": "\\f",
55
+ "\n": "\\n",
56
+ "\r": "\\r",
57
+ "\t": "\\t",
58
+ }
59
+ for _code in range(0x20):
60
+ _ESCAPES.setdefault(chr(_code), f"\\u{_code:04x}")
61
+ del _code
62
+
63
+
64
+ def _escape_match(match: re.Match[str]) -> str:
65
+ return _ESCAPES[match.group()]
66
+
67
+
68
+ def _string(s: str) -> str:
69
+ found = _SURROGATE.search(s)
70
+ if found is not None:
71
+ raise CanonicalError(
72
+ f"string contains a lone surrogate U+{ord(found.group()):04X} at index {found.start()}"
73
+ )
74
+ return '"' + _NEEDS_ESCAPE.sub(_escape_match, s) + '"'
75
+
76
+
77
+ def _utf16_sort_key(name: str) -> bytes:
78
+ # Big-endian UTF-16 bytes compare exactly as sequences of UTF-16 code units.
79
+ return name.encode("utf-16-be")
80
+
81
+
82
+ # --------------------------------------------------------------------------- numbers
83
+
84
+
85
+ def _shortest_digits_repr(x: float) -> tuple[str, int]:
86
+ """Shortest round-trip digits of a positive finite double, via CPython's repr.
87
+
88
+ Returns ``(s, n)`` with ``s`` the decimal digits (no leading or trailing
89
+ zeros) and ``x == 0.s * 10**n``.
90
+ """
91
+ text = float.__repr__(x)
92
+ mantissa, _, exp = text.partition("e")
93
+ int_part, _, frac_part = mantissa.partition(".")
94
+ digits = (int_part + frac_part).lstrip("0")
95
+ s = digits.rstrip("0")
96
+ point = (int(exp) if exp else 0) - len(frac_part) + (len(digits) - len(s))
97
+ return s, point + len(s)
98
+
99
+
100
+ def _shortest_digits_decimal(x: float) -> tuple[str, int]:
101
+ """The same as ``_shortest_digits_repr`` without relying on repr.
102
+
103
+ Used only where ``sys.float_repr_style`` is not ``"short"``. For each
104
+ precision k from 1 to 17 it takes the two k-digit decimals around x, keeps
105
+ those that read back as x, and picks the closer one (the even one on a
106
+ tie), which is the choice ECMAScript Number::toString specifies.
107
+ """
108
+ exact = Decimal(x)
109
+ for k in range(1, 18):
110
+ candidates = []
111
+ for rounding in (ROUND_FLOOR, ROUND_CEILING):
112
+ with localcontext() as ctx:
113
+ ctx.prec = k
114
+ ctx.rounding = rounding
115
+ c = +exact
116
+ if float(c) == x and c not in candidates:
117
+ candidates.append(c)
118
+ if not candidates:
119
+ continue
120
+
121
+ def rank(c: Decimal, k: int = k) -> tuple[Decimal, int]:
122
+ with localcontext() as ctx:
123
+ ctx.prec = 2000 # exact: a double has at most 767 significant digits
124
+ distance = abs(c - exact)
125
+ significand = int(c.scaleb(k - 1 - c.adjusted()))
126
+ return (distance, significand % 2)
127
+
128
+ best = min(candidates, key=rank)
129
+ sign, digit_tuple, exponent = best.as_tuple()
130
+ s = "".join(map(str, digit_tuple)).lstrip("0")
131
+ stripped = s.rstrip("0")
132
+ point = int(exponent) + (len(s) - len(stripped))
133
+ return stripped, point + len(stripped)
134
+ raise AssertionError(f"no round-trip representation found for {x!r}") # pragma: no cover
135
+
136
+
137
+ _shortest_digits = (
138
+ _shortest_digits_repr if sys.float_repr_style == "short" else _shortest_digits_decimal
139
+ )
140
+
141
+
142
+ def _format_positive(x: float) -> str:
143
+ s, n = _shortest_digits(x)
144
+ k = len(s)
145
+ if k <= n <= 21:
146
+ return s + "0" * (n - k)
147
+ if 0 < n <= 21:
148
+ return s[:n] + "." + s[n:]
149
+ if -6 < n <= 0:
150
+ return "0." + "0" * (-n) + s
151
+ e = n - 1
152
+ sign = "+" if e >= 0 else "-"
153
+ if k == 1:
154
+ return f"{s}e{sign}{abs(e)}"
155
+ return f"{s[0]}.{s[1:]}e{sign}{abs(e)}"
156
+
157
+
158
+ def _number(x: float) -> str:
159
+ """ECMAScript Number::toString(x) for a finite double."""
160
+ if math.isnan(x) or math.isinf(x):
161
+ raise CanonicalError(f"{x!r} is not a JSON number")
162
+ if x == 0:
163
+ return "0" # also -0.0
164
+ if x < 0:
165
+ return "-" + _format_positive(-x)
166
+ return _format_positive(x)
167
+
168
+
169
+ def _integer(n: int) -> str:
170
+ if n > MAX_EXACT_INT or n < -MAX_EXACT_INT:
171
+ raise CanonicalError(
172
+ f"integer {n} is larger than 2**53 in magnitude and cannot be represented "
173
+ "exactly as an IEEE 754 double"
174
+ )
175
+ return str(int(n))
176
+
177
+
178
+ # --------------------------------------------------------------------------- values
179
+
180
+
181
+ def _serialize(value: object, out: list[str]) -> None:
182
+ if value is None:
183
+ out.append("null")
184
+ elif value is True:
185
+ out.append("true")
186
+ elif value is False:
187
+ out.append("false")
188
+ elif isinstance(value, str):
189
+ out.append(_string(value))
190
+ elif isinstance(value, int): # bool cannot be subclassed, so True/False never get here
191
+ out.append(_integer(value))
192
+ elif isinstance(value, float):
193
+ out.append(_number(float(value)))
194
+ elif isinstance(value, Mapping):
195
+ names = list(value.keys())
196
+ for name in names:
197
+ if not isinstance(name, str):
198
+ raise CanonicalError(f"object member name {name!r} is not a string")
199
+ try:
200
+ names.sort(key=_utf16_sort_key)
201
+ except UnicodeEncodeError:
202
+ bad = next(n for n in names if _SURROGATE.search(n))
203
+ raise CanonicalError(f"object member name {bad!r} contains a lone surrogate") from None
204
+ out.append("{")
205
+ for i, name in enumerate(names):
206
+ if i:
207
+ out.append(",")
208
+ out.append(_string(name))
209
+ out.append(":")
210
+ _serialize(value[name], out)
211
+ out.append("}")
212
+ elif isinstance(value, (list, tuple)):
213
+ out.append("[")
214
+ for i, item in enumerate(value):
215
+ if i:
216
+ out.append(",")
217
+ _serialize(item, out)
218
+ out.append("]")
219
+ else:
220
+ raise CanonicalError(f"{type(value).__name__} is not a JSON type")
221
+
222
+
223
+ def canonicalize(obj: object) -> bytes:
224
+ """Return the RFC 8785 canonical UTF-8 bytes of a JSON-compatible value."""
225
+ out: list[str] = []
226
+ try:
227
+ _serialize(obj, out)
228
+ except RecursionError:
229
+ raise CanonicalError("value is nested too deeply, or contains a cycle") from None
230
+ return "".join(out).encode("utf-8")
231
+
232
+
233
+ def sha256_jcs(obj: object) -> str:
234
+ """``"sha256:"`` followed by the lower-hex SHA-256 of ``canonicalize(obj)``."""
235
+ return "sha256:" + hashlib.sha256(canonicalize(obj)).hexdigest()
236
+
237
+
238
+ # --------------------------------------------------------------------------- strict loading
239
+
240
+
241
+ def _reject_constant(name: str) -> object:
242
+ raise CanonicalError(f"{name} is not valid JSON")
243
+
244
+
245
+ def _parse_float(text: str) -> float:
246
+ value = float(text)
247
+ if math.isinf(value):
248
+ raise CanonicalError(f"number {text} is out of range for an IEEE 754 double")
249
+ return value
250
+
251
+
252
+ def _no_duplicates(pairs: list[tuple[str, object]]) -> dict:
253
+ obj: dict = {}
254
+ for name, value in pairs:
255
+ if name in obj:
256
+ raise CanonicalError(f"duplicate object member name {name!r}")
257
+ obj[name] = value
258
+ return obj
259
+
260
+
261
+ def _check_strings(root: object) -> None:
262
+ stack = [root]
263
+ while stack:
264
+ value = stack.pop()
265
+ if isinstance(value, str):
266
+ if _SURROGATE.search(value):
267
+ raise CanonicalError(f"string {value!r} contains a lone surrogate")
268
+ elif isinstance(value, dict):
269
+ for name, item in value.items():
270
+ if _SURROGATE.search(name):
271
+ raise CanonicalError(f"object member name {name!r} contains a lone surrogate")
272
+ stack.append(item)
273
+ elif isinstance(value, list):
274
+ stack.extend(value)
275
+
276
+
277
+ def loads_strict(data: bytes | str) -> object:
278
+ """Parse JSON text strictly.
279
+
280
+ Bytes must be UTF-8. Rejects, with ``CanonicalError``: duplicate member
281
+ names at any depth (compared after unescaping), ``NaN``, ``Infinity``,
282
+ ``-Infinity``, numbers that overflow a double, lone surrogates (escaped or
283
+ raw), a leading byte order mark, and anything that is not JSON. Integers
284
+ come back as ``int`` and other numbers as ``float``.
285
+ """
286
+ if isinstance(data, (bytes, bytearray, memoryview)):
287
+ try:
288
+ text = bytes(data).decode("utf-8")
289
+ except UnicodeDecodeError as exc:
290
+ raise CanonicalError(f"JSON text is not valid UTF-8: {exc}") from None
291
+ elif isinstance(data, str):
292
+ text = data
293
+ else:
294
+ raise TypeError(f"loads_strict expects bytes or str, not {type(data).__name__}")
295
+ if text.startswith("\ufeff"):
296
+ raise CanonicalError("JSON text starts with a byte order mark (U+FEFF); remove it")
297
+ try:
298
+ value = json.loads(
299
+ text,
300
+ object_pairs_hook=_no_duplicates,
301
+ parse_constant=_reject_constant,
302
+ parse_float=_parse_float,
303
+ )
304
+ except CanonicalError:
305
+ raise
306
+ except RecursionError:
307
+ raise CanonicalError("JSON text is nested too deeply") from None
308
+ except ValueError as exc:
309
+ raise CanonicalError(f"invalid JSON: {exc}") from None
310
+ _check_strings(value)
311
+ return value