datumesh 0.2.0__tar.gz → 0.3.0__tar.gz

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 (36) hide show
  1. {datumesh-0.2.0/src/datumesh.egg-info → datumesh-0.3.0}/PKG-INFO +8 -8
  2. {datumesh-0.2.0 → datumesh-0.3.0}/README.md +4 -4
  3. {datumesh-0.2.0 → datumesh-0.3.0}/pyproject.toml +4 -4
  4. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/__init__.py +3 -3
  5. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_envelope.py +9 -12
  6. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_errors.py +3 -1
  7. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_json.py +36 -6
  8. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_keys.py +7 -5
  9. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_lexical.py +2 -2
  10. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_unitlist.py +2 -2
  11. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_uri.py +16 -8
  12. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_verify.py +46 -20
  13. {datumesh-0.2.0 → datumesh-0.3.0/src/datumesh.egg-info}/PKG-INFO +8 -8
  14. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh.egg-info/SOURCES.txt +2 -0
  15. {datumesh-0.2.0 → datumesh-0.3.0}/tests/test_api.py +11 -8
  16. {datumesh-0.2.0 → datumesh-0.3.0}/tests/test_conformance.py +10 -10
  17. {datumesh-0.2.0 → datumesh-0.3.0}/tests/test_envelope.py +6 -5
  18. {datumesh-0.2.0 → datumesh-0.3.0}/tests/test_jcs.py +7 -2
  19. {datumesh-0.2.0 → datumesh-0.3.0}/tests/test_keys.py +15 -3
  20. datumesh-0.3.0/tests/test_spec_0_2_1.py +198 -0
  21. datumesh-0.3.0/tests/test_spec_0_3_0.py +274 -0
  22. {datumesh-0.2.0 → datumesh-0.3.0}/tests/test_unicode.py +0 -1
  23. {datumesh-0.2.0 → datumesh-0.3.0}/tests/test_uri.py +4 -3
  24. {datumesh-0.2.0 → datumesh-0.3.0}/LICENSE +0 -0
  25. {datumesh-0.2.0 → datumesh-0.3.0}/setup.cfg +0 -0
  26. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_model.py +0 -0
  27. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_ucd15.py +0 -0
  28. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_unicode.py +0 -0
  29. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/_units.py +0 -0
  30. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh/py.typed +0 -0
  31. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh.egg-info/dependency_links.txt +0 -0
  32. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh.egg-info/requires.txt +0 -0
  33. {datumesh-0.2.0 → datumesh-0.3.0}/src/datumesh.egg-info/top_level.txt +0 -0
  34. {datumesh-0.2.0 → datumesh-0.3.0}/tests/test_bitflips.py +0 -0
  35. {datumesh-0.2.0 → datumesh-0.3.0}/tests/test_blob.py +0 -0
  36. {datumesh-0.2.0 → datumesh-0.3.0}/tests/test_units.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datumesh
3
- Version: 0.2.0
4
- Summary: Verify Datumesh receipts (spec-0.2.0): RFC 8785 canonical JSON, Ed25519 signatures, blobs, units, cell URIs, response envelopes, key documents.
3
+ Version: 0.3.0
4
+ Summary: Verify Datumesh receipts (spec-0.3.0): RFC 8785 canonical JSON, Ed25519 signatures, blobs, units, cell URIs, response envelopes, key documents.
5
5
  Author: Datumesh
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://datumesh.com
8
8
  Project-URL: Repository, https://github.com/datumesh-com/datumesh-verify-py
9
- Project-URL: Specification (spec-0.2.0), https://github.com/datumesh-com/datumesh-spec/blob/spec-0.2.0/SPEC.md
10
- Keywords: datumesh,spec-0.2.0,receipt,verification,ed25519,rfc8785,jcs,canonical-json
9
+ Project-URL: Specification (spec-0.3.0), https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md
10
+ Keywords: datumesh,spec-0.3.0,receipt,verification,ed25519,rfc8785,jcs,canonical-json
11
11
  Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Operating System :: OS Independent
@@ -27,11 +27,11 @@ Dynamic: license-file
27
27
 
28
28
  # datumesh
29
29
 
30
- Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.2.0/SPEC.md) (revision `spec-0.2.0`) defines. It also checks a mesh's response envelope and resolves the unit of a cell's value.
30
+ Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md) (revision `spec-0.3.0`) defines. It also checks a mesh's response envelope and resolves the unit of a cell's value.
31
31
 
32
32
  ```sh
33
33
  pip install datumesh
34
- git clone --depth 1 --branch spec-0.2.0 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
34
+ git clone --depth 1 --branch spec-0.3.0 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
35
35
  ```
36
36
 
37
37
  ```python
@@ -49,8 +49,8 @@ Website: https://datumesh.com · Specification: https://github.com/datumesh-com/
49
49
 
50
50
  ## API
51
51
 
52
- - `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor, the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
53
- - `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one; a stale receipt may answer an earlier `asof`). The envelope itself is unsigned: `stale`, `superseded_by` and a `verified` label remain the mesh's claims.
52
+ - `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. One code comes with success: when you pass `recompute` without a `value` because you could not evaluate the function, the other recomputation checks still run, and if they pass the result is ok at level `signed` with `code` `"E_EVAL_UNAVAILABLE"` (SPEC 10.1 step 7.6). It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor (leave it out if you have none; `None` is a JSON null and fails the hash check), the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
53
+ - `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one; the receipt's `asof` must be at or before it, for `ok` and `stale` alike). The envelope itself is unsigned: `stale`, `superseded_by`, a `verified` label and the currency `ok` asserts remain the mesh's claims.
54
54
  - `resolved_unit(descriptor, cell) -> str | None`: the unit of a cell's value, such as `USD/EUR` (SPEC 7.1, units.md).
55
55
  - `keys_from_response(host, status, body) -> KeysResult`: interprets a response for `https://<host>/.well-known/datumesh.json` (SPEC 9.3).
