Coverage for async_durable_execution/preview.py: 97%
121 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-30 23:43 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-08-30 23:43 +0000
1"""Build compact structured previews for externally stored payloads."""
3from __future__ import annotations
5import json
6from dataclasses import dataclass, field
7from enum import Enum
8from typing import Any
11class PreviewMode(str, Enum):
12 """Controls which fields are visible by default."""
14 INCLUDE_ALL = "INCLUDE_ALL"
15 EXCLUDE_ALL = "EXCLUDE_ALL"
18class FieldMatchMode(str, Enum):
19 """Controls how a preview selector matches a field."""
21 ANYWHERE = "ANYWHERE"
22 PATH = "PATH"
25@dataclass(frozen=True)
26class PreviewField:
27 """A field name or exact dot-separated path used by preview rules."""
29 name: str
30 match: FieldMatchMode = FieldMatchMode.ANYWHERE
32 def __post_init__(self) -> None:
33 if not self.name:
34 msg = "PreviewField.name must not be empty."
35 raise ValueError(msg)
36 if not isinstance(self.match, FieldMatchMode):
37 msg = "PreviewField.match must be a FieldMatchMode."
38 raise TypeError(msg)
41@dataclass(frozen=True)
42class PreviewConfig:
43 """Configuration for :func:`build_preview`."""
45 mode: PreviewMode
46 include: tuple[PreviewField, ...] = field(default_factory=tuple)
47 exclude: tuple[PreviewField, ...] = field(default_factory=tuple)
48 mask: tuple[PreviewField, ...] = field(default_factory=tuple)
49 mask_string: str = "***"
50 max_preview_bytes: int = 4096
51 max_traversal_nodes: int = 10_000
52 max_depth: int = 64
54 def __post_init__(self) -> None:
55 if not isinstance(self.mode, PreviewMode):
56 msg = "PreviewConfig.mode must be a PreviewMode."
57 raise TypeError(msg)
58 if self.max_preview_bytes <= 0:
59 msg = "max_preview_bytes must be positive."
60 raise ValueError(msg)
61 if self.max_traversal_nodes <= 0:
62 msg = "max_traversal_nodes must be positive."
63 raise ValueError(msg)
64 if self.max_depth <= 0:
65 msg = "max_depth must be positive."
66 raise ValueError(msg)
69def _field_matches(path: str, preview_field: PreviewField) -> bool:
70 if preview_field.match is FieldMatchMode.PATH:
71 return path == preview_field.name
72 return preview_field.name in path.split(".")
75def _is_matched(path: str, fields: tuple[PreviewField, ...]) -> bool:
76 return any(_field_matches(path, preview_field) for preview_field in fields)
79def build_preview(
80 value: Any,
81 config: PreviewConfig,
82) -> dict[str, Any] | None:
83 """Build a bounded nested preview from a mapping-like JSON value.
85 Exclusion wins over every other rule. Masking implies visibility unless
86 the field is excluded. Arrays are traversed and matching object fields are
87 merged into their containing preview path.
88 """
89 if not isinstance(value, dict):
90 return None
92 accepted: dict[str, Any] = {}
93 visited_nodes = 0
94 stopped = False
96 def visit_node() -> bool:
97 nonlocal stopped, visited_nodes
98 visited_nodes += 1
99 if visited_nodes > config.max_traversal_nodes:
100 stopped = True
101 return not stopped
103 def add_value(path: str, preview_value: Any) -> None:
104 nonlocal stopped
105 missing = object()
106 previous = accepted.get(path, missing)
107 accepted[path] = preview_value
108 candidate_preview = _paths_to_nested_dict(accepted)
109 encoded = json.dumps(
110 candidate_preview,
111 ensure_ascii=False,
112 separators=(",", ":"),
113 ).encode("utf-8")
114 if len(encoded) <= config.max_preview_bytes:
115 return
117 if previous is missing: 117 ↛ 120line 117 didn't jump to line 120 because the condition on line 117 was always true
118 del accepted[path]
119 else:
120 accepted[path] = previous
121 stopped = True
123 def collect(current: Any, path_prefix: str, depth: int) -> None:
124 nonlocal stopped
125 if stopped or depth > config.max_depth or not visit_node(): 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true
126 return
127 if isinstance(current, list):
128 for item in current:
129 collect(item, path_prefix, depth + 1)
130 if stopped:
131 break
132 return
133 if not isinstance(current, dict): 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true
134 return
136 for raw_key, child in current.items():
137 if stopped or not visit_node():
138 break
139 key = str(raw_key)
140 if "." in key:
141 continue
143 path = f"{path_prefix}.{key}" if path_prefix else key
144 excluded = _is_matched(path, config.exclude)
145 masked = _is_matched(path, config.mask)
146 visible = not excluded and (
147 masked
148 or config.mode is PreviewMode.INCLUDE_ALL
149 or _is_matched(path, config.include)
150 )
152 if not visible:
153 if not excluded and isinstance(child, dict | list):
154 collect(child, path, depth + 1)
155 continue
156 if masked:
157 add_value(path, config.mask_string)
158 elif isinstance(child, dict | list):
159 collect(child, path, depth + 1)
160 else:
161 add_value(path, child)
163 collect(value, "", 0)
164 if not accepted:
165 return None
166 return _paths_to_nested_dict(accepted)
169def _paths_to_nested_dict(paths: dict[str, Any]) -> dict[str, Any]:
170 result: dict[str, Any] = {}
171 for path, value in paths.items():
172 parts = path.split(".")
173 node = result
174 for part in parts[:-1]:
175 existing = node.get(part)
176 if not isinstance(existing, dict):
177 existing = {}
178 node[part] = existing
179 node = existing
180 node[parts[-1]] = value
181 return result