Coverage for dataexcept/schema.py: 100%
23 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-27 14:44 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-27 14:44 +0000
1"""The published, versioned schemas for structured exception payloads.
3:func:`dataexcept.exception_to_dict` has produced envelopes since 1.2.0, and
4they grew: ``exceptions`` for group members in 1.3.0, the ``failure`` object in
51.4.0. Every one of those fields was described only in prose. Prose is not
6something a Node.js or Go consumer can test against, and a field whose meaning
7is implied by one implementation drifts the moment that implementation
8changes.
10The schemas shipped here are those contracts, written down and versioned
11independently of the package: they describe the payloads, not the release that
12happened to emit them. A version moves only when its payload changes, under the
131.x rule that a later version may add fields but does not silently change what
14an established field means.
16Two documents are published. The envelope schema is the canonical contract. The
17Pino profile describes the projection of an envelope onto the error key of a
18Pino log record, for Node.js services consuming these payloads.
19"""
21from __future__ import annotations
23import json
24from copy import deepcopy
25from functools import lru_cache
26from importlib import resources
27from typing import Any
29__all__ = [
30 "ENVELOPE_SCHEMA_ID",
31 "ENVELOPE_SCHEMA_VERSION",
32 "PINO_PROFILE_ID",
33 "PINO_PROFILE_VERSION",
34 "envelope_schema",
35 "pino_profile_schema",
36]
38#: Version of the envelope contract, not of this package.
39ENVELOPE_SCHEMA_VERSION = "1.0.0"
41#: Version of the Pino projection, which is versioned separately again: the
42#: profile can gain a field without the envelope changing, and vice versa.
43PINO_PROFILE_VERSION = "1.0.0"
45_PUBLISHED_AT = "https://diogoribeiro7.github.io/DataExcept/schema"
46_ENVELOPE_FILENAME = f"envelope-{ENVELOPE_SCHEMA_VERSION}.json"
47_PINO_FILENAME = f"pino-{PINO_PROFILE_VERSION}.json"
49#: Canonical location of the envelope schema, and the value of its ``$id``. The
50#: document is published there so a consumer in another language can fetch it
51#: without installing a Python package.
52ENVELOPE_SCHEMA_ID = f"{_PUBLISHED_AT}/{_ENVELOPE_FILENAME}"
54#: Canonical location of the Pino profile, and the value of its ``$id``.
55PINO_PROFILE_ID = f"{_PUBLISHED_AT}/{_PINO_FILENAME}"
58@lru_cache(maxsize=None)
59def _loaded_schema(filename: str) -> dict[str, Any]:
60 """Read one of the schema documents that ship with the package."""
61 document = resources.files("dataexcept").joinpath("schemas").joinpath(filename)
62 parsed: dict[str, Any] = json.loads(document.read_text(encoding="utf-8"))
63 return parsed
66def envelope_schema() -> dict[str, Any]:
67 """Return the JSON Schema describing an exception envelope.
69 The result is a fresh copy on every call, so a caller that registers it
70 with a validator, or annotates it for an API specification, cannot corrupt
71 the copy the next caller gets.
72 """
73 return deepcopy(_loaded_schema(_ENVELOPE_FILENAME))
76def pino_profile_schema() -> dict[str, Any]:
77 """Return the JSON Schema describing the Pino projection of an envelope.
79 A fresh copy on every call, for the same reason as
80 :func:`envelope_schema`.
81 """
82 return deepcopy(_loaded_schema(_PINO_FILENAME))