Coverage for dataexcept/schema.py: 100%

23 statements  

« 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. 

2 

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. 

9 

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. 

15 

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""" 

20 

21from __future__ import annotations 

22 

23import json 

24from copy import deepcopy 

25from functools import lru_cache 

26from importlib import resources 

27from typing import Any 

28 

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] 

37 

38#: Version of the envelope contract, not of this package. 

39ENVELOPE_SCHEMA_VERSION = "1.0.0" 

40 

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" 

44 

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" 

48 

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}" 

53 

54#: Canonical location of the Pino profile, and the value of its ``$id``. 

55PINO_PROFILE_ID = f"{_PUBLISHED_AT}/{_PINO_FILENAME}" 

56 

57 

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 

64 

65 

66def envelope_schema() -> dict[str, Any]: 

67 """Return the JSON Schema describing an exception envelope. 

68 

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)) 

74 

75 

76def pino_profile_schema() -> dict[str, Any]: 

77 """Return the JSON Schema describing the Pino projection of an envelope. 

78 

79 A fresh copy on every call, for the same reason as 

80 :func:`envelope_schema`. 

81 """ 

82 return deepcopy(_loaded_schema(_PINO_FILENAME))