datumesh 0.4.2__tar.gz → 0.4.3__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 (42) hide show
  1. {datumesh-0.4.2 → datumesh-0.4.3}/PKG-INFO +15 -10
  2. {datumesh-0.4.2 → datumesh-0.4.3}/README.md +11 -6
  3. {datumesh-0.4.2 → datumesh-0.4.3}/pyproject.toml +4 -4
  4. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/__init__.py +25 -6
  5. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_envelope.py +20 -4
  6. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_errors.py +10 -3
  7. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_keys.py +84 -4
  8. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_lexical.py +2 -0
  9. datumesh-0.4.3/src/datumesh/_licence.py +257 -0
  10. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_unitlist.py +1 -1
  11. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh.egg-info/PKG-INFO +15 -10
  12. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh.egg-info/SOURCES.txt +2 -0
  13. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_api.py +4 -4
  14. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_conformance.py +11 -7
  15. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_envelope.py +1 -1
  16. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_spec_0_4_1.py +1 -1
  17. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_spec_0_4_2.py +4 -3
  18. datumesh-0.4.3/tests/test_spec_0_4_3.py +353 -0
  19. {datumesh-0.4.2 → datumesh-0.4.3}/LICENSE +0 -0
  20. {datumesh-0.4.2 → datumesh-0.4.3}/setup.cfg +0 -0
  21. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_json.py +0 -0
  22. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_mesh.py +0 -0
  23. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_model.py +0 -0
  24. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_ucd15.py +0 -0
  25. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_unicode.py +0 -0
  26. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_units.py +0 -0
  27. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_uri.py +0 -0
  28. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/_verify.py +0 -0
  29. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh/py.typed +0 -0
  30. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh.egg-info/dependency_links.txt +0 -0
  31. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh.egg-info/requires.txt +0 -0
  32. {datumesh-0.4.2 → datumesh-0.4.3}/src/datumesh.egg-info/top_level.txt +0 -0
  33. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_bitflips.py +0 -0
  34. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_blob.py +0 -0
  35. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_jcs.py +0 -0
  36. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_keys.py +0 -0
  37. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_spec_0_2_1.py +0 -0
  38. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_spec_0_3_0.py +0 -0
  39. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_spec_0_4_0.py +0 -0
  40. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_unicode.py +0 -0
  41. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_units.py +0 -0
  42. {datumesh-0.4.2 → datumesh-0.4.3}/tests/test_uri.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datumesh
3
- Version: 0.4.2
4
- Summary: Verify Datumesh receipts (spec-0.4.2): RFC 8785 canonical JSON, Ed25519 signatures, slice-as-lineage, blobs, units, cell URIs, response envelopes (pending jobs, pins, slice pages, batches), key documents, mesh documents.
3
+ Version: 0.4.3
4
+ Summary: Verify Datumesh receipts (spec-0.4.3): RFC 8785 canonical JSON, Ed25519 signatures, slice-as-lineage, blobs, units, cell URIs, response envelopes (pending jobs, pins, slice pages, batches), key documents with declarations, mesh documents, licence inheritance and entitlement.
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.4.2), https://github.com/datumesh-com/datumesh-spec/blob/spec-0.4.2/SPEC.md
10
- Keywords: datumesh,spec-0.4.2,receipt,verification,ed25519,rfc8785,jcs,canonical-json
9
+ Project-URL: Specification (spec-0.4.3), https://github.com/datumesh-com/datumesh-spec/blob/spec-0.4.3/SPEC.md
10
+ Keywords: datumesh,spec-0.4.3,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 slice manifests, 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.4.2/SPEC.md) (revision `spec-0.4.2`) defines. It also checks a mesh's response envelope (including `pending` jobs), resolves the unit of a cell's value, and checks a host's mesh document for cross-mesh use.
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 slice manifests, 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.4.3/SPEC.md) (revision `spec-0.4.3`) defines. It also checks a mesh's response envelope (including `pending` jobs), resolves the unit of a cell's value, checks a host's mesh document for cross-mesh use, and, separately from verification, checks a computed receipt's licence against the inheritance rule (the entitlement check).
31
31
 
32
32
  ```sh
33
33
  pip install datumesh
34
- git clone --depth 1 --branch spec-0.4.2 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
34
+ git clone --depth 1 --branch spec-0.4.3 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
35
35
  ```
36
36
 
