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.

Files changed (176) hide show
  1. tnfr/__init__.py +50 -5
  2. tnfr/__init__.pyi +0 -7
  3. tnfr/_compat.py +0 -1
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +44 -2
  6. tnfr/alias.py +14 -13
  7. tnfr/alias.pyi +5 -37
  8. tnfr/cache.py +9 -729
  9. tnfr/cache.pyi +8 -224
  10. tnfr/callback_utils.py +16 -31
  11. tnfr/callback_utils.pyi +3 -29
  12. tnfr/cli/__init__.py +17 -11
  13. tnfr/cli/__init__.pyi +0 -21
  14. tnfr/cli/arguments.py +175 -14
  15. tnfr/cli/arguments.pyi +5 -11
  16. tnfr/cli/execution.py +434 -48
  17. tnfr/cli/execution.pyi +14 -24
  18. tnfr/cli/utils.py +20 -3
  19. tnfr/cli/utils.pyi +5 -5
  20. tnfr/config/__init__.py +2 -1
  21. tnfr/config/__init__.pyi +2 -0
  22. tnfr/config/feature_flags.py +83 -0
  23. tnfr/config/init.py +1 -1
  24. tnfr/config/operator_names.py +1 -14
  25. tnfr/config/presets.py +6 -26
  26. tnfr/constants/__init__.py +10 -13
  27. tnfr/constants/__init__.pyi +10 -22
  28. tnfr/constants/aliases.py +31 -0
  29. tnfr/constants/core.py +4 -3
  30. tnfr/constants/init.py +1 -1
  31. tnfr/constants/metric.py +3 -3
  32. tnfr/dynamics/__init__.py +64 -10
  33. tnfr/dynamics/__init__.pyi +3 -4
  34. tnfr/dynamics/adaptation.py +79 -13
  35. tnfr/dynamics/aliases.py +10 -9
  36. tnfr/dynamics/coordination.py +77 -35
  37. tnfr/dynamics/dnfr.py +575 -274
  38. tnfr/dynamics/dnfr.pyi +1 -10
  39. tnfr/dynamics/integrators.py +47 -33
  40. tnfr/dynamics/integrators.pyi +0 -1
  41. tnfr/dynamics/runtime.py +489 -129
  42. tnfr/dynamics/sampling.py +2 -0
  43. tnfr/dynamics/selectors.py +101 -62
  44. tnfr/execution.py +15 -8
  45. tnfr/execution.pyi +5 -25
  46. tnfr/flatten.py +7 -3
  47. tnfr/flatten.pyi +1 -8
  48. tnfr/gamma.py +22 -26
  49. tnfr/gamma.pyi +0 -6
  50. tnfr/glyph_history.py +37 -26
  51. tnfr/glyph_history.pyi +1 -19
  52. tnfr/glyph_runtime.py +16 -0
  53. tnfr/glyph_runtime.pyi +9 -0
  54. tnfr/immutable.py +20 -15
  55. tnfr/immutable.pyi +4 -7
  56. tnfr/initialization.py +5 -7
  57. tnfr/initialization.pyi +1 -9
  58. tnfr/io.py +6 -305
  59. tnfr/io.pyi +13 -8
  60. tnfr/mathematics/__init__.py +81 -0
  61. tnfr/mathematics/backend.py +426 -0
  62. tnfr/mathematics/dynamics.py +398 -0
  63. tnfr/mathematics/epi.py +254 -0
  64. tnfr/mathematics/generators.py +222 -0
  65. tnfr/mathematics/metrics.py +119 -0
  66. tnfr/mathematics/operators.py +233 -0
  67. tnfr/mathematics/operators_factory.py +71 -0
  68. tnfr/mathematics/projection.py +78 -0
  69. tnfr/mathematics/runtime.py +173 -0
  70. tnfr/mathematics/spaces.py +247 -0
  71. tnfr/mathematics/transforms.py +292 -0
  72. tnfr/metrics/__init__.py +10 -10
  73. tnfr/metrics/coherence.py +123 -94
  74. tnfr/metrics/common.py +22 -13
  75. tnfr/metrics/common.pyi +42 -11
  76. tnfr/metrics/core.py +72 -14
  77. tnfr/metrics/diagnosis.py +48 -57
  78. tnfr/metrics/diagnosis.pyi +3 -7
  79. tnfr/metrics/export.py +3 -5
  80. tnfr/metrics/glyph_timing.py +41 -31
  81. tnfr/metrics/reporting.py +13 -6
  82. tnfr/metrics/sense_index.py +884 -114
  83. tnfr/metrics/trig.py +167 -11
  84. tnfr/metrics/trig.pyi +1 -0
  85. tnfr/metrics/trig_cache.py +112 -15
  86. tnfr/node.py +400 -17
  87. tnfr/node.pyi +55 -38
  88. tnfr/observers.py +111 -8
  89. tnfr/observers.pyi +0 -15
  90. tnfr/ontosim.py +9 -6
  91. tnfr/ontosim.pyi +0 -5
  92. tnfr/operators/__init__.py +529 -42
  93. tnfr/operators/__init__.pyi +14 -0
  94. tnfr/operators/definitions.py +350 -18
  95. tnfr/operators/definitions.pyi +0 -14
  96. tnfr/operators/grammar.py +760 -0
  97. tnfr/operators/jitter.py +28 -22
  98. tnfr/operators/registry.py +7 -12
  99. tnfr/operators/registry.pyi +0 -2
  100. tnfr/operators/remesh.py +38 -61
  101. tnfr/rng.py +17 -300
  102. tnfr/schemas/__init__.py +8 -0
  103. tnfr/schemas/grammar.json +94 -0
  104. tnfr/selector.py +3 -4
  105. tnfr/selector.pyi +1 -1
  106. tnfr/sense.py +22 -24
  107. tnfr/sense.pyi +0 -7
  108. tnfr/structural.py +504 -21
  109. tnfr/structural.pyi +41 -18
  110. tnfr/telemetry/__init__.py +23 -1
  111. tnfr/telemetry/cache_metrics.py +226 -0
  112. tnfr/telemetry/nu_f.py +423 -0
  113. tnfr/telemetry/nu_f.pyi +123 -0
  114. tnfr/tokens.py +1 -4
  115. tnfr/tokens.pyi +1 -6
  116. tnfr/trace.py +20 -53
  117. tnfr/trace.pyi +9 -37
  118. tnfr/types.py +244 -15
  119. tnfr/types.pyi +200 -14
  120. tnfr/units.py +69 -0
  121. tnfr/units.pyi +16 -0
  122. tnfr/utils/__init__.py +107 -48
  123. tnfr/utils/__init__.pyi +80 -11
  124. tnfr/utils/cache.py +1705 -65
  125. tnfr/utils/cache.pyi +370 -58
  126. tnfr/utils/chunks.py +104 -0
  127. tnfr/utils/chunks.pyi +21 -0
  128. tnfr/utils/data.py +95 -5
  129. tnfr/utils/data.pyi +8 -17
  130. tnfr/utils/graph.py +2 -4
  131. tnfr/utils/init.py +31 -7
  132. tnfr/utils/init.pyi +4 -11
  133. tnfr/utils/io.py +313 -14
  134. tnfr/{helpers → utils}/numeric.py +50 -24
  135. tnfr/utils/numeric.pyi +21 -0
  136. tnfr/validation/__init__.py +92 -4
  137. tnfr/validation/__init__.pyi +77 -17
  138. tnfr/validation/compatibility.py +79 -43
  139. tnfr/validation/compatibility.pyi +4 -6
  140. tnfr/validation/grammar.py +55 -133
  141. tnfr/validation/grammar.pyi +37 -8
  142. tnfr/validation/graph.py +138 -0
  143. tnfr/validation/graph.pyi +17 -0
  144. tnfr/validation/rules.py +161 -74
  145. tnfr/validation/rules.pyi +55 -18
  146. tnfr/validation/runtime.py +263 -0
  147. tnfr/validation/runtime.pyi +31 -0
  148. tnfr/validation/soft_filters.py +170 -0
  149. tnfr/validation/soft_filters.pyi +37 -0
  150. tnfr/validation/spectral.py +159 -0
  151. tnfr/validation/spectral.pyi +46 -0
  152. tnfr/validation/syntax.py +28 -139
  153. tnfr/validation/syntax.pyi +7 -4
  154. tnfr/validation/window.py +39 -0
  155. tnfr/validation/window.pyi +1 -0
  156. tnfr/viz/__init__.py +9 -0
  157. tnfr/viz/matplotlib.py +246 -0
  158. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/METADATA +63 -19
  159. tnfr-7.0.0.dist-info/RECORD +185 -0
  160. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
  161. tnfr/constants_glyphs.py +0 -16
  162. tnfr/constants_glyphs.pyi +0 -12
  163. tnfr/grammar.py +0 -25
  164. tnfr/grammar.pyi +0 -13
  165. tnfr/helpers/__init__.py +0 -151
  166. tnfr/helpers/__init__.pyi +0 -66
  167. tnfr/helpers/numeric.pyi +0 -12
  168. tnfr/presets.py +0 -15
  169. tnfr/presets.pyi +0 -7
  170. tnfr/utils/io.pyi +0 -10
  171. tnfr/utils/validators.py +0 -130
  172. tnfr/utils/validators.pyi +0 -19
  173. tnfr-6.0.0.dist-info/RECORD +0 -157
  174. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
  175. {tnfr-6.0.0.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
  176. {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
- """Central cache registry infrastructure for TNFR services."""
1
+ """Legacy cache helpers module.
2
2
 
3
- from __future__ import annotations
4
-
5
- import logging
6
- import threading
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
- def iter_metrics(self) -> Iterator[tuple[str, CacheStatistics]]:
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
- def popitem(self) -> tuple[K, V]: # type: ignore[override]
714
- key, value = super().popitem()
715
- if self._locks is not None:
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
+ )