56
56
  - `fetch_keys(host, timeout=10.0) -> KeysResult`: the only function that uses the network. One GET, TLS verified, redirects not followed.
@@ -1,10 +1,10 @@
1
1
  # datumesh
2
2
 
3
- Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.2.0/SPEC.md) (revision `spec-0.2.0`) defines. It also checks a mesh's response envelope and resolves the unit of a cell's value.
3
+ Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md) (revision `spec-0.3.0`) defines. It also checks a mesh's response envelope and resolves the unit of a cell's value.
4
4
 
5
5
  ```sh
6
6
  pip install datumesh
7
- git clone --depth 1 --branch spec-0.2.0 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
7
+ git clone --depth 1 --branch spec-0.3.0 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
8
8
  ```
9
9
 
10
10
  ```python
@@ -22,8 +22,8 @@ Website: https://datumesh.com · Specification: https://github.com/datumesh-com/
22
22
 
23
23
  ## API
24
24
 
25
- - `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor, the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
26
- - `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one; a stale receipt may answer an earlier `asof`). The envelope itself is unsigned: `stale`, `superseded_by` and a `verified` label remain the mesh's claims.
25
+ - `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. One code comes with success: when you pass `recompute` without a `value` because you could not evaluate the function, the other recomputation checks still run, and if they pass the result is ok at level `signed` with `code` `"E_EVAL_UNAVAILABLE"` (SPEC 10.1 step 7.6). It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor (leave it out if you have none; `None` is a JSON null and fails the hash check), the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
26
+ - `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one; the receipt's `asof` must be at or before it, for `ok` and `stale` alike). The envelope itself is unsigned: `stale`, `superseded_by`, a `verified` label and the currency `ok` asserts remain the mesh's claims.
27
27
  - `resolved_unit(descriptor, cell) -> str | None`: the unit of a cell's value, such as `USD/EUR` (SPEC 7.1, units.md).
28
28
  - `keys_from_response(host, status, body) -> KeysResult`: interprets a response for `https://<host>/.well-known/datumesh.json` (SPEC 9.3).
29
29
  - `fetch_keys(host, timeout=10.0) -> KeysResult`: the only function that uses the network. One GET, TLS verified, redirects not followed.
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datumesh"
7
- version = "0.2.0"
8
- description = "Verify Datumesh receipts (spec-0.2.0): RFC 8785 canonical JSON, Ed25519 signatures, blobs, units, cell URIs, response envelopes, key documents."
7
+ version = "0.3.0"
8
+ description = "Verify Datumesh receipts (spec-0.3.0): RFC 8785 canonical JSON, Ed25519 signatures, blobs, units, cell URIs, response envelopes, key documents."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
11
  license = "Apache-2.0"
12
12
  license-files = ["LICENSE"]
13
13
  authors = [{ name = "Datumesh" }]