37
37
  ```python
@@ -52,16 +52,21 @@ Website: https://datumesh.com · Specification: https://github.com/datumesh-com/
52
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`. Two codes come with success, at level `signed` (SPEC 10.1 step 7.6): `"E_EVAL_UNAVAILABLE"` when you pass `recompute` without a `value` because you could not evaluate the function (the other recomputation checks still run first), and `"E_INPUT_SUPERSEDED"` when your value differs but the evaluation's `lineage` shows that an input was replaced by a later version (the receipt is historical, not wrong). 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 (slice members included), the manifests of slice entries (`slice:<slice id>:<members hash>`), the value you computed and optionally the evaluation's lineage (the library does not evaluate functions, formulas included); `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
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). A `not_covered` envelope may carry a formula evaluation `fault` (exposed as `fault`). The envelope itself is unsigned: `stale`, `superseded_by`, a `verified` label and the currency `ok` asserts remain the mesh's claims.
54
54
  - Jobs (spec-0.4.1): a `pending` envelope (HTTP 202) carries `job`, `estimate_seconds` and `poll` (exposed on the result) and echoes your request, which must match. When you poll, pass the original request plus `"job": <the job id>`: the answer is checked against the original cell and `asof`, never the job URL, and only then may it be `bad_request` with `E_JOB_UNKNOWN` (ask again).
55
- - Pins, trust and the mesh asked (spec-0.4.2): a request description may carry `fn_hash` (you pinned the function: `parse_uri`/`format_uri` handle `fn_hash=`) and `mesh` (the host you asked). Answers to a pinned request must echo the pin; with `mesh`, `computed` (this mesh evaluated and signed it) is checked against the signer, and `poll` against the host. Without `mesh`, comparing them with the host you asked is your own check.
56
- - Slice pages (spec-0.4.2): `check_envelope` with `{"slice_id", "asof"?, "fn_hash"?, "mesh"?, "after"?}` checks a page of the slice endpoint: every member's receipt, its membership, `asof`, trust and key order (`result.members`, `result.next`).
57
- - `check_batch(status, body, keys, requests) -> BatchResult` (spec-0.4.2): the answer to `POST /cells`, checked as a whole, then item by item against each request (`result.items`); a refusal of the whole batch gives `result.outcome`.
55
+ - Pins, trust and the mesh asked (spec-0.4.3): a request description may carry `fn_hash` (you pinned the function: `parse_uri`/`format_uri` handle `fn_hash=`) and `mesh` (the host you asked). Answers to a pinned request must echo the pin; with `mesh`, `computed` (this mesh evaluated and signed it) is checked against the signer, and `poll` against the host. Without `mesh`, comparing them with the host you asked is your own check.
56
+ - Slice pages (spec-0.4.3): `check_envelope` with `{"slice_id", "asof"?, "fn_hash"?, "mesh"?, "after"?}` checks a page of the slice endpoint: every member's receipt, its membership, `asof`, trust and key order (`result.members`, `result.next`).
57
+ - `check_batch(status, body, keys, requests) -> BatchResult` (spec-0.4.3): the answer to `POST /cells`, checked as a whole, then item by item against each request (`result.items`); a refusal of the whole batch gives `result.outcome`.
58
58
  - `resolve_mesh(host, keys, mesh) -> MeshResult`: checks a host's key document and mesh document (`/.well-known/datumesh-mesh.json`, SPEC 9.6) together, given each response as a `(status, body)` pair; the result has `bindings` and the known `endpoints`, or `E_KEY_UNAVAILABLE`, `E_KEY_DOC` or `E_MESH_DOC`. It uses no network, and verification never needs it.
59
+ - Licences (spec-0.4.3): `inherit_licence(licences, declarations, fn, issued, mesh_licence) -> LicenceResult` applies the inheritance rule of SPEC 8.5 (the strictest input licence wins; a restricted licence only where the function's publisher declared it, open at `issued`) and gives `licence` (None for a refusal, which a mesh answers `unlicensed`) and `reason`. `check_entitlement(receipt, licences, keys) -> EntitlementResult` (SPEC 10.3) says whether a verified computed receipt's licence is the one the rule gives, with one of eight reasons; `keys` is the key document of the publisher's host (the host of `cell.ns`), and another host's is a `ValueError`. `input_licences(receipt, receipts, manifests=None, keys=None)` collects the input licences through the receipts and slice manifests you have. None of this is verification: a receipt that is not entitled is still signature-valid.
60
+ - Key documents may carry `declarations` (spec-0.4.3, SPEC 9.2): `KeysResult.declarations` maps each declared function name to its `Declaration` entries (`licence`, `valid_from`, `valid_to`). A malformed one rejects the whole document (`E_KEY_DOC`); otherwise they change nothing in verification.
61
+ - `E_FOREIGN_UNAVAILABLE` (spec-0.4.3): an `error` envelope at HTTP 502 saying another mesh could not be reached, with `detail` (exposed as `error_detail`): one of `FOREIGN_DETAILS` (`offline`, `network`, `dns`, `tls`, `timeout`, `refused`). `is_unrooted(namespace)` tells names under `.invalid` (SPEC 2.5), whose receipts verify only against a key document supplied out of band.
59
62
  - `resolved_unit(descriptor, cell) -> str | None`: the unit of a cell's value, such as `USD/EUR` (SPEC 7.1, units.md).
60
63
  - `keys_from_response(host, status, body) -> KeysResult`: interprets a response for `https://<host>/.well-known/datumesh.json` (SPEC 9.3).
61
64
  - `fetch_keys(host, timeout=10.0) -> KeysResult`: the only function that uses the network. One GET, TLS verified, redirects not followed.
62
65
  - `canonicalize(obj, kind=None) -> bytes`, `cell_hash`, `function_hash`, `receipt_hash`, `receipt_payload`, `jcs`: validation, RFC 8785 canonical bytes and SHA-256 hashes. Kinds `"slice"` and `"manifest"`, with `slice_id`, `members_hash` and `slice_entry`, cover slice objects and manifests (SPEC 4.4, 15.13).
63
66
  - `parse_uri(uri, signature)`, `format_uri(ref)`, `uri_to_https(uri, mesh)`: cell URIs (SPEC 5).
