tnfr 6.0.0__py3-none-any.whl → 7.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of tnfr might be problematic. Click here for more details.
- tnfr/__init__.py +50 -5
- tnfr/__init__.pyi +0 -7
- tnfr/_compat.py +0 -1
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +44 -2
- tnfr/alias.py +14 -13
- tnfr/alias.pyi +5 -37
- tnfr/cache.py +9 -729
- tnfr/cache.pyi +8 -224
- tnfr/callback_utils.py +16 -31
- tnfr/callback_utils.pyi +3 -29
- tnfr/cli/__init__.py +17 -11
- tnfr/cli/__init__.pyi +0 -21
- tnfr/cli/arguments.py +175 -14
- tnfr/cli/arguments.pyi +5 -11
- tnfr/cli/execution.py +434 -48
- tnfr/cli/execution.pyi +14 -24
- tnfr/cli/utils.py +20 -3
- tnfr/cli/utils.pyi +5 -5
- tnfr/config/__init__.py +2 -1
- tnfr/config/__init__.pyi +2 -0
- tnfr/config/feature_flags.py +83 -0
- tnfr/config/init.py +1 -1
- tnfr/config/operator_names.py +1 -14
- tnfr/config/presets.py +6 -26
- tnfr/constants/__init__.py +10 -13
- tnfr/constants/__init__.pyi +10 -22
- tnfr/constants/aliases.py +31 -0
- tnfr/constants/core.py +4 -3
- tnfr/constants/init.py +1 -1
- tnfr/constants/metric.py +3 -3
- tnfr/dynamics/__init__.py +64 -10
- tnfr/dynamics/__init__.pyi +3 -4
- tnfr/dynamics/adaptation.py +79 -13
- tnfr/dynamics/aliases.py +10 -9
- tnfr/dynamics/coordination.py +77 -35
- tnfr/dynamics/dnfr.py +575 -274
- tnfr/dynamics/dnfr.pyi +1 -10
- tnfr/dynamics/integrators.py +47 -33
- tnfr/dynamics/integrators.pyi +0 -1
- tnfr/dynamics/runtime.py +489 -129
- tnfr/dynamics/sampling.py +2 -0
- tnfr/dynamics/selectors.py +101 -62
- tnfr/execution.py +15 -8
- tnfr/execution.pyi +5 -25
- tnfr/flatten.py +7 -3
- tnfr/flatten.pyi +1 -8
- tnfr/gamma.py +22 -26
- tnfr/gamma.pyi +0 -6
- tnfr/glyph_history.py +37 -26
- tnfr/glyph_history.pyi +1 -19
- tnfr/glyph_runtime.py +16 -0
- tnfr/glyph_runtime.pyi +9 -0
- tnfr/immutable.py +20 -15
- tnfr/immutable.pyi +4 -7
- tnfr/initialization.py +5 -7
- tnfr/initialization.pyi +1 -9
- tnfr/io.py +6 -305
- tnfr/io.pyi +13 -8
- tnfr/mathematics/__init__.py +81 -0
- tnfr/mathematics/backend.py +426 -0
- tnfr/mathematics/dynamics.py +398 -0
- tnfr/mathematics/epi.py +254 -0
- tnfr/mathematics/generators.py +222 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/operators.py +233 -0
- tnfr/mathematics/operators_factory.py +71 -0
- tnfr/mathematics/projection.py +78 -0
- tnfr/mathematics/runtime.py +173 -0
- tnfr/mathematics/spaces.py +247 -0
- tnfr/mathematics/transforms.py +292 -0
- tnfr/metrics/__init__.py +10 -10
- tnfr/metrics/coherence.py +123 -94
- tnfr/metrics/common.py +22 -13
- tnfr/metrics/common.pyi +42 -11
- tnfr/metrics/core.py +72 -14
- tnfr/metrics/diagnosis.py +48 -57
- tnfr/metrics/diagnosis.pyi +3 -7
- tnfr/metrics/export.py +3 -5
- tnfr/metrics/glyph_timing.py +41 -31
- tnfr/metrics/reporting.py +13 -6
- tnfr/metrics/sense_index.py +884 -114
- tnfr/metrics/trig.py +167 -11
- tnfr/metrics/trig.pyi +1 -0
- tnfr/metrics/trig_cache.py +112 -15
- tnfr/node.py +400 -17
- tnfr/node.pyi +55 -38
- tnfr/observers.py +111 -8
- tnfr/observers.pyi +0 -15
- tnfr/ontosim.py +9 -6
- tnfr/ontosim.pyi +0 -5
- tnfr/operators/__init__.py +529 -42
- tnfr/operators/__init__.pyi +14 -0
- tnfr/operators/definitions.py +350 -18
- tnfr/operators/definitions.pyi +0 -14
- tnfr/operators/grammar.py +760 -0
- tnfr/operators/jitter.py +28 -22
- tnfr/operators/registry.py +7 -12
- tnfr/operators/registry.pyi +0 -2
- tnfr/operators/remesh.py +38 -61
- tnfr/rng.py +17 -300
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/selector.py +3 -4
- tnfr/selector.pyi +1 -1
- tnfr/sense.py +22 -24
- tnfr/sense.pyi +0 -7
- tnfr/structural.py +504 -21
- tnfr/structural.pyi +41 -18
- tnfr/telemetry/__init__.py +23 -1
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/nu_f.py +423 -0
- tnfr/telemetry/nu_f.pyi +123 -0
- tnfr/tokens.py +1 -4
- tnfr/tokens.pyi +1 -6
- tnfr/trace.py +20 -53
- tnfr/trace.pyi +9 -37
- tnfr/types.py +244 -15
- tnfr/types.pyi +200 -14
- tnfr/units.py +69 -0
- tnfr/units.pyi +16 -0
- tnfr/utils/__init__.py +107 -48
- tnfr/utils/__init__.pyi +80 -11
- tnfr/utils/cache.py +1705 -65
- tnfr/utils/cache.pyi +370 -58
- tnfr/utils/chunks.py +104 -0
- tnfr/utils/chunks.pyi +21 -0
- tnfr/utils/data.py +95 -5
- tnfr/utils/data.pyi +8 -17
- tnfr/utils/graph.py +2 -4
- tnfr/utils/init.py +31 -7
- tnfr/utils/init.pyi +4 -11
- tnfr/utils/io.py +313 -14
- tnfr/{helpers → utils}/numeric.py +50 -24
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +92 -4
- tnfr/validation/__init__.pyi +77 -17
- tnfr/validation/compatibility.py +79 -43
- tnfr/validation/compatibility.pyi +4 -6
- tnfr/validation/grammar.py +55 -133
- tnfr/validation/grammar.pyi +37 -8
- tnfr/validation/graph.py +138 -0
- tnfr/validation/graph.pyi +17 -0
- tnfr/validation/rules.py +161 -74
- tnfr/validation/rules.pyi +55 -18
- tnfr/validation/runtime.py +263 -0
- tnfr/validation/runtime.pyi +31 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +37 -0
- tnfr/validation/spectral.py +159 -0
- tnfr/validation/spectral.pyi +46 -0
- tnfr/validation/syntax.py +28 -139
- tnfr/validation/syntax.pyi +7 -4
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/viz/__init__.py +9 -0
- tnfr/viz/matplotlib.py +246 -0
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/METADATA +63 -19
- tnfr-7.0.0.dist-info/RECORD +185 -0
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/constants_glyphs.py +0 -16
- tnfr/constants_glyphs.pyi +0 -12
- tnfr/grammar.py +0 -25
- tnfr/grammar.pyi +0 -13
- tnfr/helpers/__init__.py +0 -151
- tnfr/helpers/__init__.pyi +0 -66
- tnfr/helpers/numeric.pyi +0 -12
- tnfr/presets.py +0 -15
- tnfr/presets.pyi +0 -7
- tnfr/utils/io.pyi +0 -10
- tnfr/utils/validators.py +0 -130
- tnfr/utils/validators.pyi +0 -19
- tnfr-6.0.0.dist-info/RECORD +0 -157
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
tnfr/cache.py
CHANGED
|
@@ -1,732 +1,12 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Legacy cache helpers module.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
from collections.abc import Iterable
|
|
8
|
-
from contextlib import contextmanager
|
|
9
|
-
from dataclasses import dataclass, field
|
|
10
|
-
from time import perf_counter
|
|
11
|
-
from typing import Any, Callable, Generic, Hashable, Iterator, Mapping, MutableMapping, TypeVar, cast
|
|
12
|
-
|
|
13
|
-
from cachetools import LRUCache
|
|
14
|
-
|
|
15
|
-
from .types import TimingContext
|
|
16
|
-
|
|
17
|
-
__all__ = [
|
|
18
|
-
"CacheManager",
|
|
19
|
-
"CacheCapacityConfig",
|
|
20
|
-
"CacheStatistics",
|
|
21
|
-
"InstrumentedLRUCache",
|
|
22
|
-
"ManagedLRUCache",
|
|
23
|
-
"prune_lock_mapping",
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
K = TypeVar("K", bound=Hashable)
|
|
28
|
-
V = TypeVar("V")
|
|
29
|
-
|
|
30
|
-
_logger = logging.getLogger(__name__)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@dataclass(frozen=True)
|
|
34
|
-
class CacheCapacityConfig:
|
|
35
|
-
"""Configuration snapshot for cache capacity policies."""
|
|
36
|
-
|
|
37
|
-
default_capacity: int | None
|
|
38
|
-
overrides: dict[str, int | None]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@dataclass(frozen=True)
|
|
42
|
-
class CacheStatistics:
|
|
43
|
-
"""Immutable snapshot of cache telemetry counters."""
|
|
44
|
-
|
|
45
|
-
hits: int = 0
|
|
46
|
-
misses: int = 0
|
|
47
|
-
evictions: int = 0
|
|
48
|
-
total_time: float = 0.0
|
|
49
|
-
timings: int = 0
|
|
50
|
-
|
|
51
|
-
def merge(self, other: CacheStatistics) -> CacheStatistics:
|
|
52
|
-
"""Return aggregated metrics combining ``self`` and ``other``."""
|
|
53
|
-
|
|
54
|
-
return CacheStatistics(
|
|
55
|
-
hits=self.hits + other.hits,
|
|
56
|
-
misses=self.misses + other.misses,
|
|
57
|
-
evictions=self.evictions + other.evictions,
|
|
58
|
-
total_time=self.total_time + other.total_time,
|
|
59
|
-
timings=self.timings + other.timings,
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@dataclass
|
|
64
|
-
class _CacheMetrics:
|
|
65
|
-
hits: int = 0
|
|
66
|
-
misses: int = 0
|
|
67
|
-
evictions: int = 0
|
|
68
|
-
total_time: float = 0.0
|
|
69
|
-
timings: int = 0
|
|
70
|
-
lock: threading.Lock = field(default_factory=threading.Lock, repr=False)
|
|
71
|
-
|
|
72
|
-
def snapshot(self) -> CacheStatistics:
|
|
73
|
-
return CacheStatistics(
|
|
74
|
-
hits=self.hits,
|
|
75
|
-
misses=self.misses,
|
|
76
|
-
evictions=self.evictions,
|
|
77
|
-
total_time=self.total_time,
|
|
78
|
-
timings=self.timings,
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@dataclass
|
|
83
|
-
class _CacheEntry:
|
|
84
|
-
factory: Callable[[], Any]
|
|
85
|
-
lock: threading.Lock
|
|
86
|
-
reset: Callable[[Any], Any] | None = None
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class CacheManager:
|
|
90
|
-
"""Coordinate named caches guarded by per-entry locks."""
|
|
91
|
-
|
|
92
|
-
_MISSING = object()
|
|
93
|
-
|
|
94
|
-
def __init__(
|
|
95
|
-
self,
|
|
96
|
-
storage: MutableMapping[str, Any] | None = None,
|
|
97
|
-
*,
|
|
98
|
-
default_capacity: int | None = None,
|
|
99
|
-
overrides: Mapping[str, int | None] | None = None,
|
|
100
|
-
) -> None:
|
|
101
|
-
self._storage: MutableMapping[str, Any]
|
|
102
|
-
if storage is None:
|
|
103
|
-
self._storage = {}
|
|
104
|
-
else:
|
|
105
|
-
self._storage = storage
|
|
106
|
-
self._entries: dict[str, _CacheEntry] = {}
|
|
107
|
-
self._registry_lock = threading.RLock()
|
|
108
|
-
self._default_capacity = self._normalise_capacity(default_capacity)
|
|
109
|
-
self._capacity_overrides: dict[str, int | None] = {}
|
|
110
|
-
self._metrics: dict[str, _CacheMetrics] = {}
|
|
111
|
-
self._metrics_publishers: list[Callable[[str, CacheStatistics], None]] = []
|
|
112
|
-
if overrides:
|
|
113
|
-
self.configure(overrides=overrides)
|
|
114
|
-
|
|
115
|
-
@staticmethod
|
|
116
|
-
def _normalise_capacity(value: int | None) -> int | None:
|
|
117
|
-
if value is None:
|
|
118
|
-
return None
|
|
119
|
-
size = int(value)
|
|
120
|
-
if size < 0:
|
|
121
|
-
raise ValueError("capacity must be non-negative or None")
|
|
122
|
-
return size
|
|
123
|
-
|
|
124
|
-
def register(
|
|
125
|
-
self,
|
|
126
|
-
name: str,
|
|
127
|
-
factory: Callable[[], Any],
|
|
128
|
-
*,
|
|
129
|
-
lock_factory: Callable[[], threading.Lock | threading.RLock] | None = None,
|
|
130
|
-
reset: Callable[[Any], Any] | None = None,
|
|
131
|
-
create: bool = True,
|
|
132
|
-
) -> None:
|
|
133
|
-
"""Register ``name`` with ``factory`` and optional lifecycle hooks."""
|
|
134
|
-
|
|
135
|
-
if lock_factory is None:
|
|
136
|
-
lock_factory = threading.RLock
|
|
137
|
-
with self._registry_lock:
|
|
138
|
-
entry = self._entries.get(name)
|
|
139
|
-
if entry is None:
|
|
140
|
-
entry = _CacheEntry(factory=factory, lock=lock_factory(), reset=reset)
|
|
141
|
-
self._entries[name] = entry
|
|
142
|
-
else:
|
|
143
|
-
# Update hooks when re-registering the same cache name.
|
|
144
|
-
entry.factory = factory
|
|
145
|
-
entry.reset = reset
|
|
146
|
-
self._ensure_metrics(name)
|
|
147
|
-
if create:
|
|
148
|
-
self.get(name)
|
|
149
|
-
|
|
150
|
-
def configure(
|
|
151
|
-
self,
|
|
152
|
-
*,
|
|
153
|
-
default_capacity: int | None | object = _MISSING,
|
|
154
|
-
overrides: Mapping[str, int | None] | None = None,
|
|
155
|
-
replace_overrides: bool = False,
|
|
156
|
-
) -> None:
|
|
157
|
-
"""Update the cache capacity policy shared by registered entries."""
|
|
158
|
-
|
|
159
|
-
with self._registry_lock:
|
|
160
|
-
if default_capacity is not self._MISSING:
|
|
161
|
-
self._default_capacity = self._normalise_capacity(
|
|
162
|
-
default_capacity if default_capacity is not None else None
|
|
163
|
-
)
|
|
164
|
-
if overrides is not None:
|
|
165
|
-
if replace_overrides:
|
|
166
|
-
self._capacity_overrides.clear()
|
|
167
|
-
for key, value in overrides.items():
|
|
168
|
-
self._capacity_overrides[key] = self._normalise_capacity(value)
|
|
169
|
-
|
|
170
|
-
def configure_from_mapping(self, config: Mapping[str, Any]) -> None:
|
|
171
|
-
"""Load configuration produced by :meth:`export_config`."""
|
|
172
|
-
|
|
173
|
-
default = config.get("default_capacity", self._MISSING)
|
|
174
|
-
overrides = config.get("overrides")
|
|
175
|
-
overrides_mapping: Mapping[str, int | None] | None
|
|
176
|
-
overrides_mapping = overrides if isinstance(overrides, Mapping) else None
|
|
177
|
-
self.configure(default_capacity=default, overrides=overrides_mapping)
|
|
178
|
-
|
|
179
|
-
def export_config(self) -> CacheCapacityConfig:
|
|
180
|
-
"""Return a copy of the current capacity configuration."""
|
|
181
|
-
|
|
182
|
-
with self._registry_lock:
|
|
183
|
-
return CacheCapacityConfig(
|
|
184
|
-
default_capacity=self._default_capacity,
|
|
185
|
-
overrides=dict(self._capacity_overrides),
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
def get_capacity(
|
|
189
|
-
self,
|
|
190
|
-
name: str,
|
|
191
|
-
*,
|
|
192
|
-
requested: int | None = None,
|
|
193
|
-
fallback: int | None = None,
|
|
194
|
-
use_default: bool = True,
|
|
195
|
-
) -> int | None:
|
|
196
|
-
"""Return capacity for ``name`` considering overrides and defaults."""
|
|
197
|
-
|
|
198
|
-
with self._registry_lock:
|
|
199
|
-
override = self._capacity_overrides.get(name, self._MISSING)
|
|
200
|
-
default = self._default_capacity
|
|
201
|
-
if override is not self._MISSING:
|
|
202
|
-
return override
|
|
203
|
-
values: tuple[int | None, ...]
|
|
204
|
-
if use_default:
|
|
205
|
-
values = (requested, default, fallback)
|
|
206
|
-
else:
|
|
207
|
-
values = (requested, fallback)
|
|
208
|
-
for value in values:
|
|
209
|
-
if value is self._MISSING:
|
|
210
|
-
continue
|
|
211
|
-
normalised = self._normalise_capacity(value)
|
|
212
|
-
if normalised is not None:
|
|
213
|
-
return normalised
|
|
214
|
-
return None
|
|
215
|
-
|
|
216
|
-
def has_override(self, name: str) -> bool:
|
|
217
|
-
"""Return ``True`` if ``name`` has an explicit capacity override."""
|
|
218
|
-
|
|
219
|
-
with self._registry_lock:
|
|
220
|
-
return name in self._capacity_overrides
|
|
221
|
-
|
|
222
|
-
def get_lock(self, name: str) -> threading.Lock | threading.RLock:
|
|
223
|
-
entry = self._entries.get(name)
|
|
224
|
-
if entry is None:
|
|
225
|
-
raise KeyError(name)
|
|
226
|
-
return entry.lock
|
|
227
|
-
|
|
228
|
-
def names(self) -> Iterator[str]:
|
|
229
|
-
with self._registry_lock:
|
|
230
|
-
return iter(tuple(self._entries))
|
|
231
|
-
|
|
232
|
-
def get(self, name: str, *, create: bool = True) -> Any:
|
|
233
|
-
entry = self._entries.get(name)
|
|
234
|
-
if entry is None:
|
|
235
|
-
raise KeyError(name)
|
|
236
|
-
with entry.lock:
|
|
237
|
-
value = self._storage.get(name)
|
|
238
|
-
if create and value is None:
|
|
239
|
-
value = entry.factory()
|
|
240
|
-
self._storage[name] = value
|
|
241
|
-
return value
|
|
242
|
-
|
|
243
|
-
def peek(self, name: str) -> Any:
|
|
244
|
-
return self.get(name, create=False)
|
|
245
|
-
|
|
246
|
-
def store(self, name: str, value: Any) -> None:
|
|
247
|
-
entry = self._entries.get(name)
|
|
248
|
-
if entry is None:
|
|
249
|
-
raise KeyError(name)
|
|
250
|
-
with entry.lock:
|
|
251
|
-
self._storage[name] = value
|
|
252
|
-
|
|
253
|
-
def update(
|
|
254
|
-
self,
|
|
255
|
-
name: str,
|
|
256
|
-
updater: Callable[[Any], Any],
|
|
257
|
-
*,
|
|
258
|
-
create: bool = True,
|
|
259
|
-
) -> Any:
|
|
260
|
-
entry = self._entries.get(name)
|
|
261
|
-
if entry is None:
|
|
262
|
-
raise KeyError(name)
|
|
263
|
-
with entry.lock:
|
|
264
|
-
current = self._storage.get(name)
|
|
265
|
-
if create and current is None:
|
|
266
|
-
current = entry.factory()
|
|
267
|
-
new_value = updater(current)
|
|
268
|
-
self._storage[name] = new_value
|
|
269
|
-
return new_value
|
|
270
|
-
|
|
271
|
-
def clear(self, name: str | None = None) -> None:
|
|
272
|
-
if name is not None:
|
|
273
|
-
names = (name,)
|
|
274
|
-
else:
|
|
275
|
-
with self._registry_lock:
|
|
276
|
-
names = tuple(self._entries)
|
|
277
|
-
for cache_name in names:
|
|
278
|
-
entry = self._entries.get(cache_name)
|
|
279
|
-
if entry is None:
|
|
280
|
-
continue
|
|
281
|
-
with entry.lock:
|
|
282
|
-
current = self._storage.get(cache_name)
|
|
283
|
-
new_value = None
|
|
284
|
-
if entry.reset is not None:
|
|
285
|
-
new_value = entry.reset(current)
|
|
286
|
-
if new_value is None:
|
|
287
|
-
try:
|
|
288
|
-
new_value = entry.factory()
|
|
289
|
-
except Exception:
|
|
290
|
-
self._storage.pop(cache_name, None)
|
|
291
|
-
continue
|
|
292
|
-
self._storage[cache_name] = new_value
|
|
293
|
-
|
|
294
|
-
# ------------------------------------------------------------------
|
|
295
|
-
# Metrics helpers
|
|
296
|
-
|
|
297
|
-
def _ensure_metrics(self, name: str) -> _CacheMetrics:
|
|
298
|
-
metrics = self._metrics.get(name)
|
|
299
|
-
if metrics is None:
|
|
300
|
-
with self._registry_lock:
|
|
301
|
-
metrics = self._metrics.get(name)
|
|
302
|
-
if metrics is None:
|
|
303
|
-
metrics = _CacheMetrics()
|
|
304
|
-
self._metrics[name] = metrics
|
|
305
|
-
return metrics
|
|
306
|
-
|
|
307
|
-
def increment_hit(
|
|
308
|
-
self,
|
|
309
|
-
name: str,
|
|
310
|
-
*,
|
|
311
|
-
amount: int = 1,
|
|
312
|
-
duration: float | None = None,
|
|
313
|
-
) -> None:
|
|
314
|
-
metrics = self._ensure_metrics(name)
|
|
315
|
-
with metrics.lock:
|
|
316
|
-
metrics.hits += int(amount)
|
|
317
|
-
if duration is not None:
|
|
318
|
-
metrics.total_time += float(duration)
|
|
319
|
-
metrics.timings += 1
|
|
320
|
-
|
|
321
|
-
def increment_miss(
|
|
322
|
-
self,
|
|
323
|
-
name: str,
|
|
324
|
-
*,
|
|
325
|
-
amount: int = 1,
|
|
326
|
-
duration: float | None = None,
|
|
327
|
-
) -> None:
|
|
328
|
-
metrics = self._ensure_metrics(name)
|
|
329
|
-
with metrics.lock:
|
|
330
|
-
metrics.misses += int(amount)
|
|
331
|
-
if duration is not None:
|
|
332
|
-
metrics.total_time += float(duration)
|
|
333
|
-
metrics.timings += 1
|
|
334
|
-
|
|
335
|
-
def increment_eviction(self, name: str, *, amount: int = 1) -> None:
|
|
336
|
-
metrics = self._ensure_metrics(name)
|
|
337
|
-
with metrics.lock:
|
|
338
|
-
metrics.evictions += int(amount)
|
|
339
|
-
|
|
340
|
-
def record_timing(self, name: str, duration: float) -> None:
|
|
341
|
-
metrics = self._ensure_metrics(name)
|
|
342
|
-
with metrics.lock:
|
|
343
|
-
metrics.total_time += float(duration)
|
|
344
|
-
metrics.timings += 1
|
|
345
|
-
|
|
346
|
-
@contextmanager
|
|
347
|
-
def timer(self, name: str) -> TimingContext:
|
|
348
|
-
"""Context manager recording execution time for ``name``."""
|
|
349
|
-
|
|
350
|
-
start = perf_counter()
|
|
351
|
-
try:
|
|
352
|
-
yield
|
|
353
|
-
finally:
|
|
354
|
-
self.record_timing(name, perf_counter() - start)
|
|
355
|
-
|
|
356
|
-
def get_metrics(self, name: str) -> CacheStatistics:
|
|
357
|
-
metrics = self._metrics.get(name)
|
|
358
|
-
if metrics is None:
|
|
359
|
-
return CacheStatistics()
|
|
360
|
-
with metrics.lock:
|
|
361
|
-
return metrics.snapshot()
|
|
3
|
+
This compatibility shim was removed in favour of :mod:`tnfr.utils.cache`.
|
|
4
|
+
Importing :mod:`tnfr.cache` now fails with a clear message so that callers
|
|
5
|
+
update their imports instead of relying on the removed re-export behaviour.
|
|
6
|
+
"""
|
|
362
7
|
|
|
363
|
-
|
|
364
|
-
with self._registry_lock:
|
|
365
|
-
items = tuple(self._metrics.items())
|
|
366
|
-
for name, metrics in items:
|
|
367
|
-
with metrics.lock:
|
|
368
|
-
yield name, metrics.snapshot()
|
|
369
|
-
|
|
370
|
-
def aggregate_metrics(self) -> CacheStatistics:
|
|
371
|
-
aggregate = CacheStatistics()
|
|
372
|
-
for _, stats in self.iter_metrics():
|
|
373
|
-
aggregate = aggregate.merge(stats)
|
|
374
|
-
return aggregate
|
|
375
|
-
|
|
376
|
-
def register_metrics_publisher(
|
|
377
|
-
self, publisher: Callable[[str, CacheStatistics], None]
|
|
378
|
-
) -> None:
|
|
379
|
-
with self._registry_lock:
|
|
380
|
-
self._metrics_publishers.append(publisher)
|
|
381
|
-
|
|
382
|
-
def publish_metrics(
|
|
383
|
-
self,
|
|
384
|
-
*,
|
|
385
|
-
publisher: Callable[[str, CacheStatistics], None] | None = None,
|
|
386
|
-
) -> None:
|
|
387
|
-
if publisher is None:
|
|
388
|
-
with self._registry_lock:
|
|
389
|
-
publishers = tuple(self._metrics_publishers)
|
|
390
|
-
else:
|
|
391
|
-
publishers = (publisher,)
|
|
392
|
-
if not publishers:
|
|
393
|
-
return
|
|
394
|
-
snapshot = tuple(self.iter_metrics())
|
|
395
|
-
for emit in publishers:
|
|
396
|
-
for name, stats in snapshot:
|
|
397
|
-
try:
|
|
398
|
-
emit(name, stats)
|
|
399
|
-
except Exception: # pragma: no cover - defensive logging
|
|
400
|
-
logging.getLogger(__name__).exception(
|
|
401
|
-
"Cache metrics publisher failed for %s", name
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
def log_metrics(self, logger: logging.Logger, *, level: int = logging.INFO) -> None:
|
|
405
|
-
"""Emit cache metrics using ``logger`` for telemetry hooks."""
|
|
406
|
-
|
|
407
|
-
for name, stats in self.iter_metrics():
|
|
408
|
-
logger.log(
|
|
409
|
-
level,
|
|
410
|
-
"cache=%s hits=%d misses=%d evictions=%d timings=%d total_time=%.6f",
|
|
411
|
-
name,
|
|
412
|
-
stats.hits,
|
|
413
|
-
stats.misses,
|
|
414
|
-
stats.evictions,
|
|
415
|
-
stats.timings,
|
|
416
|
-
stats.total_time,
|
|
417
|
-
)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
def _normalise_callbacks(
|
|
421
|
-
callbacks: Iterable[Callable[[K, V], None]] | Callable[[K, V], None] | None,
|
|
422
|
-
) -> tuple[Callable[[K, V], None], ...]:
|
|
423
|
-
if callbacks is None:
|
|
424
|
-
return ()
|
|
425
|
-
if callable(callbacks):
|
|
426
|
-
return (callbacks,)
|
|
427
|
-
return tuple(callbacks)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
def prune_lock_mapping(
|
|
431
|
-
cache: Mapping[K, Any] | MutableMapping[K, Any] | None,
|
|
432
|
-
locks: MutableMapping[K, Any] | None,
|
|
433
|
-
) -> None:
|
|
434
|
-
"""Drop lock entries not present in ``cache``."""
|
|
435
|
-
|
|
436
|
-
if locks is None:
|
|
437
|
-
return
|
|
438
|
-
if cache is None:
|
|
439
|
-
cache_keys: set[K] = set()
|
|
440
|
-
else:
|
|
441
|
-
cache_keys = set(cache.keys())
|
|
442
|
-
for key in list(locks.keys()):
|
|
443
|
-
if key not in cache_keys:
|
|
444
|
-
locks.pop(key, None)
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
class InstrumentedLRUCache(MutableMapping[K, V], Generic[K, V]):
|
|
448
|
-
"""LRU cache wrapper that synchronises telemetry, callbacks and locks.
|
|
449
|
-
|
|
450
|
-
The wrapper owns an internal :class:`cachetools.LRUCache` instance and
|
|
451
|
-
forwards all read operations to it. Mutating operations are instrumented to
|
|
452
|
-
update :class:`CacheManager` metrics, execute registered callbacks and keep
|
|
453
|
-
an optional lock mapping aligned with the stored keys. Telemetry callbacks
|
|
454
|
-
always execute before eviction callbacks, preserving the registration order
|
|
455
|
-
for deterministic side effects.
|
|
456
|
-
|
|
457
|
-
Callbacks can be extended or replaced after construction via
|
|
458
|
-
:meth:`set_telemetry_callbacks` and :meth:`set_eviction_callbacks`. When
|
|
459
|
-
``append`` is ``False`` (default) the provided callbacks replace the
|
|
460
|
-
existing sequence; otherwise they are appended at the end while keeping the
|
|
461
|
-
previous ordering intact.
|
|
462
|
-
"""
|
|
463
|
-
|
|
464
|
-
_MISSING = object()
|
|
465
|
-
|
|
466
|
-
def __init__(
|
|
467
|
-
self,
|
|
468
|
-
maxsize: int,
|
|
469
|
-
*,
|
|
470
|
-
manager: CacheManager | None = None,
|
|
471
|
-
metrics_key: str | None = None,
|
|
472
|
-
telemetry_callbacks: Iterable[Callable[[K, V], None]]
|
|
473
|
-
| Callable[[K, V], None]
|
|
474
|
-
| None = None,
|
|
475
|
-
eviction_callbacks: Iterable[Callable[[K, V], None]]
|
|
476
|
-
| Callable[[K, V], None]
|
|
477
|
-
| None = None,
|
|
478
|
-
locks: MutableMapping[K, Any] | None = None,
|
|
479
|
-
getsizeof: Callable[[V], int] | None = None,
|
|
480
|
-
count_overwrite_hit: bool = True,
|
|
481
|
-
) -> None:
|
|
482
|
-
self._cache: LRUCache[K, V] = LRUCache(maxsize, getsizeof=getsizeof)
|
|
483
|
-
original_popitem = self._cache.popitem
|
|
484
|
-
|
|
485
|
-
def _instrumented_popitem() -> tuple[K, V]:
|
|
486
|
-
key, value = original_popitem()
|
|
487
|
-
self._dispatch_removal(key, value)
|
|
488
|
-
return key, value
|
|
489
|
-
|
|
490
|
-
self._cache.popitem = _instrumented_popitem # type: ignore[assignment]
|
|
491
|
-
self._manager = manager
|
|
492
|
-
self._metrics_key = metrics_key
|
|
493
|
-
self._locks = locks
|
|
494
|
-
self._count_overwrite_hit = bool(count_overwrite_hit)
|
|
495
|
-
self._telemetry_callbacks: list[Callable[[K, V], None]]
|
|
496
|
-
self._telemetry_callbacks = list(_normalise_callbacks(telemetry_callbacks))
|
|
497
|
-
self._eviction_callbacks: list[Callable[[K, V], None]]
|
|
498
|
-
self._eviction_callbacks = list(_normalise_callbacks(eviction_callbacks))
|
|
499
|
-
|
|
500
|
-
# ------------------------------------------------------------------
|
|
501
|
-
# Callback registration helpers
|
|
502
|
-
|
|
503
|
-
@property
|
|
504
|
-
def telemetry_callbacks(self) -> tuple[Callable[[K, V], None], ...]:
|
|
505
|
-
"""Return currently registered telemetry callbacks."""
|
|
506
|
-
|
|
507
|
-
return tuple(self._telemetry_callbacks)
|
|
508
|
-
|
|
509
|
-
@property
|
|
510
|
-
def eviction_callbacks(self) -> tuple[Callable[[K, V], None], ...]:
|
|
511
|
-
"""Return currently registered eviction callbacks."""
|
|
512
|
-
|
|
513
|
-
return tuple(self._eviction_callbacks)
|
|
514
|
-
|
|
515
|
-
def set_telemetry_callbacks(
|
|
516
|
-
self,
|
|
517
|
-
callbacks: Iterable[Callable[[K, V], None]]
|
|
518
|
-
| Callable[[K, V], None]
|
|
519
|
-
| None,
|
|
520
|
-
*,
|
|
521
|
-
append: bool = False,
|
|
522
|
-
) -> None:
|
|
523
|
-
"""Update telemetry callbacks executed on removals.
|
|
524
|
-
|
|
525
|
-
When ``append`` is ``True`` the provided callbacks are added to the end
|
|
526
|
-
of the execution chain while preserving relative order. Otherwise, the
|
|
527
|
-
previous callbacks are replaced.
|
|
528
|
-
"""
|
|
529
|
-
|
|
530
|
-
new_callbacks = list(_normalise_callbacks(callbacks))
|
|
531
|
-
if append:
|
|
532
|
-
self._telemetry_callbacks.extend(new_callbacks)
|
|
533
|
-
else:
|
|
534
|
-
self._telemetry_callbacks = new_callbacks
|
|
535
|
-
|
|
536
|
-
def set_eviction_callbacks(
|
|
537
|
-
self,
|
|
538
|
-
callbacks: Iterable[Callable[[K, V], None]]
|
|
539
|
-
| Callable[[K, V], None]
|
|
540
|
-
| None,
|
|
541
|
-
*,
|
|
542
|
-
append: bool = False,
|
|
543
|
-
) -> None:
|
|
544
|
-
"""Update eviction callbacks executed on removals.
|
|
545
|
-
|
|
546
|
-
Behaviour matches :meth:`set_telemetry_callbacks`.
|
|
547
|
-
"""
|
|
548
|
-
|
|
549
|
-
new_callbacks = list(_normalise_callbacks(callbacks))
|
|
550
|
-
if append:
|
|
551
|
-
self._eviction_callbacks.extend(new_callbacks)
|
|
552
|
-
else:
|
|
553
|
-
self._eviction_callbacks = new_callbacks
|
|
554
|
-
|
|
555
|
-
# ------------------------------------------------------------------
|
|
556
|
-
# MutableMapping interface
|
|
557
|
-
|
|
558
|
-
def __getitem__(self, key: K) -> V:
|
|
559
|
-
return self._cache[key]
|
|
560
|
-
|
|
561
|
-
def __setitem__(self, key: K, value: V) -> None:
|
|
562
|
-
exists = key in self._cache
|
|
563
|
-
self._cache[key] = value
|
|
564
|
-
if exists:
|
|
565
|
-
if self._count_overwrite_hit:
|
|
566
|
-
self._record_hit(1)
|
|
567
|
-
else:
|
|
568
|
-
self._record_miss(1)
|
|
569
|
-
|
|
570
|
-
def __delitem__(self, key: K) -> None:
|
|
571
|
-
try:
|
|
572
|
-
value = self._cache[key]
|
|
573
|
-
except KeyError:
|
|
574
|
-
self._record_miss(1)
|
|
575
|
-
raise
|
|
576
|
-
del self._cache[key]
|
|
577
|
-
self._dispatch_removal(key, value, hits=1)
|
|
578
|
-
|
|
579
|
-
def __iter__(self) -> Iterator[K]:
|
|
580
|
-
return iter(self._cache)
|
|
581
|
-
|
|
582
|
-
def __len__(self) -> int:
|
|
583
|
-
return len(self._cache)
|
|
584
|
-
|
|
585
|
-
def __contains__(self, key: object) -> bool:
|
|
586
|
-
return key in self._cache
|
|
587
|
-
|
|
588
|
-
def __repr__(self) -> str: # pragma: no cover - debugging helper
|
|
589
|
-
return f"{self.__class__.__name__}({self._cache!r})"
|
|
590
|
-
|
|
591
|
-
# ------------------------------------------------------------------
|
|
592
|
-
# Cache helpers
|
|
593
|
-
|
|
594
|
-
@property
|
|
595
|
-
def maxsize(self) -> int:
|
|
596
|
-
return self._cache.maxsize
|
|
597
|
-
|
|
598
|
-
@property
|
|
599
|
-
def currsize(self) -> int:
|
|
600
|
-
return self._cache.currsize
|
|
601
|
-
|
|
602
|
-
def get(self, key: K, default: V | None = None) -> V | None:
|
|
603
|
-
return self._cache.get(key, default)
|
|
604
|
-
|
|
605
|
-
def pop(self, key: K, default: Any = _MISSING) -> V:
|
|
606
|
-
try:
|
|
607
|
-
value = self._cache[key]
|
|
608
|
-
except KeyError:
|
|
609
|
-
self._record_miss(1)
|
|
610
|
-
if default is self._MISSING:
|
|
611
|
-
raise
|
|
612
|
-
return cast(V, default)
|
|
613
|
-
del self._cache[key]
|
|
614
|
-
self._dispatch_removal(key, value, hits=1)
|
|
615
|
-
return value
|
|
616
|
-
|
|
617
|
-
def popitem(self) -> tuple[K, V]:
|
|
618
|
-
return self._cache.popitem()
|
|
619
|
-
|
|
620
|
-
def clear(self) -> None: # type: ignore[override]
|
|
621
|
-
while True:
|
|
622
|
-
try:
|
|
623
|
-
self.popitem()
|
|
624
|
-
except KeyError:
|
|
625
|
-
break
|
|
626
|
-
if self._locks is not None:
|
|
627
|
-
try:
|
|
628
|
-
self._locks.clear()
|
|
629
|
-
except Exception: # pragma: no cover - defensive logging
|
|
630
|
-
_logger.exception("lock cleanup failed during cache clear")
|
|
631
|
-
|
|
632
|
-
# ------------------------------------------------------------------
|
|
633
|
-
# Internal helpers
|
|
634
|
-
|
|
635
|
-
def _record_hit(self, amount: int) -> None:
|
|
636
|
-
if amount and self._manager is not None and self._metrics_key is not None:
|
|
637
|
-
self._manager.increment_hit(self._metrics_key, amount=amount)
|
|
638
|
-
|
|
639
|
-
def _record_miss(self, amount: int) -> None:
|
|
640
|
-
if amount and self._manager is not None and self._metrics_key is not None:
|
|
641
|
-
self._manager.increment_miss(self._metrics_key, amount=amount)
|
|
642
|
-
|
|
643
|
-
def _record_eviction(self, amount: int) -> None:
|
|
644
|
-
if amount and self._manager is not None and self._metrics_key is not None:
|
|
645
|
-
self._manager.increment_eviction(self._metrics_key, amount=amount)
|
|
646
|
-
|
|
647
|
-
def _dispatch_removal(
|
|
648
|
-
self,
|
|
649
|
-
key: K,
|
|
650
|
-
value: V,
|
|
651
|
-
*,
|
|
652
|
-
hits: int = 0,
|
|
653
|
-
misses: int = 0,
|
|
654
|
-
eviction_amount: int = 1,
|
|
655
|
-
purge_lock: bool = True,
|
|
656
|
-
) -> None:
|
|
657
|
-
if hits:
|
|
658
|
-
self._record_hit(hits)
|
|
659
|
-
if misses:
|
|
660
|
-
self._record_miss(misses)
|
|
661
|
-
if eviction_amount:
|
|
662
|
-
self._record_eviction(eviction_amount)
|
|
663
|
-
self._emit_callbacks(self._telemetry_callbacks, key, value, "telemetry")
|
|
664
|
-
self._emit_callbacks(self._eviction_callbacks, key, value, "eviction")
|
|
665
|
-
if purge_lock:
|
|
666
|
-
self._purge_lock(key)
|
|
667
|
-
|
|
668
|
-
def _emit_callbacks(
|
|
669
|
-
self,
|
|
670
|
-
callbacks: Iterable[Callable[[K, V], None]],
|
|
671
|
-
key: K,
|
|
672
|
-
value: V,
|
|
673
|
-
kind: str,
|
|
674
|
-
) -> None:
|
|
675
|
-
for callback in callbacks:
|
|
676
|
-
try:
|
|
677
|
-
callback(key, value)
|
|
678
|
-
except Exception: # pragma: no cover - defensive logging
|
|
679
|
-
_logger.exception("%s callback failed for %r", kind, key)
|
|
680
|
-
|
|
681
|
-
def _purge_lock(self, key: K) -> None:
|
|
682
|
-
if self._locks is None:
|
|
683
|
-
return
|
|
684
|
-
try:
|
|
685
|
-
self._locks.pop(key, None)
|
|
686
|
-
except Exception: # pragma: no cover - defensive logging
|
|
687
|
-
_logger.exception("lock cleanup failed for %r", key)
|
|
688
|
-
|
|
689
|
-
class ManagedLRUCache(LRUCache[K, V]):
|
|
690
|
-
"""LRU cache wrapper with telemetry hooks and lock synchronisation."""
|
|
691
|
-
|
|
692
|
-
def __init__(
|
|
693
|
-
self,
|
|
694
|
-
maxsize: int,
|
|
695
|
-
*,
|
|
696
|
-
manager: CacheManager | None = None,
|
|
697
|
-
metrics_key: str | None = None,
|
|
698
|
-
eviction_callbacks: Iterable[Callable[[K, V], None]]
|
|
699
|
-
| Callable[[K, V], None]
|
|
700
|
-
| None = None,
|
|
701
|
-
telemetry_callbacks: Iterable[Callable[[K, V], None]]
|
|
702
|
-
| Callable[[K, V], None]
|
|
703
|
-
| None = None,
|
|
704
|
-
locks: MutableMapping[K, Any] | None = None,
|
|
705
|
-
) -> None:
|
|
706
|
-
super().__init__(maxsize)
|
|
707
|
-
self._manager = manager
|
|
708
|
-
self._metrics_key = metrics_key
|
|
709
|
-
self._locks = locks
|
|
710
|
-
self._eviction_callbacks = _normalise_callbacks(eviction_callbacks)
|
|
711
|
-
self._telemetry_callbacks = _normalise_callbacks(telemetry_callbacks)
|
|
8
|
+
from __future__ import annotations
|
|
712
9
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
try:
|
|
717
|
-
self._locks.pop(key, None)
|
|
718
|
-
except Exception: # pragma: no cover - defensive logging
|
|
719
|
-
_logger.exception("lock cleanup failed for %r", key)
|
|
720
|
-
if self._manager is not None and self._metrics_key is not None:
|
|
721
|
-
self._manager.increment_eviction(self._metrics_key)
|
|
722
|
-
for callback in self._telemetry_callbacks:
|
|
723
|
-
try:
|
|
724
|
-
callback(key, value)
|
|
725
|
-
except Exception: # pragma: no cover - defensive logging
|
|
726
|
-
_logger.exception("telemetry callback failed for %r", key)
|
|
727
|
-
for callback in self._eviction_callbacks:
|
|
728
|
-
try:
|
|
729
|
-
callback(key, value)
|
|
730
|
-
except Exception: # pragma: no cover - defensive logging
|
|
731
|
-
_logger.exception("eviction callback failed for %r", key)
|
|
732
|
-
return key, value
|
|
10
|
+
raise ImportError(
|
|
11
|
+
"`tnfr.cache` was removed. Import helpers from `tnfr.utils.cache` instead."
|
|
12
|
+
)
|