14
- keywords = ["datumesh", "spec-0.2.0", "receipt", "verification", "ed25519", "rfc8785", "jcs", "canonical-json"]
14
+ keywords = ["datumesh", "spec-0.3.0", "receipt", "verification", "ed25519", "rfc8785", "jcs", "canonical-json"]
15
15
  classifiers = [
16
16
  "Development Status :: 3 - Alpha",
17
17
  "Intended Audience :: Developers",
@@ -30,7 +30,7 @@ dependencies = ["pynacl>=1.5.0"]
30
30
  [project.urls]
31
31
  Homepage = "https://datumesh.com"
32
32
  Repository = "https://github.com/datumesh-com/datumesh-verify-py"
33
- "Specification (spec-0.2.0)" = "https://github.com/datumesh-com/datumesh-spec/blob/spec-0.2.0/SPEC.md"
33
+ "Specification (spec-0.3.0)" = "https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md"
34
34
 
35
35
  [tool.setuptools.packages.find]
36
36
  where = ["src"]
@@ -1,6 +1,6 @@
1
1
  """datumesh: verify Datumesh receipts (signed answers for addressable data cells).
2
2
 
3
- Implements the Datumesh protocol specification, revision ``spec-0.2.0``
3
+ Implements the Datumesh protocol specification, revision ``spec-0.3.0``
4
4
  (https://github.com/datumesh-com/datumesh-spec): RFC 8785 canonical JSON,
5
5
  SHA-256 hashes, Ed25519 receipt signatures, blob content, units, cell URIs,
6
6
  response envelopes and key documents. Only :func:`fetch_keys` touches the network.
@@ -19,9 +19,9 @@ from ._keys import Key, KeysResult, fetch_keys, key_id, keys_from_response
19
19
  from ._uri import format_uri, parse_uri, uri_to_https
20
20
  from ._verify import VerifyResult, load, payload_bytes, verify
21
21
 
22
- __version__ = "0.2.0"
22
+ __version__ = "0.3.0"
23
23
  #: The specification revision this package conforms to.
24
- SPEC_VERSION = "spec-0.2.0"
24
+ SPEC_VERSION = "spec-0.3.0"
25
25
  #: Every literal unit listed in units.md (index units, ``index:<period>=<value>``, are a pattern and not listed).
26
26
  UNITS = tuple(sorted(_units.LITERALS))
27
27
  #: The currency codes of units.md 2.1.
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import hashlib
6
6
  import re
7
7
  from dataclasses import dataclass
8
- from typing import Any, Dict, Mapping, Optional, Tuple, Union
8
+ from typing import Any, Dict, Mapping, NoReturn, Optional, Tuple, Union
9
9
 
10
10
  from . import _lexical as lx
11
11
  from . import _units
@@ -59,7 +59,7 @@ class EnvelopeResult:
59
59
  path: Optional[str]
60
60
 
61
61
 
62
- def _bad(detail: str, path: str) -> None:
62
+ def _bad(detail: str, path: str) -> NoReturn:
63
63
  fail("E_ENVELOPE", detail, path)
64
64
 
65
65
 
@@ -183,8 +183,8 @@ def check_envelope(status: Any, body: Union[str, bytes, bytearray, memoryview],
183
183
  of 5.5 (``E_ENVELOPE``); the status matches the outcome (``E_ENVELOPE``); an
184
184
  embedded receipt is verified through step 6 of 10.1 with ``keys`` (the forms
185
185
  :func:`verify` takes) and keeps that procedure's codes; the receipt answers
186
- the request: same cell id, and when an ``asof`` was requested the same ``asof``
187
- for ``ok`` or one at or before it for ``stale`` (``E_ENVELOPE``); then ``trust`` is checked against the receipt (``primary``
186
+ the request: same cell id, and when an ``asof`` was requested a receipt
187
+ ``asof`` at or before it, for ``ok`` and ``stale`` alike (``E_ENVELOPE``); then ``trust`` is checked against the receipt (``primary``
188
188
  exactly when the signer's host is the namespace's host and there are no
189
189
  inputs) and ``superseded_by`` must not be the receipt's own hash (``E_ENVELOPE``).
190
190
 
@@ -214,14 +214,11 @@ def check_envelope(status: Any, body: Union[str, bytes, bytearray, memoryview],
214
214
  raise DatumeshError(e.code, e.detail, path) from None
215
215
  if hashlib.sha256(jcs_bytes(r["cell"])).hexdigest() != want_cell:
216
216
  _bad("the receipt's cell is not the cell requested", "$.receipt.cell")
217
- # ok: the same asof; stale: at or before it (instants compare as strings, SPEC 3.3).
218
- if want_asof is not None:
219
- if outcome == "stale" and not r["asof"] <= want_asof:
220
- _bad(f"a stale receipt answers as of {r['asof']}, after the requested {want_asof}",
221
- "$.receipt.asof")
222
- if outcome == "ok" and r["asof"] != want_asof:
223
- _bad(f"the receipt answers as of {r['asof']}, the request asked for {want_asof}",
224
- "$.receipt.asof")
217
+ # ok and stale alike: the receipt's asof at or before the requested one (D31, spec-0.3.0;
218
+ # canonical instants compare as strings, SPEC 3.3). No requested asof: any.
219
+ if want_asof is not None and r["asof"] > want_asof:
220
+ _bad(f"the {outcome} receipt answers as of {r['asof']}, after the requested {want_asof}",
221
+ "$.receipt.asof")
225
222
  trust = env["trust"]
226
223
  if (trust == "primary") != _is_primary(r):
227
224
  _bad("trust is primary exactly when the signer's host is the namespace's host and there are no "
@@ -4,7 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from typing import NoReturn, Optional
6
6
 
7
- #: Every error code SPEC.md 10.2 defines, in the order of its table.
7
+ #: Every error code SPEC.md 10.2 defines, in the order of its table. One, ``E_EVAL_UNAVAILABLE``,
8
+ #: is not a failure: :func:`verify` reports it with success (SPEC 10.1 step 7.6).
8
9
  ERROR_CODES: tuple[str, ...] = (
9
10
  "E_PARSE_JSON",
10
11
  "E_PARSE_DUPLICATE_KEY",
@@ -45,6 +46,7 @@ ERROR_CODES: tuple[str, ...] = (
45
46
  "E_INPUT_INVALID",
46
47
  "E_INPUT_UNDECLARED",
47
48
  "E_VALUE_MISMATCH",
49
+ "E_EVAL_UNAVAILABLE",
48
50
  "E_BLOB_MISMATCH",
49
51
  "E_ENVELOPE",
50
52
  )
@@ -4,12 +4,31 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import math
7
+ import re
7
8
  from typing import Any, Dict, List, Tuple, Union
8
9
 
9
10
  from ._errors import DatumeshError, fail
10
11
 
11
12
  JSONText = Union[str, bytes, bytearray, memoryview]
12
13
 
14
+ #: SPEC 6.1: at most 64 levels of ``[`` and ``{`` open at once, counted outside strings.
15
+ MAX_DEPTH = 64
16
+ # A string (possibly unterminated) or a bracket; brackets inside strings are skipped with the string.
17
+ _DEPTH_TOKEN = re.compile(r'"(?:[^"\\]|\\.)*"?|[\[\]{}]', re.S)
18
+
19
+
20
+ def _too_deep(text: str) -> bool:
21
+ depth = 0
22
+ for m in _DEPTH_TOKEN.finditer(text):
23
+ c = m.group()
24
+ if c in "[{":
25
+ depth += 1
26
+ if depth > MAX_DEPTH:
27
+ return True
28
+ elif c in "]}":
29
+ depth -= 1
30
+ return False
31
+
13
32
 
14
33
  class _Duplicate(Exception):
15
34
  def __init__(self, key: str) -> None:
@@ -49,7 +68,11 @@ def parse_json(data: JSONText, path: str = "$") -> Any:
49
68
  ``data`` is bytes (must be UTF-8 without a byte order mark) or str (must not
50
69
  start with U+FEFF). Duplicate member names raise E_PARSE_DUPLICATE_KEY; every
51
70
  other failure (syntax, lone surrogates, NaN/Infinity, numbers outside the
52
- double range, invalid UTF-8) raises E_PARSE_JSON. Numbers become floats.
71
+ double range, invalid UTF-8, nesting deeper than 64) raises E_PARSE_JSON.
72
+ Numbers become floats. Failures are reported in the order of SPEC 6.1: byte
73
+ order mark, invalid UTF-8, nesting, then the first in text order of a syntax
74
+ error, NaN/Infinity, an overflowing number or a duplicate member (detected when
75
+ its object closes), and lone surrogates last.
53
76
  """
54
77
  if isinstance(data, (bytes, bytearray, memoryview)):
55
78
  raw = bytes(data)
@@ -65,6 +88,8 @@ def parse_json(data: JSONText, path: str = "$") -> Any:
65
88
  fail("E_PARSE_JSON", "byte order mark", path)
66
89
  else:
67
90
  raise TypeError(f"JSON text must be str or bytes, not {type(data).__name__}")
91
+ if _too_deep(text):
92
+ fail("E_PARSE_JSON", f"nesting deeper than {MAX_DEPTH} levels", path)
68
93
  try:
69
94
  value = json.loads(
70
95
  text,
@@ -102,10 +127,15 @@ def number_texts(data: JSONText) -> Dict[str, str]:
102
127
 
103
128
  def check_json_value(value: Any, path: str = "$") -> None:
104
129
  """Check that an already-parsed value is I-JSON: dict/list/str/float/int/bool/None only,
105
- string keys, no lone surrogates, finite numbers within the double range."""
106
- stack: List[Tuple[Any, str]] = [(value, path)]
130
+ string keys, no lone surrogates, finite numbers within the double range, and at most
131
+ 64 levels of arrays and objects (as its text would be, SPEC 6.1)."""
132
+ stack: List[Tuple[Any, str, int]] = [(value, path, 0)]
107
133
  while stack:
108
- v, p = stack.pop()
134
+ v, p, depth = stack.pop()
135
+ if isinstance(v, (dict, list)):
136
+ depth += 1
137
+ if depth > MAX_DEPTH:
138
+ fail("E_PARSE_JSON", f"nesting deeper than {MAX_DEPTH} levels", p)
109
139
  if v is None or isinstance(v, bool):
110
140
  continue
111
141
  if isinstance(v, str):
@@ -125,10 +155,10 @@ def check_json_value(value: Any, path: str = "$") -> None:
125
155
  fail("E_PARSE_JSON", f"member name {k!r} is not a string", p)
126
156
  if _has_surrogate(k):
127
157
  fail("E_PARSE_JSON", "lone surrogate in member name", p)
128
- stack.append((item, f"{p}.{k}"))
158
+ stack.append((item, f"{p}.{k}", depth))
129
159
  elif isinstance(v, list):
130
160
  for i, item in enumerate(v):
131
- stack.append((item, f"{p}[{i}]"))
161
+ stack.append((item, f"{p}[{i}]", depth))
132
162
  else:
133
163
  fail("E_PARSE_JSON", f"{type(v).__name__} is not a JSON value", p)
134
164
 
@@ -123,18 +123,20 @@ def keys_from_response(host: str, status: Optional[int], body: Union[str, bytes,
123
123
  ``status`` is the HTTP status, or None for a network, DNS, TLS or timeout
124
124
  failure. Any status other than 200 (redirects and 404 included) is
125
125
  E_KEY_UNAVAILABLE; a 200 whose body is not a valid key document for
126
- ``host`` is E_KEY_DOC. Never raises.
126
+ ``host``, or any 200 for a host that is not a DNS name, is E_KEY_DOC.
127
+ Never raises.
127
128
  """
128
129
  if not isinstance(host, str):
129
130
  return KeysResult(False, "E_KEY_UNAVAILABLE", None, f"host must be a str, not {type(host).__name__}")
130
- try:
131
- lx.check_dns_name(host, "host")
132
- except DatumeshError as e:
133
- return KeysResult(False, "E_KEY_UNAVAILABLE", host, f"cannot retrieve keys for an invalid host: {e.detail}")
134
131
  if status is None:
135
132
  return KeysResult(False, "E_KEY_UNAVAILABLE", host, "network, DNS, TLS or timeout failure")
136
133
  if isinstance(status, bool) or not isinstance(status, int) or status != 200:
137
134
  return KeysResult(False, "E_KEY_UNAVAILABLE", host, f"HTTP status {status!r}")
135
+ try:
136
+ lx.check_dns_name(host, "host")
137
+ except DatumeshError as e:
138
+ # SPEC 9.3: a response offered for a host that is not a DNS name cannot satisfy 9.2.
139
+ return _doc_error(host, f"a document for a host that is not a DNS name cannot satisfy 9.2: {e.detail}")
138
140
  if body is None:
139
141
  return _doc_error(host, "200 response without a body")
140
142
  if not isinstance(body, (str, bytes, bytearray, memoryview)):
@@ -9,7 +9,7 @@ licences); characters, NFC, then length (strings, SPEC 3.5).
9
9
  from __future__ import annotations
10
10
 
11
11
  import re
12
- from typing import Any, Optional
12
+ from typing import Any, Optional, Tuple
13
13
 
14
14
  from ._errors import fail
15
15
  from ._unicode import is_assigned, is_nfc
@@ -152,7 +152,7 @@ def check_blob(s: str, path: Optional[str]) -> None:
152
152
  f"at most {LIMIT_MEDIA_TYPE} characters>", path)