64
- - `DatumeshError` (with `.code` and `.path`), `ERROR_CODES`, `SUCCESS_CODES`, `FAULT_CODES`, `SPEC_VERSION`, `UNITS`, `CURRENCIES`.
67
+ - `DatumeshError` (with `.code` and `.path`), `ERROR_CODES`, `SUCCESS_CODES`, `FAULT_CODES`, `FOREIGN_DETAILS`, `REASONS`, `SPEC_VERSION`, `UNITS`, `CURRENCIES`.
68
+
69
+ Changes are listed in [CHANGELOG.md](CHANGELOG.md).
65
70
 
66
71
  Requires Python 3.10 or later and PyNaCl. String rules use Unicode 15.1 tables shipped with the package, so results do not depend on the Python version's own Unicode database; the unit list is copied from the specification's `units.md`. Choosing which publishers to trust is up to you: a valid signature only means the key holder made the statement.
67
72
 
@@ -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 slice manifests, 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.4.2/SPEC.md) (revision `spec-0.4.2`) defines. It also checks a mesh's response envelope (including `pending` jobs), resolves the unit of a cell's value, and checks a host's mesh document for cross-mesh use.
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 slice manifests, 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.4.3/SPEC.md) (revision `spec-0.4.3`) defines. It also checks a mesh's response envelope (including `pending` jobs), resolves the unit of a cell's value, checks a host's mesh document for cross-mesh use, and, separately from verification, checks a computed receipt's licence against the inheritance rule (the entitlement check).
4
4
 
5
5
  ```sh
6
6
  pip install datumesh
7
- git clone --depth 1 --branch spec-0.4.2 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
7
+ git clone --depth 1 --branch spec-0.4.3 https://github.com/datumesh-com/datumesh-spec && cd datumesh-spec
8
8
  ```
9
9
 
10
10
  ```python
@@ -25,16 +25,21 @@ Website: https://datumesh.com · Specification: https://github.com/datumesh-com/
25
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`. Two codes come with success, at level `signed` (SPEC 10.1 step 7.6): `"E_EVAL_UNAVAILABLE"` when you pass `recompute` without a `value` because you could not evaluate the function (the other recomputation checks still run first), and `"E_INPUT_SUPERSEDED"` when your value differs but the evaluation's `lineage` shows that an input was replaced by a later version (the receipt is historical, not wrong). 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 (slice members included), the manifests of slice entries (`slice:<slice id>:<members hash>`), the value you computed and optionally the evaluation's lineage (the library does not evaluate functions, formulas included); `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
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). A `not_covered` envelope may carry a formula evaluation `fault` (exposed as `fault`). The envelope itself is unsigned: `stale`, `superseded_by`, a `verified` label and the currency `ok` asserts remain the mesh's claims.
27
27
  - Jobs (spec-0.4.1): a `pending` envelope (HTTP 202) carries `job`, `estimate_seconds` and `poll` (exposed on the result) and echoes your request, which must match. When you poll, pass the original request plus `"job": <the job id>`: the answer is checked against the original cell and `asof`, never the job URL, and only then may it be `bad_request` with `E_JOB_UNKNOWN` (ask again).
28
- - Pins, trust and the mesh asked (spec-0.4.2): a request description may carry `fn_hash` (you pinned the function: `parse_uri`/`format_uri` handle `fn_hash=`) and `mesh` (the host you asked). Answers to a pinned request must echo the pin; with `mesh`, `computed` (this mesh evaluated and signed it) is checked against the signer, and `poll` against the host. Without `mesh`, comparing them with the host you asked is your own check.
29
- - Slice pages (spec-0.4.2): `check_envelope` with `{"slice_id", "asof"?, "fn_hash"?, "mesh"?, "after"?}` checks a page of the slice endpoint: every member's receipt, its membership, `asof`, trust and key order (`result.members`, `result.next`).
30
- - `check_batch(status, body, keys, requests) -> BatchResult` (spec-0.4.2): the answer to `POST /cells`, checked as a whole, then item by item against each request (`result.items`); a refusal of the whole batch gives `result.outcome`.
28
+ - Pins, trust and the mesh asked (spec-0.4.3): a request description may carry `fn_hash` (you pinned the function: `parse_uri`/`format_uri` handle `fn_hash=`) and `mesh` (the host you asked). Answers to a pinned request must echo the pin; with `mesh`, `computed` (this mesh evaluated and signed it) is checked against the signer, and `poll` against the host. Without `mesh`, comparing them with the host you asked is your own check.
29
+ - Slice pages (spec-0.4.3): `check_envelope` with `{"slice_id", "asof"?, "fn_hash"?, "mesh"?, "after"?}` checks a page of the slice endpoint: every member's receipt, its membership, `asof`, trust and key order (`result.members`, `result.next`).
30
+ - `check_batch(status, body, keys, requests) -> BatchResult` (spec-0.4.3): the answer to `POST /cells`, checked as a whole, then item by item against each request (`result.items`); a refusal of the whole batch gives `result.outcome`.
31
31
  - `resolve_mesh(host, keys, mesh) -> MeshResult`: checks a host's key document and mesh document (`/.well-known/datumesh-mesh.json`, SPEC 9.6) together, given each response as a `(status, body)` pair; the result has `bindings` and the known `endpoints`, or `E_KEY_UNAVAILABLE`, `E_KEY_DOC` or `E_MESH_DOC`. It uses no network, and verification never needs it.
32
+ - Licences (spec-0.4.3): `inherit_licence(licences, declarations, fn, issued, mesh_licence) -> LicenceResult` applies the inheritance rule of SPEC 8.5 (the strictest input licence wins; a restricted licence only where the function's publisher declared it, open at `issued`) and gives `licence` (None for a refusal, which a mesh answers `unlicensed`) and `reason`. `check_entitlement(receipt, licences, keys) -> EntitlementResult` (SPEC 10.3) says whether a verified computed receipt's licence is the one the rule gives, with one of eight reasons; `keys` is the key document of the publisher's host (the host of `cell.ns`), and another host's is a `ValueError`. `input_licences(receipt, receipts, manifests=None, keys=None)` collects the input licences through the receipts and slice manifests you have. None of this is verification: a receipt that is not entitled is still signature-valid.
33
+ - Key documents may carry `declarations` (spec-0.4.3, SPEC 9.2): `KeysResult.declarations` maps each declared function name to its `Declaration` entries (`licence`, `valid_from`, `valid_to`). A malformed one rejects the whole document (`E_KEY_DOC`); otherwise they change nothing in verification.
34
+ - `E_FOREIGN_UNAVAILABLE` (spec-0.4.3): an `error` envelope at HTTP 502 saying another mesh could not be reached, with `detail` (exposed as `error_detail`): one of `FOREIGN_DETAILS` (`offline`, `network`, `dns`, `tls`, `timeout`, `refused`). `is_unrooted(namespace)` tells names under `.invalid` (SPEC 2.5), whose receipts verify only against a key document supplied out of band.
32
35
  - `resolved_unit(descriptor, cell) -> str | None`: the unit of a cell's value, such as `USD/EUR` (SPEC 7.1, units.md).
33
36
  - `keys_from_response(host, status, body) -> KeysResult`: interprets a response for `https://<host>/.well-known/datumesh.json` (SPEC 9.3).