153
153
 
154
154
 
155
- def blob_parts(s: str) -> tuple:
155
+ def blob_parts(s: str) -> Tuple[str, int, str]:
156
156
  """(sha256 hex, length, media type) of a blob value already checked with :func:`check_blob`."""
157
157
  digest, length, media = s.split(":", 2)
158
158
  return digest, int(length), media
@@ -1,6 +1,6 @@
1
- """The unit list of spec/units.md (spec-0.2.0). Generated by tools/gen_units.py; do not edit."""
1
+ """The unit list of spec/units.md (spec-0.3.0). Generated by tools/gen_units.py; do not edit."""
2
2
 
3
- SOURCE_SHA256 = "6c9f21e0a219525c8616d2fce08136a747d38627ff183d9d513be029d6e85738"
3
+ SOURCE_SHA256 = "cf165af78779a95a21e1ad35e235e490f2dd125a562ede7652e97b9a5d4bb5ab"
4
4
 
5
5
  #: ```units-currency``` blocks, in file order (ISO 4217 List One without XXX, then withdrawn codes).
6
6
  CURRENCY = (
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional, Tuple
6
6
 
7
7
  from . import _lexical as lx
8
8
  from ._errors import fail
9
+ from ._json import sort_key
9
10
  from ._model import SLICE, validate_ref
10
11
 
11
12
  SCHEME = "cell://"
@@ -48,10 +49,17 @@ def _encode(text: str) -> str:
48
49
 
49
50
 
50
51
  def _declared_types(signature: Any) -> Dict[str, str]:
51
- args = signature.get("args") if isinstance(signature, dict) else None
52
- if not isinstance(args, dict) or not all(
53
- isinstance(a, dict) and isinstance(a.get("type"), str) for a in args.values()):
54
- raise TypeError('signature must be {"args": {<name>: {"type": <type name>, ...}}, ...}')
52
+ """SPEC 5.4 step 5: read the argument types; an unreadable signature is E_SCHEMA."""
53
+ if not isinstance(signature, dict):
54
+ fail("E_SCHEMA", "the signature is an object", "signature")
55
+ args = signature.get("args")
56
+ if not isinstance(args, dict):
57
+ fail("E_SCHEMA", "the signature has an args object", "signature" if "args" not in signature
58
+ else "signature.args")
59
+ for name in sorted(args, key=sort_key):
60
+ a = args[name]
61
+ if not isinstance(a, dict) or not isinstance(a.get("type"), str):
62
+ fail("E_SCHEMA", "an argument declaration is an object with a string type", f"signature.args.{name}")
55
63
  return {name: a["type"] for name, a in args.items()}
56
64
 
57
65
 
@@ -61,12 +69,11 @@ def parse_uri(uri: str, signature: Dict[str, Any]) -> Dict[str, Any]:
61
69
  ``signature`` is the ``signature`` member of the function's descriptor; it
62
70
  supplies each argument's type. As SPEC 5.4 says, it is taken as given (it comes
63
71
  from a descriptor already checked under section 7): only its argument types are
64
- read, and a signature from which they cannot be read is a TypeError. Checks run
65
- in the order of SPEC 5.4 and the first failure is raised as DatumeshError.
72
+ read, in step 5, and a signature from which they cannot be read is E_SCHEMA.
73
+ Checks run in the order of SPEC 5.4 and the first failure is raised as DatumeshError.
66
74
  """
67
75
  if not isinstance(uri, str):
68
76
  raise TypeError(f"uri must be a str, not {type(uri).__name__}")
69
- declared = _declared_types(signature)
70
77
 
71
78
  # 1. Outer grammar.
72
79
  if not uri.startswith(SCHEME) or "#" in uri:
@@ -120,7 +127,8 @@ def parse_uri(uri: str, signature: Dict[str, Any]) -> Dict[str, Any]:
120
127
  if len(params) > lx.LIMIT_ARGS:
121
128
  fail("E_CANON_LIMIT", f"more than {lx.LIMIT_ARGS} arguments", "uri.query")
122
129
 
123
- # 5. Arguments against the signature: unknown, then missing.
130
+ # 5. The signature (read, not checked: SPEC 5.4), then the arguments against it: unknown, then missing.
131
+ declared = _declared_types(signature)
124
132
  present = {name for name, _ in params}
125
133
  for name, _ in params:
126
134
  if name not in declared:
@@ -23,12 +23,16 @@ class VerifyResult:
23
23
  """The outcome of :func:`verify`.
24
24
 
25
25
  ``ok``: the receipt verified. ``code``: ``"OK"`` or the first failing