34
37
  - `fetch_keys(host, timeout=10.0) -> KeysResult`: the only function that uses the network. One GET, TLS verified, redirects not followed.
35
38
  - `canonicalize(obj, kind=None) -> bytes`, `cell_hash`, `function_hash`, `receipt_hash`, `receipt_payload`, `jcs`: validation, RFC 8785 canonical bytes and SHA-256 hashes. Kinds `"slice"` and `"manifest"`, with `slice_id`, `members_hash` and `slice_entry`, cover slice objects and manifests (SPEC 4.4, 15.13).
36
39
  - `parse_uri(uri, signature)`, `format_uri(ref)`, `uri_to_https(uri, mesh)`: cell URIs (SPEC 5).
37
- - `DatumeshError` (with `.code` and `.path`), `ERROR_CODES`, `SUCCESS_CODES`, `FAULT_CODES`, `SPEC_VERSION`, `UNITS`, `CURRENCIES`.
40
+ - `DatumeshError` (with `.code` and `.path`), `ERROR_CODES`, `SUCCESS_CODES`, `FAULT_CODES`, `FOREIGN_DETAILS`, `REASONS`, `SPEC_VERSION`, `UNITS`, `CURRENCIES`.
41
+
42
+ Changes are listed in [CHANGELOG.md](CHANGELOG.md).
38
43
 
39
44
  Requires Python 3.10 or later and PyNaCl. String rules use Unicode 15.1 tables shipped with the package, so results do not depend on the Python version's own Unicode database; the unit list is copied from the specification's `units.md`. Choosing which publishers to trust is up to you: a valid signature only means the key holder made the statement.