26
- error code. ``level``: ``"signed"`` (steps 1-6) or ``"recomputed"`` (step 7
26
+ error code; on success it is ``"E_EVAL_UNAVAILABLE"`` instead of ``"OK"``
27
+ when recomputation was asked for without an evaluated ``value`` (SPEC 10.1
28
+ step 7.6: steps 7.1-7.5 passed, nothing was compared, the level stays
29
+ ``signed``). ``level``: ``"signed"`` (steps 1-6) or ``"recomputed"`` (step 7
27
30
  too) when ok, else None. ``hash``: the receipt hash once the receipt is in
28
31
  canonical form (steps 1-3 passed), else None. ``key``: the ``computed_by``
29
32
  value when ok. ``detail``: human-readable text. ``path``: where the
30
- failure is (a JSON path) or None. ``blob_checked``: True only when blob
31
- content was supplied and step 7.7 ran and passed (the level is unchanged).
33
+ failure is (a JSON path), ``"$.value"`` with ``E_EVAL_UNAVAILABLE``, or None.
34
+ ``blob_checked``: True only when blob content was supplied and step 7.7 ran
35
+ and passed (the level and the code are unchanged).
32
36
  """
33
37
 
34
38
  ok: bool
@@ -76,13 +80,14 @@ def _signed(receipt: Any, sources: Tuple[_Source, ...]) -> Tuple[Dict[str, Any],
76
80
  return r, h
77
81
 
78
82
 
79
- def _recompute(r: Dict[str, Any], recompute: Mapping[str, Any], sources: Tuple[_Source, ...]) -> None:
80
- """Step 7. Raises DatumeshError."""
81
- # 7.1 descriptor
82
- desc = recompute.get("descriptor")
83
- if desc is None:
83
+ def _recompute(r: Dict[str, Any], recompute: Mapping[str, Any], sources: Tuple[_Source, ...]) -> bool:
84
+ """Step 7.1-7.6. Raises DatumeshError. Returns False when 7.1-7.5 passed and there is no
85
+ evaluated value to compare (E_EVAL_UNAVAILABLE), True when 7.6 passed."""
86
+ # 7.1 descriptor: only an absent member is "none obtained". Whatever is obtained is a JSON
87
+ # value (None is JSON null); text or bytes are parsed strictly first.
88
+ if "descriptor" not in recompute:
84
89
  fail("E_FN_UNAVAILABLE", "no descriptor supplied for cell.fn_hash", "$.cell.fn_hash")
85
- desc = load(desc, "descriptor")
90
+ desc = load(recompute["descriptor"], "descriptor")
86
91
  if hashlib.sha256(jcs_bytes(desc)).hexdigest() != r["cell"]["fn_hash"]:
87
92
  fail("E_FN_HASH_MISMATCH", "the descriptor does not hash to cell.fn_hash", "$.cell.fn_hash")
88
93
  _model.validate_function(desc, "descriptor")
@@ -124,16 +129,20 @@ def _recompute(r: Dict[str, Any], recompute: Mapping[str, Any], sources: Tuple[_
124
129
  fqn = f"{sub['cell']['ns']}/{sub['cell']['fn']}"
125
130
  if fqn not in declared_inputs:
126
131
  fail("E_INPUT_UNDECLARED", f"input {h} is a cell of {fqn}, which the descriptor does not declare", p)
127
- if sub["asof"] != r["asof"]:
128
- fail("E_TIME_ORDER", f"input {h} has asof {sub['asof']}, not {r['asof']}", p)
132
+ # at or before the receipt's asof (D17, spec-0.3.0); canonical instants compare as strings (3.3)
133
+ if sub["asof"] > r["asof"]:
134
+ fail("E_TIME_ORDER", f"input {h} has asof {sub['asof']}, after the receipt's {r['asof']}", p)
129
135
  if latest_tx is None or sub["tx"] > latest_tx:
130
136
  latest_tx = sub["tx"]
131
137
  if latest_tx is not None and r["tx"] != latest_tx:
132
138
  fail("E_TIME_ORDER", f"a computed cell's tx is the latest input tx ({latest_tx})", "$.tx")
133
- # 7.6 value
139
+ # 7.6 value; without one, evaluation is unavailable (not a failure)
140
+ if "value" not in recompute:
141
+ return False
134
142
  v = dict(recompute["value"])
135
143
  if not _model.same_scalar(v, {"type": r["type"], "value": r["value"]}):
136
144
  fail("E_VALUE_MISMATCH", "the recomputed value differs from the receipt's", "$.value")
145
+ return True
137
146
 
138
147
 
139
148
  def check_blob_content(r: Dict[str, Any], content: bytes) -> None:
@@ -149,10 +158,12 @@ def _check_recompute_arg(recompute: Any) -> None:
149
158
  if recompute is None:
150
159
  return
151
160
  if not isinstance(recompute, Mapping):
152
- raise TypeError("recompute must be a mapping with 'value' and optional 'descriptor' and 'inputs'")
153
- v = recompute.get("value")
154
- if not isinstance(v, Mapping) or "type" not in v or "value" not in v:
155
- raise TypeError("recompute['value'] must be {'type': ..., 'value': ...}: the evaluated result")
161
+ raise TypeError("recompute must be a mapping with optional 'descriptor', 'inputs' and 'value'")
162
+ if "value" in recompute:
163
+ v = recompute["value"]
164
+ if not isinstance(v, Mapping) or "type" not in v or "value" not in v:
165
+ raise TypeError("recompute['value'] must be {'type': ..., 'value': ...}: the evaluated result "
166
+ "(leave it out when the function could not be evaluated)")
156
167
  inputs = recompute.get("inputs")
157
168
  if inputs is not None and not isinstance(inputs, Mapping):
158
169
  raise TypeError("recompute['inputs'] must be a mapping of receipt hash to receipt JSON text")
@@ -168,11 +179,21 @@ def verify(receipt: Any, keys: Any, recompute: Optional[Mapping[str, Any]] = Non
168
179
  validity windows), or a list of those; the first source that covers the
169
180
  ``computed_by`` host is used. ``recompute`` (optional) enables step 7:
170
181
  ``{"descriptor": ..., "inputs": {hash: receipt text}, "value": {"type", "value"}}``.
182
+ ``descriptor`` is the descriptor obtained for ``cell.fn_hash``: a parsed JSON
183
+ value, or JSON text/bytes (parsed strictly); leave it out when none was
184
+ obtained (E_FN_UNAVAILABLE). Any other value, ``None`` (JSON null)
185
+ included, is a descriptor offered and must hash to ``cell.fn_hash``
186
+ (E_FN_HASH_MISMATCH). ``value`` is what evaluating the function gave; leave
187
+ it out when the function could not be evaluated: once steps 7.1-7.5 pass,
188
+ the result is then ok at level ``signed`` with ``code`` ``"E_EVAL_UNAVAILABLE"``
189
+ and ``path`` ``"$.value"`` (SPEC 10.1 step 7.6).
171
190
  ``blob`` (optional): the content bytes of a blob value. Step 7.7 then runs
172
191
  last (after step 7 if ``recompute`` is given, else directly after step 6):
173
192
  the receipt's ``type`` must be ``blob`` (E_SCHEMA otherwise) and the content
174
193
  must have the blob's length and SHA-256 (E_BLOB_MISMATCH). The level stays
175
- ``signed`` or ``recomputed``; ``blob_checked`` reports that 7.7 passed.
194
+ ``signed`` or ``recomputed``; ``blob_checked`` reports that 7.7 passed. 7.7
195
+ also runs after E_EVAL_UNAVAILABLE: if it passes the code is kept, if it
196
+ fails its failure is reported instead.
176
197
 
177
198
  Never raises for an invalid receipt or bad key material: the outcome is in
178
199
  the returned :class:`VerifyResult`. Raises TypeError only for arguments of
@@ -188,9 +209,14 @@ def verify(receipt: Any, keys: Any, recompute: Optional[Mapping[str, Any]] = Non
188
209
  try:
189
210
  r, h = _signed(receipt, sources)
190
211
  level, detail = "signed", "signature-valid"
212
+ code, path = "OK", None
191
213
  if recompute is not None:
192
- _recompute(r, recompute, sources)
193
- level, detail = "recomputed", "fully verified by recomputation"
214
+ if _recompute(r, recompute, sources):
215
+ level, detail = "recomputed", "fully verified by recomputation"
216
+ else:
217
+ code, path = "E_EVAL_UNAVAILABLE", "$.value"
218
+ detail = ("signature-valid; recomputation checks 7.1-7.5 passed, but the function was not "
219
+ "evaluated (no value supplied)")
194
220
  checked = False
195
221
  if blob is not None:
196
222
  if r["type"] != "blob":
@@ -198,7 +224,7 @@ def verify(receipt: Any, keys: Any, recompute: Optional[Mapping[str, Any]] = Non
198
224
  check_blob_content(r, bytes(blob))
199
225
  detail += "; blob content matches"
200
226
  checked = True
201
- return VerifyResult(True, "OK", level, h, r["computed_by"], detail, None, checked)
227
+ return VerifyResult(True, code, level, h, r["computed_by"], detail, path, checked)
202
228
  except DatumeshError as e:
203
229
  h = h or getattr(e, "receipt_hash", None)
204
230
  return VerifyResult(False, e.code, None, h, None, e.detail, e.path)
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datumesh
3
- Version: 0.2.0
4
- Summary: Verify Datumesh receipts (spec-0.2.0): RFC 8785 canonical JSON, Ed25519 signatures, blobs, units, cell URIs, response envelopes, key documents.
3
+ Version: 0.3.0
4
+ Summary: Verify Datumesh receipts (spec-0.3.0): RFC 8785 canonical JSON, Ed25519 signatures, blobs, units, cell URIs, response envelopes, key documents.
5
5
  Author: Datumesh
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://datumesh.com
8
8
  Project-URL: Repository, https://github.com/datumesh-com/datumesh-verify-py
9
- Project-URL: Specification (spec-0.2.0), https://github.com/datumesh-com/datumesh-spec/blob/spec-0.2.0/SPEC.md
10
- Keywords: datumesh,spec-0.2.0,receipt,verification,ed25519,rfc8785,jcs,canonical-json
9
+ Project-URL: Specification (spec-0.3.0), https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md
10
+ Keywords: datumesh,spec-0.3.0,receipt,verification,ed25519,rfc8785,jcs,canonical-json
11
11
  Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Intended Audience :: Developers
13
13
  Classifier: Operating System :: OS Independent
@@ -27,11 +27,11 @@ Dynamic: license-file
27
27
 
28
28
  # datumesh
29
29
 
30
- Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.2.0/SPEC.md) (revision `spec-0.2.0`) defines. It also checks a mesh's response envelope and resolves the unit of a cell's value.
30
+ Verify [Datumesh](https://datumesh.com) receipts in Python. A receipt is a signed answer for one addressable data cell; this library checks its canonical form (RFC 8785 JSON) and its Ed25519 signature against the signer's published keys, and optionally the recomputation checks (function descriptor, input receipts, and a value you computed) and the content of a blob value, as the [Datumesh specification](https://github.com/datumesh-com/datumesh-spec/blob/spec-0.3.0/SPEC.md) (revision `spec-0.3.0`) defines. It also checks a mesh's response envelope and resolves the unit of a cell's value.
31
31
 
32
32
  ```sh
33
33
  pip install datumesh
34
- git clone --depth 1 --branch spec-0.2.0 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
34
+ git clone --depth 1 --branch spec-0.3.0 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
35
35
  ```
36
36
 
37
37
  ```python
@@ -49,8 +49,8 @@ Website: https://datumesh.com · Specification: https://github.com/datumesh-com/
49
49
 
50
50
  ## API
51
51
 
52
- - `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor, the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
53
- - `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one; a stale receipt may answer an earlier `asof`). The envelope itself is unsigned: `stale`, `superseded_by` and a `verified` label remain the mesh's claims.
52
+ - `verify(receipt, keys, recompute=None, blob=None) -> VerifyResult`: runs the verification procedure of SPEC 10.1. Never raises for an invalid receipt or bad key material; the result has `ok`, `code` (`"OK"` or an error code), `level` (`"signed"` or `"recomputed"`), `hash`, `key`, `detail` and `path`. One code comes with success: when you pass `recompute` without a `value` because you could not evaluate the function, the other recomputation checks still run, and if they pass the result is ok at level `signed` with `code` `"E_EVAL_UNAVAILABLE"` (SPEC 10.1 step 7.6). It uses no network: keys are an argument (a key document, a `KeysResult`, a `{key id: public key}` mapping, or a list of these). `recompute` takes the descriptor (leave it out if you have none; `None` is a JSON null and fails the hash check), the input receipts and the value you computed (the library does not evaluate functions); `blob` takes the content bytes of a blob value, whose length and SHA-256 are then checked (step 7.7, reported as `blob_checked`).
53
+ - `check_envelope(status, body, keys, request) -> EnvelopeResult`: checks a mesh's response to `https://<mesh>/cell/...` (SPEC 5.5), verifies the receipt inside it, and checks that it answers your request (`request` is `{"cell_id": ..., "asof": ...}`, `asof` only if you asked for one; the receipt's `asof` must be at or before it, for `ok` and `stale` alike). The envelope itself is unsigned: `stale`, `superseded_by`, a `verified` label and the currency `ok` asserts remain the mesh's claims.
54
54
  - `resolved_unit(descriptor, cell) -> str | None`: the unit of a cell's value, such as `USD/EUR` (SPEC 7.1, units.md).
55
55
  - `keys_from_response(host, status, body) -> KeysResult`: interprets a response for `https://<host>/.well-known/datumesh.json` (SPEC 9.3).
56
56
  - `fetch_keys(host, timeout=10.0) -> KeysResult`: the only function that uses the network. One GET, TLS verified, redirects not followed.
@@ -27,6 +27,8 @@ tests/test_conformance.py
27
27
  tests/test_envelope.py
28
28
  tests/test_jcs.py
29
29
  tests/test_keys.py
30
+ tests/test_spec_0_2_1.py
31
+ tests/test_spec_0_3_0.py
30
32
  tests/test_unicode.py
31
33
  tests/test_units.py
32
34
  tests/test_uri.py
@@ -18,11 +18,11 @@ LINEAGE = next(v for v in vectors_of("receipt", True) if v["id"] == "receipt/com
18
18
 
19
19
  class ApiTest(unittest.TestCase):
20
20
  def test_constants(self) -> None:
21
- self.assertEqual(datumesh.SPEC_VERSION, "spec-0.2.0")
22
- self.assertEqual(datumesh.__version__, "0.2.0")
23
- self.assertEqual(len(datumesh.ERROR_CODES), 41)
24
- self.assertEqual(len(set(datumesh.ERROR_CODES)), 41)
25
- for code in ("E_CANON_UNIT", "E_CANON_BLOB", "E_BLOB_MISMATCH", "E_ENVELOPE"):
21
+ self.assertEqual(datumesh.SPEC_VERSION, "spec-0.3.0")
22
+ self.assertEqual(datumesh.__version__, "0.3.0")
23
+ self.assertEqual(len(datumesh.ERROR_CODES), 42)
24
+ self.assertEqual(len(set(datumesh.ERROR_CODES)), 42)
25
+ for code in ("E_CANON_UNIT", "E_CANON_BLOB", "E_BLOB_MISMATCH", "E_ENVELOPE", "E_EVAL_UNAVAILABLE"):
26
26
  self.assertIn(code, datumesh.ERROR_CODES)
27
27
  for name in ("resolved_unit", "check_envelope", "EnvelopeResult", "UNITS"):
28
28
  self.assertIn(name, datumesh.__all__)
@@ -88,9 +88,12 @@ class ApiTest(unittest.TestCase):
88
88
  # descriptor as JSON text
89
89
  rc2 = dict(rc, descriptor=json.dumps(rc["descriptor"]))
90
90
  self.assertEqual(verify(LINEAGE["input"], keys, rc2).level, "recomputed")
91
- # descriptor None or absent
92
- self.assertEqual(verify(LINEAGE["input"], keys, dict(rc, descriptor=None)).code, "E_FN_UNAVAILABLE")
91
+ # descriptor absent (E_FN_UNAVAILABLE), or None: a JSON null offered (E_FN_HASH_MISMATCH, spec-0.3.0)
92
+ self.assertEqual(verify(LINEAGE["input"], keys, dict(rc, descriptor=None)).code, "E_FN_HASH_MISMATCH")
93
93
  self.assertEqual(verify(LINEAGE["input"], keys, {"value": rc["value"]}).code, "E_FN_UNAVAILABLE")
94
+ # value absent: evaluation unavailable (spec-0.3.0), success at level signed
95
+ res = verify(LINEAGE["input"], keys, {"descriptor": rc["descriptor"], "inputs": rc["inputs"]})
96
+ self.assertEqual((res.ok, res.code, res.level), (True, "E_EVAL_UNAVAILABLE", "signed"))
94
97
  # inputs missing, given as parsed objects, or tampered
95
98
  self.assertEqual(verify(LINEAGE["input"], keys, dict(rc, inputs={})).code, "E_INPUT_UNAVAILABLE")
96
99
  parsed = {h: json.loads(t) for h, t in rc["inputs"].items()}
@@ -98,7 +101,7 @@ class ApiTest(unittest.TestCase):
98
101
  # boolean vs string values are different
99
102
  self.assertEqual(verify(LINEAGE["input"], keys, dict(rc, value={"type": "number", "value": "108.920"})).code,
100
103
  "E_VALUE_MISMATCH")
101
- for bad in ([], {"descriptor": rc["descriptor"]}, {"value": "108.92"}, dict(rc, inputs=[])):
104
+ for bad in ([], dict(rc, value=None), {"value": "108.92"}, dict(rc, inputs=[])):
102
105
  with self.subTest(bad=str(bad)[:40]), self.assertRaises(TypeError):
103
106
  verify(LINEAGE["input"], keys, bad) # type: ignore[arg-type]
104
107