40
45
 
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datumesh"
7
- version = "0.4.2"
8
- description = "Verify Datumesh receipts (spec-0.4.2): RFC 8785 canonical JSON, Ed25519 signatures, slice-as-lineage, blobs, units, cell URIs, response envelopes (pending jobs, pins, slice pages, batches), key documents, mesh documents."
7
+ version = "0.4.3"
8
+ description = "Verify Datumesh receipts (spec-0.4.3): RFC 8785 canonical JSON, Ed25519 signatures, slice-as-lineage, blobs, units, cell URIs, response envelopes (pending jobs, pins, slice pages, batches), key documents with declarations, mesh documents, licence inheritance and entitlement."
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.4.2", "receipt", "verification", "ed25519", "rfc8785", "jcs", "canonical-json"]
14
+ keywords = ["datumesh", "spec-0.4.3", "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.4.2)" = "https://github.com/datumesh-com/datumesh-spec/blob/spec-0.4.2/SPEC.md"
33
+ "Specification (spec-0.4.3)" = "https://github.com/datumesh-com/datumesh-spec/blob/spec-0.4.3/SPEC.md"
34
34
 
35
35
  [tool.setuptools.packages.find]
36
36
  where = ["src"]
@@ -1,12 +1,15 @@
1
1
  """datumesh: verify Datumesh receipts (signed answers for addressable data cells).
2
2
 
3
- Implements the Datumesh protocol specification, revision ``spec-0.4.2``
3
+ Implements the Datumesh protocol specification, revision ``spec-0.4.3``
4
4
  (https://github.com/datumesh-com/datumesh-spec), as a verifier library: RFC 8785
5
5
  canonical JSON, SHA-256 hashes, Ed25519 receipt signatures, blob content, units,
6
6
  cell URIs (pins included), response envelopes, slice pages and batches, key
7
7
  documents, and slice-as-lineage (slice entries,
8
8
  slice objects and manifests), and, separately from verification, cross-mesh
9
- resolution of a host's mesh document (:func:`resolve_mesh`). It evaluates no
9
+ resolution of a host's mesh document (:func:`resolve_mesh`), and since spec-0.4.3 the
10
+ publisher's ``declarations`` in key documents, licence inheritance (:func:`inherit_licence`), the
11
+ licence entitlement check (:func:`check_entitlement`) and unrooted namespaces
12
+ (:func:`is_unrooted`). It evaluates no
10
13
  function, formulas included: the evaluated value comes from the caller. Only
11
14
  :func:`fetch_keys` touches the network.
12
15
  """
@@ -18,16 +21,18 @@ from typing import Any, Dict, Optional
18
21
 
19
22
  from . import _model, _units
20
23
  from ._envelope import TRUST, BatchResult, EnvelopeResult, MemberResult, check_batch, check_envelope
21
- from ._errors import ERROR_CODES, FAULT_CODES, SUCCESS_CODES, DatumeshError, fail
24
+ from ._errors import ERROR_CODES, FAULT_CODES, FOREIGN_DETAILS, SUCCESS_CODES, DatumeshError, fail
22
25
  from ._json import jcs_bytes
23
- from ._keys import Key, KeysResult, fetch_keys, key_id, keys_from_response
26
+ from ._keys import Declaration, Key, KeysResult, fetch_keys, key_id, keys_from_response
27
+ from ._licence import (ATTRIBUTION, ENTITLED_REASONS, PUBLIC_DOMAIN, REASONS, SHARE_ALIKE, EntitlementResult,
28
+ LicenceResult, check_entitlement, inherit_licence, input_licences, is_unrooted, licence_tier)
24
29
  from ._mesh import MESH_ENDPOINTS, MESH_WELL_KNOWN_PATH, MeshResult, resolve_mesh
25
30
  from ._uri import format_uri, parse_uri, uri_to_https
26
31
  from ._verify import VerifyResult, load, payload_bytes, verify
27
32
 
28
- __version__ = "0.4.2"
33
+ __version__ = "0.4.3"
29
34
  #: The specification revision this package conforms to.
30
- SPEC_VERSION = "spec-0.4.2"
35
+ SPEC_VERSION = "spec-0.4.3"
31
36
  #: Every literal unit listed in units.md (index units, ``index:<period>=<value>``, are a pattern and not listed).
32
37
  UNITS = tuple(sorted(_units.LITERALS))
33
38
  #: The currency codes of units.md 2.1.
@@ -39,6 +44,20 @@ KINDS = ("scalar", "cell", "ref", "function", "receipt", "slice", "manifest")
39
44
 
40
45
  __all__ = [
41
46
  "SPEC_VERSION",
47
+ "ATTRIBUTION",
48
+ "Declaration",
49
+ "ENTITLED_REASONS",
50
+ "EntitlementResult",
51
+ "FOREIGN_DETAILS",
52
+ "LicenceResult",
53
+ "PUBLIC_DOMAIN",
54
+ "REASONS",
55
+ "SHARE_ALIKE",
56
+ "check_entitlement",
57
+ "inherit_licence",
58
+ "input_licences",
59
+ "is_unrooted",
60
+ "licence_tier",
42
61
  "BatchResult",
43
62
  "MemberResult",
44
63
  "TRUST",
@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Mapping, NoReturn, Optional, Sequence, Tuple
11
11
  from . import _lexical as lx
12
12
  from . import _model
13
13
  from . import _units
14
- from ._errors import ERROR_CODES, FAULT_CODES, DatumeshError, fail
14
+ from ._errors import ERROR_CODES, FAULT_CODES, FOREIGN_DETAILS, DatumeshError, fail
15
15
  from ._json import jcs_bytes, number_tree, numbers_of, parse_json
16
16
  from ._keys import _Source, key_sources
17
17
  from ._verify import _signed
@@ -25,7 +25,7 @@ OUTCOMES: Dict[str, Tuple[range, Tuple[str, ...], Tuple[str, ...]]] = {
25
25
  "payment_required": (range(402, 403), ("quote",), ("fn_hash",)),
26
26
  "rate_limited": (range(429, 430), ("retry_after_seconds",), ("fn_hash",)),
27
27
  "bad_request": (range(400, 401), ("error",), ("fn_hash",)),
28
- "error": (range(500, 600), ("error",), ("fn_hash",)),
28
+ "error": (range(500, 600), ("error",), ("detail", "fn_hash")), # detail: spec-0.4.3
29
29
  "pending": (range(202, 203), ("estimate_seconds", "job", "poll", "request"), ()), # spec-0.4.1
30
30
  }
31
31
  #: A page of the slice endpoint (SPEC 5.6, spec-0.4.2): an ``ok`` with these members instead.
@@ -51,6 +51,9 @@ LIMIT_NEXT = 512
51
51
  #: The trust values (SPEC 5.5); ``computed`` since spec-0.4.2.
52
52
  TRUST = ("primary", "verified", "unverified", "computed")
53
53
  MAX_SECONDS = 2 ** 53 - 1
54
+ #: spec-0.4.3: a foreign mesh could not be reached; an ``error`` envelope's code, with ``detail``, at HTTP 502.
55
+ FOREIGN = "E_FOREIGN_UNAVAILABLE"
56
+ FOREIGN_STATUS = 502
54
57
 
55
58
 
56
59
  @dataclass(frozen=True)
@@ -82,6 +85,9 @@ class EnvelopeResult:
82
85
  echoes (or None); ``error``, the code an ``error``, ``bad_request`` or ``not_covered``
83
86
  envelope carries (``E_NAME_UNBOUND`` for the last); and for a page of the slice
84
87
  endpoint, ``members`` (a :class:`MemberResult` per member, in order) and ``next``.
88
+ ``error_detail`` (spec-0.4.3): with ``E_FOREIGN_UNAVAILABLE``, the envelope's ``detail`` (one of
89
+ ``FOREIGN_DETAILS``: why the foreign mesh could not be reached); else None. (``detail`` is this result's
90
+ human-readable text, as before.)
85
91
  """
86
92
 
87
93
  ok: bool
@@ -99,6 +105,7 @@ class EnvelopeResult:
99
105
  error: Optional[str] = None
100
106
  members: Tuple[MemberResult, ...] = ()
101
107
  next: Optional[str] = None
108
+ error_detail: Optional[str] = None
102
109
 
103
110
 
104
111
  @dataclass(frozen=True)
@@ -212,10 +219,15 @@ def _members(env: Any, numbers: Dict[str, str], path: str, at_slice: bool, membe
212
219
  _bad("reason is only present with superseded_by", f"{path}.reason")
213
220
  if "fault" in env and "error" in env:
214
221
  _bad("not_covered carries fault or error, not both", f"{path}.error")
222
+ if (env.get("error") == FOREIGN) != ("detail" in env): # spec-0.4.3
223
+ _bad("detail is present exactly when error is E_FOREIGN_UNAVAILABLE", f"{path}.detail")
215
224
  for name in sorted(env): # JCS order (all names are ASCII)
216
225
  v, p = env[name], f"{path}.{name}"
217
226
  if name in ("age_seconds", "retry_after_seconds", "estimate_seconds"):
218
227
  _seconds(v, numbers.get(name), p)
228
+ elif name == "detail":
229
+ if not isinstance(v, str) or v not in FOREIGN_DETAILS:
230
+ _bad(f"detail is one of {', '.join(FOREIGN_DETAILS)}", p)
219
231
  elif name == "error":
220
232
  if outcome == "bad_request":
221
233
  allowed_codes = BAD_REQUEST_CODES | {"E_JOB_UNKNOWN"}
@@ -391,6 +403,9 @@ def _check_parsed(status: Any, env_v: Any, tree: Any, sources: Tuple[_Source, ..
391
403
  _bad("a batch is refused whole only by a refusal outcome", f"{path}.outcome")
392
404
  if isinstance(status, bool) or not isinstance(status, int) or status not in OUTCOMES[outcome][0]:
393
405
  _bad(f"HTTP status {status!r} does not match outcome {outcome!r}", "status" if path == "$" else path)
406
+ if env.get("error") == FOREIGN and status != FOREIGN_STATUS: # spec-0.4.3
407
+ _bad(f"E_FOREIGN_UNAVAILABLE is answered with HTTP {FOREIGN_STATUS}, not {status}",
408
+ "status" if path == "$" else path)
394
409
  # step 3a (SPEC 5.5, a468606): what the request decides without the receipt, in this order
395
410
  if env.get("error") == "E_JOB_UNKNOWN" and req.job is None:
396
411
  _bad("E_JOB_UNKNOWN answers a job URL only, not a /cell request", f"{path}.error")
@@ -435,12 +450,13 @@ def _check_parsed(status: Any, env_v: Any, tree: Any, sources: Tuple[_Source, ..
435
450
  fault, error = env.get("fault"), env.get("error")
436
451
  detail = "well-formed envelope" + ("; receipt signature-valid" if h else "") + (
437
452
  f"; evaluation fault {fault}" if fault else "") + (f"; job {env['job']}" if outcome == "pending" else "") + (
438
- f"; a page of {len(members)} members" if page else "")
453
+ f"; a page of {len(members)} members" if page else "") + (
454
+ f"; foreign mesh unavailable ({env['detail']})" if "detail" in env else "")
439
455
  if outcome == "pending":
440
456
  return EnvelopeResult(True, "OK", outcome, None, None, detail, None, None, env["job"],
441
457
  int(numbers_of(tree)["estimate_seconds"]), env["poll"])
442
458
  return EnvelopeResult(True, "OK", outcome, trust, h, detail, None, fault, None, None, None, env.get("fn_hash"),
443
- error, members, env.get("next"))
459
+ error, members, env.get("next"), env.get("detail"))
444
460
 
445
461
 
446
462
  def _check_page(env: Dict[str, Any], tree: Any, sources: Tuple[_Source, ...], req: _Request,
@@ -6,9 +6,10 @@ from typing import NoReturn, Optional
6
6
 
7
7
  #: Every error code SPEC.md 10.2 defines, in the order of its table. Two, ``E_EVAL_UNAVAILABLE`` and
8
8
  #: ``E_INPUT_SUPERSEDED``, are not failures: :func:`verify` reports them with success (SPEC 10.1 step 7.6).
9
- #: The last four (spec-0.4.1 and spec-0.4.2) are not verification results: ``E_JOB_UNKNOWN``,
10
- #: ``E_SLICE_AT_CELL`` and ``E_NAME_UNBOUND`` are a mesh's answers about a request, in an envelope (5.5), and
11
- #: ``E_MESH_DOC`` a result of cross-mesh resolution (9.6).
9
+ #: The last five (spec-0.4.1, spec-0.4.2 and spec-0.4.3) are not verification results: ``E_JOB_UNKNOWN``,
10
+ #: ``E_SLICE_AT_CELL`` and ``E_NAME_UNBOUND`` are a mesh's answers about a request, in an envelope (5.5),
11
+ #: ``E_MESH_DOC`` a result of cross-mesh resolution (9.6), and ``E_FOREIGN_UNAVAILABLE`` a mesh's or resolver's
12
+ #: answer, in an ``error`` envelope with HTTP 502, that a foreign mesh could not be reached (5.5).
12
13
  ERROR_CODES: tuple[str, ...] = (
13
14
  "E_PARSE_JSON",
14
15
  "E_PARSE_DUPLICATE_KEY",
@@ -58,6 +59,7 @@ ERROR_CODES: tuple[str, ...] = (
58
59
  "E_MESH_DOC",
59
60
  "E_SLICE_AT_CELL",
60
61
  "E_NAME_UNBOUND",
62
+ "E_FOREIGN_UNAVAILABLE",
61
63
  )
62
64
 
63
65
 
@@ -70,6 +72,11 @@ FAULT_CODES: tuple[str, ...] = ("E_FORMULA_DIV_ZERO", "E_FORMULA_DOMAIN", "E_FOR
70
72
  "E_FORMULA_ABSENT", "E_FORMULA_FUTURE")
71
73
 
72
74
 
75
+ #: The ``detail`` an ``error`` envelope carries with ``E_FOREIGN_UNAVAILABLE``, and only with it (SPEC 5.5,
76
+ #: spec-0.4.3): why the foreign mesh could not be reached.
77
+ FOREIGN_DETAILS: tuple[str, ...] = ("offline", "network", "dns", "tls", "timeout", "refused")
78
+
79
+
73
80
  class DatumeshError(ValueError):
74
81
  """An input broke a rule of the Datumesh specification.
75
82
 
@@ -8,7 +8,8 @@ import ssl
8
8
  import urllib.error
9
9
  import urllib.request
10
10
  from dataclasses import dataclass, field
11
- from typing import Any, Dict, Optional, Tuple, Union
11
+ from types import MappingProxyType
12
+ from typing import Any, Dict, List, Mapping, Optional, Tuple, Union
12
13
 
13
14
  from . import _lexical as lx
14
15
  from ._errors import DatumeshError, fail
@@ -44,6 +45,32 @@ class Key:
44
45
  return self.valid_to is None or issued < self.valid_to
45
46
 
46
47
 
48
+ @dataclass(frozen=True)
49
+ class Declaration:
50
+ """One entry of a key document's ``declarations`` (SPEC 9.2, spec-0.4.3): the publisher permits its function
51
+ to be computed over inputs under ``licence`` (a restricted licence, 8.5) from ``valid_from`` until
52
+ ``valid_to`` (None: open-ended)."""
53
+
54
+ licence: str
55
+ valid_from: str
56
+ valid_to: Optional[str] = None
57
+
58
+ def open_at(self, issued: str) -> bool:
59
+ """SPEC 8.5 step 4, as a key's window (9.5): ``valid_from <= issued`` and, if present, ``issued < valid_to``."""
60
+ return self.valid_from <= issued and (self.valid_to is None or issued < self.valid_to)
61
+
62
+ def as_dict(self) -> Dict[str, str]:
63
+ """The entry as the key document writes it."""
64
+ d = {"licence": self.licence, "valid_from": self.valid_from}
65
+ if self.valid_to is not None:
66
+ d["valid_to"] = self.valid_to
67
+ return d
68
+
69
+
70
+ #: A key document's declarations: full function name to its entries, in document order.
71
+ Declarations = Mapping[str, Tuple[Declaration, ...]]
72
+
73
+
47
74
  @dataclass(frozen=True)
48
75
  class KeysResult:
49
76
  """The outcome of resolving a host's keys (SPEC 9.3).
@@ -51,6 +78,9 @@ class KeysResult:
51
78
  ``ok`` is True and ``code`` is ``"OK"`` when ``keys`` holds the host's
52
79
  validated keys; otherwise ``code`` is ``E_KEY_UNAVAILABLE`` or ``E_KEY_DOC``
53
80
  and ``keys`` is empty. ``host`` is the host the keys belong to.
81
+ ``declarations`` (spec-0.4.3): the document's ``declarations`` (SPEC 9.2), a read-only mapping from full
82
+ function name to a tuple of :class:`Declaration`, or None when the document has none (or is not ok).
83
+ They change nothing in verification.
54
84
  """
55
85
 
56
86
  ok: bool
@@ -58,21 +88,70 @@ class KeysResult:
58
88
  host: Optional[str]
59
89
  detail: str = ""
60
90
  keys: Tuple[Key, ...] = field(default_factory=tuple)
91
+ declarations: Optional[Declarations] = field(default=None, hash=False)
61
92
 
62
93
  @property
63
94
  def key_ids(self) -> Tuple[str, ...]:
64
95
  return tuple(k.id for k in self.keys)
65
96
 
97
+ def declarations_json(self) -> Optional[Dict[str, List[Dict[str, str]]]]:
98
+ """The declarations as the document gives them (a ``keys`` vector's output), or None."""
99
+ if self.declarations is None:
100
+ return None
101
+ return {name: [e.as_dict() for e in entries] for name, entries in self.declarations.items()}
102
+
66
103
 
67
104
  def _doc_error(host: Optional[str], detail: str) -> KeysResult:
68
105
  return KeysResult(False, "E_KEY_DOC", host, detail)
69
106
 
70
107
 
108
+ def declaration_entries(entries: Any, path: str) -> Tuple[Declaration, ...]:
109
+ """One declared name's entries (SPEC 9.2): an array of 1 to 16 objects with exactly ``licence`` (8.1),
110
+ ``valid_from`` and optionally ``valid_to`` (instants, ``valid_to`` later), all strings. Raises
111
+ DatumeshError (the caller maps it to E_KEY_DOC)."""
112
+ if not isinstance(entries, list) or not 1 <= len(entries) <= lx.LIMIT_DECLARATION_ENTRIES:
113
+ fail("E_SCHEMA", f"a declared name has an array of 1 to {lx.LIMIT_DECLARATION_ENTRIES} entries", path)
114
+ out = []
115
+ for i, e in enumerate(entries):
116
+ p = f"{path}[{i}]"
117
+ require_object(e, p, ("licence", "valid_from"), ("valid_to",))
118
+ for name in ("licence", "valid_from", "valid_to"):
119
+ if name in e and not isinstance(e[name], str):
120
+ fail("E_SCHEMA", f"{name} must be a string", f"{p}.{name}")
121
+ lx.check_licence(e["licence"], f"{p}.licence")
122
+ lx.check_instant(e["valid_from"], f"{p}.valid_from")
123
+ valid_to = e.get("valid_to")
124
+ if valid_to is not None:
125
+ lx.check_instant(valid_to, f"{p}.valid_to")
126
+ if not valid_to > e["valid_from"]:
127
+ fail("E_SCHEMA", "valid_to must be later than valid_from", f"{p}.valid_to")
128
+ out.append(Declaration(e["licence"], e["valid_from"], valid_to))
129
+ return tuple(out)
130
+
131
+
132
+ def _declarations(v: Any, doc_host: str) -> Declarations:
133
+ """SPEC 9.2 (spec-0.4.3): ``declarations`` is an object of 1 to 64 members, each a full function name (2.3)
134
+ whose namespace's host is ``doc_host`` exactly (a subdomain is another host)."""
135
+ if not isinstance(v, dict) or not 1 <= len(v) <= lx.LIMIT_DECLARATIONS:
136
+ fail("E_SCHEMA", f"declarations is an object of 1 to {lx.LIMIT_DECLARATIONS} function names",
137
+ "$.declarations")
138
+ out: Dict[str, Tuple[Declaration, ...]] = {}
139
+ for name, entries in v.items():
140
+ p = f"$.declarations[{name!r}]"
141
+ lx.check_function_name(name, p)
142
+ if lx.host_of(name) != doc_host:
143
+ fail("E_SCHEMA", f"{name!r} is not a function under the document's host {doc_host!r}", p)
144
+ out[name] = declaration_entries(entries, p)
145
+ return MappingProxyType(out)
146
+
147
+
71
148
  def validate_key_document(doc: Any, host: Optional[str]) -> KeysResult:
72
149
  """Check a parsed key document against SPEC 9.2. ``host`` is the host it was
73
- fetched from (None: take the document's own ``host`` member)."""
150
+ fetched from (None: take the document's own ``host`` member). The optional
151
+ ``declarations`` (spec-0.4.3) are checked with the rest: anything malformed
152
+ rejects the whole document (E_KEY_DOC)."""
74
153
  try:
75
- require_object(doc, "$", ("host", "keys", "proto"))
154
+ require_object(doc, "$", ("host", "keys", "proto"), ("declarations",))
76
155
  if doc["proto"] != PROTO:
77
156
  fail("E_SCHEMA", f"proto must be {PROTO!r}", "$.proto")
78
157
  doc_host = doc["host"]
@@ -110,7 +189,8 @@ def validate_key_document(doc: Any, host: Optional[str]) -> KeysResult:
110
189
  seen_ids.add(entry["id"])
111
190
  seen_public.add(entry["public"])
112
191
  out.append(Key(entry["id"], entry["public"], entry["valid_from"], valid_to))
113
- return KeysResult(True, "OK", doc_host, "", tuple(out))
192
+ decls = _declarations(doc["declarations"], doc_host) if "declarations" in doc else None
193
+ return KeysResult(True, "OK", doc_host, "", tuple(out), decls)
114
194
  except DatumeshError as e:
115
195
  where = f" at {e.path}" if e.path else ""
116
196
  return _doc_error(host if host is not None else (doc.get("host") if isinstance(doc, dict) and isinstance(doc.get("host"), str) else None),
@@ -31,6 +31,8 @@ LIMIT_LICENCE_URL = 512
31
31
  LIMIT_KEYS = 64
32
32
  LIMIT_EXTENT = 4096 # values in an argument's extent (spec-0.4.0)
33
33
  LIMIT_MEMBERS = 1048576 # member receipt hashes in a slice manifest (spec-0.4.0)
34
+ LIMIT_DECLARATIONS = 64 # function names in a key document's declarations (spec-0.4.3)
35
+ LIMIT_DECLARATION_ENTRIES = 16 # entries for one declared name (spec-0.4.3)
34
36
  LIMIT_DNS = 253
35
37
  LIMIT_BLOB_LENGTH = 2 ** 53 - 1
36
38
  LIMIT_MEDIA_TYPE = 128