tnfr 4.5.2__py3-none-any.whl → 8.5.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 +334 -50
- tnfr/__init__.pyi +33 -0
- tnfr/_compat.py +10 -0
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +49 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +214 -37
- tnfr/alias.pyi +108 -0
- tnfr/backends/__init__.py +354 -0
- tnfr/backends/jax_backend.py +173 -0
- tnfr/backends/numpy_backend.py +238 -0
- tnfr/backends/optimized_numpy.py +420 -0
- tnfr/backends/torch_backend.py +408 -0
- tnfr/cache.py +149 -556
- tnfr/cache.pyi +13 -0
- tnfr/cli/__init__.py +51 -16
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +344 -32
- tnfr/cli/arguments.pyi +29 -0
- tnfr/cli/execution.py +676 -50
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/interactive_validator.py +614 -0
- tnfr/cli/utils.py +18 -3
- tnfr/cli/utils.pyi +7 -0
- tnfr/cli/validate.py +236 -0
- tnfr/compat/__init__.py +85 -0
- tnfr/compat/dataclass.py +136 -0
- tnfr/compat/jsonschema_stub.py +61 -0
- tnfr/compat/matplotlib_stub.py +73 -0
- tnfr/compat/numpy_stub.py +155 -0
- tnfr/config/__init__.py +224 -0
- tnfr/config/__init__.pyi +10 -0
- tnfr/{constants_glyphs.py → config/constants.py} +26 -20
- tnfr/config/constants.pyi +12 -0
- tnfr/config/defaults.py +54 -0
- tnfr/{constants/core.py → config/defaults_core.py} +59 -6
- tnfr/config/defaults_init.py +33 -0
- tnfr/config/defaults_metric.py +104 -0
- tnfr/config/feature_flags.py +81 -0
- tnfr/config/feature_flags.pyi +16 -0
- tnfr/config/glyph_constants.py +31 -0
- tnfr/config/init.py +77 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +254 -0
- tnfr/config/operator_names.pyi +36 -0
- tnfr/config/physics_derivation.py +354 -0
- tnfr/config/presets.py +83 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/config/security.py +927 -0
- tnfr/config/thresholds.py +114 -0
- tnfr/config/tnfr_config.py +498 -0
- tnfr/constants/__init__.py +51 -133
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +33 -0
- tnfr/constants/aliases.pyi +27 -0
- tnfr/constants/init.py +3 -1
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +9 -15
- tnfr/constants/metric.pyi +19 -0
- tnfr/core/__init__.py +33 -0
- tnfr/core/container.py +226 -0
- tnfr/core/default_implementations.py +329 -0
- tnfr/core/interfaces.py +279 -0
- tnfr/dynamics/__init__.py +213 -633
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +267 -0
- tnfr/dynamics/adaptation.pyi +7 -0
- tnfr/dynamics/adaptive_sequences.py +189 -0
- tnfr/dynamics/adaptive_sequences.pyi +14 -0
- tnfr/dynamics/aliases.py +23 -0
- tnfr/dynamics/aliases.pyi +19 -0
- tnfr/dynamics/bifurcation.py +232 -0
- tnfr/dynamics/canonical.py +229 -0
- tnfr/dynamics/canonical.pyi +48 -0
- tnfr/dynamics/coordination.py +385 -0
- tnfr/dynamics/coordination.pyi +25 -0
- tnfr/dynamics/dnfr.py +2699 -398
- tnfr/dynamics/dnfr.pyi +26 -0
- tnfr/dynamics/dynamic_limits.py +225 -0
- tnfr/dynamics/feedback.py +252 -0
- tnfr/dynamics/feedback.pyi +24 -0
- tnfr/dynamics/fused_dnfr.py +454 -0
- tnfr/dynamics/homeostasis.py +157 -0
- tnfr/dynamics/homeostasis.pyi +14 -0
- tnfr/dynamics/integrators.py +496 -102
- tnfr/dynamics/integrators.pyi +36 -0
- tnfr/dynamics/learning.py +310 -0
- tnfr/dynamics/learning.pyi +33 -0
- tnfr/dynamics/metabolism.py +254 -0
- tnfr/dynamics/nbody.py +796 -0
- tnfr/dynamics/nbody_tnfr.py +783 -0
- tnfr/dynamics/propagation.py +326 -0
- tnfr/dynamics/runtime.py +908 -0
- tnfr/dynamics/runtime.pyi +77 -0
- tnfr/dynamics/sampling.py +10 -5
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +711 -0
- tnfr/dynamics/selectors.pyi +85 -0
- tnfr/dynamics/structural_clip.py +207 -0
- tnfr/errors/__init__.py +37 -0
- tnfr/errors/contextual.py +492 -0
- tnfr/execution.py +77 -55
- tnfr/execution.pyi +45 -0
- tnfr/extensions/__init__.py +205 -0
- tnfr/extensions/__init__.pyi +18 -0
- tnfr/extensions/base.py +173 -0
- tnfr/extensions/base.pyi +35 -0
- tnfr/extensions/business/__init__.py +71 -0
- tnfr/extensions/business/__init__.pyi +11 -0
- tnfr/extensions/business/cookbook.py +88 -0
- tnfr/extensions/business/cookbook.pyi +8 -0
- tnfr/extensions/business/health_analyzers.py +202 -0
- tnfr/extensions/business/health_analyzers.pyi +9 -0
- tnfr/extensions/business/patterns.py +183 -0
- tnfr/extensions/business/patterns.pyi +8 -0
- tnfr/extensions/medical/__init__.py +73 -0
- tnfr/extensions/medical/__init__.pyi +11 -0
- tnfr/extensions/medical/cookbook.py +88 -0
- tnfr/extensions/medical/cookbook.pyi +8 -0
- tnfr/extensions/medical/health_analyzers.py +181 -0
- tnfr/extensions/medical/health_analyzers.pyi +9 -0
- tnfr/extensions/medical/patterns.py +163 -0
- tnfr/extensions/medical/patterns.pyi +8 -0
- tnfr/flatten.py +29 -50
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +66 -53
- tnfr/gamma.pyi +36 -0
- tnfr/glyph_history.py +144 -57
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +19 -0
- tnfr/glyph_runtime.pyi +8 -0
- tnfr/immutable.py +70 -30
- tnfr/immutable.pyi +36 -0
- tnfr/initialization.py +22 -16
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +5 -241
- tnfr/io.pyi +13 -0
- tnfr/locking.pyi +7 -0
- tnfr/mathematics/__init__.py +79 -0
- tnfr/mathematics/backend.py +453 -0
- tnfr/mathematics/backend.pyi +99 -0
- tnfr/mathematics/dynamics.py +408 -0
- tnfr/mathematics/dynamics.pyi +90 -0
- tnfr/mathematics/epi.py +391 -0
- tnfr/mathematics/epi.pyi +65 -0
- tnfr/mathematics/generators.py +242 -0
- tnfr/mathematics/generators.pyi +29 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/metrics.pyi +16 -0
- tnfr/mathematics/operators.py +239 -0
- tnfr/mathematics/operators.pyi +59 -0
- tnfr/mathematics/operators_factory.py +124 -0
- tnfr/mathematics/operators_factory.pyi +11 -0
- tnfr/mathematics/projection.py +87 -0
- tnfr/mathematics/projection.pyi +33 -0
- tnfr/mathematics/runtime.py +182 -0
- tnfr/mathematics/runtime.pyi +64 -0
- tnfr/mathematics/spaces.py +256 -0
- tnfr/mathematics/spaces.pyi +83 -0
- tnfr/mathematics/transforms.py +305 -0
- tnfr/mathematics/transforms.pyi +62 -0
- tnfr/metrics/__init__.py +47 -9
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/buffer_cache.py +163 -0
- tnfr/metrics/buffer_cache.pyi +24 -0
- tnfr/metrics/cache_utils.py +214 -0
- tnfr/metrics/coherence.py +1510 -330
- tnfr/metrics/coherence.pyi +129 -0
- tnfr/metrics/common.py +23 -16
- tnfr/metrics/common.pyi +35 -0
- tnfr/metrics/core.py +251 -36
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +709 -110
- tnfr/metrics/diagnosis.pyi +86 -0
- tnfr/metrics/emergence.py +245 -0
- tnfr/metrics/export.py +60 -18
- tnfr/metrics/export.pyi +7 -0
- tnfr/metrics/glyph_timing.py +233 -43
- tnfr/metrics/glyph_timing.pyi +81 -0
- tnfr/metrics/learning_metrics.py +280 -0
- tnfr/metrics/learning_metrics.pyi +21 -0
- tnfr/metrics/phase_coherence.py +351 -0
- tnfr/metrics/phase_compatibility.py +349 -0
- tnfr/metrics/reporting.py +63 -28
- tnfr/metrics/reporting.pyi +25 -0
- tnfr/metrics/sense_index.py +1126 -43
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +215 -23
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +148 -24
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/multiscale/__init__.py +32 -0
- tnfr/multiscale/hierarchical.py +517 -0
- tnfr/node.py +646 -140
- tnfr/node.pyi +139 -0
- tnfr/observers.py +160 -45
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +23 -19
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +1358 -106
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/algebra.py +277 -0
- tnfr/operators/canonical_patterns.py +420 -0
- tnfr/operators/cascade.py +267 -0
- tnfr/operators/cycle_detection.py +358 -0
- tnfr/operators/definitions.py +4108 -0
- tnfr/operators/definitions.pyi +78 -0
- tnfr/operators/grammar.py +1164 -0
- tnfr/operators/grammar.pyi +140 -0
- tnfr/operators/hamiltonian.py +710 -0
- tnfr/operators/health_analyzer.py +809 -0
- tnfr/operators/jitter.py +107 -38
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/lifecycle.py +314 -0
- tnfr/operators/metabolism.py +618 -0
- tnfr/operators/metrics.py +2138 -0
- tnfr/operators/network_analysis/__init__.py +27 -0
- tnfr/operators/network_analysis/source_detection.py +186 -0
- tnfr/operators/nodal_equation.py +395 -0
- tnfr/operators/pattern_detection.py +660 -0
- tnfr/operators/patterns.py +669 -0
- tnfr/operators/postconditions/__init__.py +38 -0
- tnfr/operators/postconditions/mutation.py +236 -0
- tnfr/operators/preconditions/__init__.py +1226 -0
- tnfr/operators/preconditions/coherence.py +305 -0
- tnfr/operators/preconditions/dissonance.py +236 -0
- tnfr/operators/preconditions/emission.py +128 -0
- tnfr/operators/preconditions/mutation.py +580 -0
- tnfr/operators/preconditions/reception.py +125 -0
- tnfr/operators/preconditions/resonance.py +364 -0
- tnfr/operators/registry.py +74 -0
- tnfr/operators/registry.pyi +9 -0
- tnfr/operators/remesh.py +1415 -91
- tnfr/operators/remesh.pyi +26 -0
- tnfr/operators/structural_units.py +268 -0
- tnfr/operators/unified_grammar.py +105 -0
- tnfr/parallel/__init__.py +54 -0
- tnfr/parallel/auto_scaler.py +234 -0
- tnfr/parallel/distributed.py +384 -0
- tnfr/parallel/engine.py +238 -0
- tnfr/parallel/gpu_engine.py +420 -0
- tnfr/parallel/monitoring.py +248 -0
- tnfr/parallel/partitioner.py +459 -0
- tnfr/py.typed +0 -0
- tnfr/recipes/__init__.py +22 -0
- tnfr/recipes/cookbook.py +743 -0
- tnfr/rng.py +75 -151
- tnfr/rng.pyi +26 -0
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/sdk/__init__.py +107 -0
- tnfr/sdk/__init__.pyi +19 -0
- tnfr/sdk/adaptive_system.py +173 -0
- tnfr/sdk/adaptive_system.pyi +21 -0
- tnfr/sdk/builders.py +370 -0
- tnfr/sdk/builders.pyi +51 -0
- tnfr/sdk/fluent.py +1121 -0
- tnfr/sdk/fluent.pyi +74 -0
- tnfr/sdk/templates.py +342 -0
- tnfr/sdk/templates.pyi +41 -0
- tnfr/sdk/utils.py +341 -0
- tnfr/secure_config.py +46 -0
- tnfr/security/__init__.py +70 -0
- tnfr/security/database.py +514 -0
- tnfr/security/subprocess.py +503 -0
- tnfr/security/validation.py +290 -0
- tnfr/selector.py +59 -22
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +92 -67
- tnfr/sense.pyi +23 -0
- tnfr/services/__init__.py +17 -0
- tnfr/services/orchestrator.py +325 -0
- tnfr/sparse/__init__.py +39 -0
- tnfr/sparse/representations.py +492 -0
- tnfr/structural.py +639 -263
- tnfr/structural.pyi +83 -0
- tnfr/telemetry/__init__.py +35 -0
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/cache_metrics.pyi +64 -0
- tnfr/telemetry/nu_f.py +422 -0
- tnfr/telemetry/nu_f.pyi +108 -0
- tnfr/telemetry/verbosity.py +36 -0
- tnfr/telemetry/verbosity.pyi +15 -0
- tnfr/tokens.py +2 -4
- tnfr/tokens.pyi +36 -0
- tnfr/tools/__init__.py +20 -0
- tnfr/tools/domain_templates.py +478 -0
- tnfr/tools/sequence_generator.py +846 -0
- tnfr/topology/__init__.py +13 -0
- tnfr/topology/asymmetry.py +151 -0
- tnfr/trace.py +300 -126
- tnfr/trace.pyi +42 -0
- tnfr/tutorials/__init__.py +38 -0
- tnfr/tutorials/autonomous_evolution.py +285 -0
- tnfr/tutorials/interactive.py +1576 -0
- tnfr/tutorials/structural_metabolism.py +238 -0
- tnfr/types.py +743 -12
- tnfr/types.pyi +357 -0
- tnfr/units.py +68 -0
- tnfr/units.pyi +13 -0
- tnfr/utils/__init__.py +282 -0
- tnfr/utils/__init__.pyi +215 -0
- tnfr/utils/cache.py +4223 -0
- tnfr/utils/cache.pyi +470 -0
- tnfr/{callback_utils.py → utils/callbacks.py} +26 -39
- tnfr/utils/callbacks.pyi +49 -0
- tnfr/utils/chunks.py +108 -0
- tnfr/utils/chunks.pyi +22 -0
- tnfr/utils/data.py +428 -0
- tnfr/utils/data.pyi +74 -0
- tnfr/utils/graph.py +85 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +821 -0
- tnfr/utils/init.pyi +80 -0
- tnfr/utils/io.py +559 -0
- tnfr/utils/io.pyi +66 -0
- tnfr/{helpers → utils}/numeric.py +51 -24
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +257 -0
- tnfr/validation/__init__.pyi +85 -0
- tnfr/validation/compatibility.py +460 -0
- tnfr/validation/compatibility.pyi +6 -0
- tnfr/validation/config.py +73 -0
- tnfr/validation/graph.py +139 -0
- tnfr/validation/graph.pyi +18 -0
- tnfr/validation/input_validation.py +755 -0
- tnfr/validation/invariants.py +712 -0
- tnfr/validation/rules.py +253 -0
- tnfr/validation/rules.pyi +44 -0
- tnfr/validation/runtime.py +279 -0
- tnfr/validation/runtime.pyi +28 -0
- tnfr/validation/sequence_validator.py +162 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +32 -0
- tnfr/validation/spectral.py +164 -0
- tnfr/validation/spectral.pyi +42 -0
- tnfr/validation/validator.py +1266 -0
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/visualization/__init__.py +98 -0
- tnfr/visualization/cascade_viz.py +256 -0
- tnfr/visualization/hierarchy.py +284 -0
- tnfr/visualization/sequence_plotter.py +784 -0
- tnfr/viz/__init__.py +60 -0
- tnfr/viz/matplotlib.py +278 -0
- tnfr/viz/matplotlib.pyi +35 -0
- tnfr-8.5.0.dist-info/METADATA +573 -0
- tnfr-8.5.0.dist-info/RECORD +353 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/collections_utils.py +0 -300
- tnfr/config.py +0 -32
- tnfr/grammar.py +0 -344
- tnfr/graph_utils.py +0 -84
- tnfr/helpers/__init__.py +0 -71
- tnfr/import_utils.py +0 -228
- tnfr/json_utils.py +0 -162
- tnfr/logging_utils.py +0 -116
- tnfr/presets.py +0 -60
- tnfr/validators.py +0 -84
- tnfr/value_utils.py +0 -59
- tnfr-4.5.2.dist-info/METADATA +0 -379
- tnfr-4.5.2.dist-info/RECORD +0 -67
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/graph_utils.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"""Utilities for graph-level bookkeeping.
|
|
2
|
-
|
|
3
|
-
This module centralises helpers that operate on the metadata stored inside
|
|
4
|
-
graph objects. Besides flagging ΔNFR preparation caches it also exposes
|
|
5
|
-
lightweight adapters to obtain the canonical ``graph`` mapping and to read
|
|
6
|
-
validated configuration dictionaries.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import warnings
|
|
12
|
-
from types import MappingProxyType
|
|
13
|
-
from typing import Any, Mapping
|
|
14
|
-
|
|
15
|
-
__all__ = (
|
|
16
|
-
"get_graph",
|
|
17
|
-
"get_graph_mapping",
|
|
18
|
-
"mark_dnfr_prep_dirty",
|
|
19
|
-
"supports_add_edge",
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def get_graph(obj: Any) -> Any:
|
|
24
|
-
"""Return ``obj.graph`` when present or ``obj`` otherwise."""
|
|
25
|
-
return getattr(obj, "graph", obj)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_graph_mapping(
|
|
29
|
-
G: Any, key: str, warn_msg: str
|
|
30
|
-
) -> Mapping[str, Any] | None:
|
|
31
|
-
"""Return an immutable view of ``G``'s stored mapping for ``key``.
|
|
32
|
-
|
|
33
|
-
The helper normalises access to ``G.graph[key]`` by returning
|
|
34
|
-
``None`` when the key is missing or holds a non-mapping value. When a
|
|
35
|
-
mapping is found it is wrapped in :class:`types.MappingProxyType` to guard
|
|
36
|
-
against accidental mutation. ``warn_msg`` is emitted via
|
|
37
|
-
:func:`warnings.warn` when the stored value is not a mapping.
|
|
38
|
-
"""
|
|
39
|
-
graph = get_graph(G)
|
|
40
|
-
getter = getattr(graph, "get", None)
|
|
41
|
-
if getter is None:
|
|
42
|
-
return None
|
|
43
|
-
|
|
44
|
-
data = getter(key)
|
|
45
|
-
if data is None:
|
|
46
|
-
return None
|
|
47
|
-
if not isinstance(data, Mapping):
|
|
48
|
-
warnings.warn(warn_msg, UserWarning, stacklevel=2)
|
|
49
|
-
return None
|
|
50
|
-
return MappingProxyType(data)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def mark_dnfr_prep_dirty(G: Any) -> None:
|
|
54
|
-
"""Flag ΔNFR preparation data as stale.
|
|
55
|
-
|
|
56
|
-
Parameters
|
|
57
|
-
----------
|
|
58
|
-
G : Any
|
|
59
|
-
Graph-like object whose ``graph`` attribute will receive the
|
|
60
|
-
``"_dnfr_prep_dirty"`` flag.
|
|
61
|
-
|
|
62
|
-
Returns
|
|
63
|
-
-------
|
|
64
|
-
None
|
|
65
|
-
This function mutates ``G`` in place.
|
|
66
|
-
"""
|
|
67
|
-
graph = get_graph(G)
|
|
68
|
-
graph["_dnfr_prep_dirty"] = True
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def supports_add_edge(graph: Any) -> bool:
|
|
72
|
-
"""Return ``True`` if ``graph`` exposes an ``add_edge`` method.
|
|
73
|
-
|
|
74
|
-
Parameters
|
|
75
|
-
----------
|
|
76
|
-
graph : Any
|
|
77
|
-
Object representing a graph.
|
|
78
|
-
|
|
79
|
-
Returns
|
|
80
|
-
-------
|
|
81
|
-
bool
|
|
82
|
-
``True`` when ``graph`` implements ``add_edge``; ``False`` otherwise.
|
|
83
|
-
"""
|
|
84
|
-
return hasattr(graph, "add_edge")
|
tnfr/helpers/__init__.py
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
"""Curated high-level helpers exposed by :mod:`tnfr.helpers`.
|
|
2
|
-
|
|
3
|
-
The module is intentionally small and surfaces utilities that are stable for
|
|
4
|
-
external use, covering data preparation, glyph history management, and graph
|
|
5
|
-
cache invalidation.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from ..cache import (
|
|
11
|
-
EdgeCacheManager,
|
|
12
|
-
cached_node_list,
|
|
13
|
-
cached_nodes_and_A,
|
|
14
|
-
edge_version_cache,
|
|
15
|
-
edge_version_update,
|
|
16
|
-
ensure_node_index_map,
|
|
17
|
-
ensure_node_offset_map,
|
|
18
|
-
increment_edge_version,
|
|
19
|
-
node_set_checksum,
|
|
20
|
-
stable_json,
|
|
21
|
-
)
|
|
22
|
-
from ..graph_utils import get_graph, get_graph_mapping, mark_dnfr_prep_dirty
|
|
23
|
-
from .numeric import (
|
|
24
|
-
angle_diff,
|
|
25
|
-
clamp,
|
|
26
|
-
clamp01,
|
|
27
|
-
kahan_sum_nd,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def __getattr__(name: str):
|
|
32
|
-
if name in _GLYPH_HISTORY_EXPORTS:
|
|
33
|
-
from .. import glyph_history as _glyph_history
|
|
34
|
-
|
|
35
|
-
value = getattr(_glyph_history, name)
|
|
36
|
-
globals()[name] = value
|
|
37
|
-
return value
|
|
38
|
-
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
39
|
-
|
|
40
|
-
__all__ = (
|
|
41
|
-
"EdgeCacheManager",
|
|
42
|
-
"angle_diff",
|
|
43
|
-
"cached_node_list",
|
|
44
|
-
"cached_nodes_and_A",
|
|
45
|
-
"clamp",
|
|
46
|
-
"clamp01",
|
|
47
|
-
"edge_version_cache",
|
|
48
|
-
"edge_version_update",
|
|
49
|
-
"ensure_node_index_map",
|
|
50
|
-
"ensure_node_offset_map",
|
|
51
|
-
"get_graph",
|
|
52
|
-
"get_graph_mapping",
|
|
53
|
-
"increment_edge_version",
|
|
54
|
-
"kahan_sum_nd",
|
|
55
|
-
"mark_dnfr_prep_dirty",
|
|
56
|
-
"node_set_checksum",
|
|
57
|
-
"stable_json",
|
|
58
|
-
"count_glyphs",
|
|
59
|
-
"ensure_history",
|
|
60
|
-
"last_glyph",
|
|
61
|
-
"push_glyph",
|
|
62
|
-
"recent_glyph",
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
_GLYPH_HISTORY_EXPORTS = (
|
|
66
|
-
"count_glyphs",
|
|
67
|
-
"ensure_history",
|
|
68
|
-
"last_glyph",
|
|
69
|
-
"push_glyph",
|
|
70
|
-
"recent_glyph",
|
|
71
|
-
)
|
tnfr/import_utils.py
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
"""Helpers for optional imports and cached access to heavy modules.
|
|
2
|
-
|
|
3
|
-
This module centralises caching for optional dependencies. It exposes
|
|
4
|
-
:func:`cached_import`, backed by a small :func:`functools.lru_cache`, alongside a
|
|
5
|
-
light-weight registry that tracks failed imports and warnings. Use
|
|
6
|
-
:func:`prune_failed_imports` or ``cached_import.cache_clear`` to reset state when
|
|
7
|
-
new packages become available at runtime.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import importlib
|
|
13
|
-
import warnings
|
|
14
|
-
from collections import OrderedDict
|
|
15
|
-
from dataclasses import dataclass, field
|
|
16
|
-
from functools import lru_cache
|
|
17
|
-
from typing import Any, Callable, Literal
|
|
18
|
-
import threading
|
|
19
|
-
|
|
20
|
-
from .logging_utils import get_logger
|
|
21
|
-
|
|
22
|
-
__all__ = (
|
|
23
|
-
"cached_import",
|
|
24
|
-
"get_numpy",
|
|
25
|
-
"get_nodonx",
|
|
26
|
-
"prune_failed_imports",
|
|
27
|
-
"IMPORT_LOG",
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
logger = get_logger(__name__)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _emit(message: str, mode: Literal["warn", "log", "both"]) -> None:
|
|
35
|
-
"""Emit ``message`` via :mod:`warnings`, logger or both."""
|
|
36
|
-
|
|
37
|
-
if mode in ("warn", "both"):
|
|
38
|
-
warnings.warn(message, RuntimeWarning, stacklevel=2)
|
|
39
|
-
if mode in ("log", "both"):
|
|
40
|
-
logger.warning(message)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
EMIT_MAP: dict[str, Callable[[str], None]] = {
|
|
44
|
-
"warn": lambda msg: _emit(msg, "warn"),
|
|
45
|
-
"log": lambda msg: _emit(msg, "log"),
|
|
46
|
-
"both": lambda msg: _emit(msg, "both"),
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _format_failure_message(module: str, attr: str | None, err: Exception) -> str:
|
|
51
|
-
"""Return a standardised failure message."""
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
f"Failed to import module '{module}': {err}"
|
|
55
|
-
if isinstance(err, ImportError)
|
|
56
|
-
else f"Module '{module}' has no attribute '{attr}': {err}"
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
_FAILED_IMPORT_LIMIT = 128
|
|
61
|
-
_DEFAULT_CACHE_SIZE = 128
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@dataclass(slots=True)
|
|
65
|
-
class ImportRegistry:
|
|
66
|
-
"""Process-wide registry tracking failed imports and emitted warnings."""
|
|
67
|
-
|
|
68
|
-
limit: int = _FAILED_IMPORT_LIMIT
|
|
69
|
-
failed: OrderedDict[str, None] = field(default_factory=OrderedDict)
|
|
70
|
-
warned: set[str] = field(default_factory=set)
|
|
71
|
-
lock: threading.Lock = field(default_factory=threading.Lock)
|
|
72
|
-
|
|
73
|
-
def _insert(self, key: str) -> None:
|
|
74
|
-
self.failed[key] = None
|
|
75
|
-
self.failed.move_to_end(key)
|
|
76
|
-
while len(self.failed) > self.limit:
|
|
77
|
-
self.failed.popitem(last=False)
|
|
78
|
-
|
|
79
|
-
def record_failure(self, key: str, *, module: str | None = None) -> None:
|
|
80
|
-
"""Record ``key`` and, optionally, ``module`` as failed imports."""
|
|
81
|
-
|
|
82
|
-
with self.lock:
|
|
83
|
-
self._insert(key)
|
|
84
|
-
if module and module != key:
|
|
85
|
-
self._insert(module)
|
|
86
|
-
|
|
87
|
-
def discard(self, key: str) -> None:
|
|
88
|
-
"""Remove ``key`` from the registry and clear its warning state."""
|
|
89
|
-
|
|
90
|
-
with self.lock:
|
|
91
|
-
self.failed.pop(key, None)
|
|
92
|
-
self.warned.discard(key)
|
|
93
|
-
|
|
94
|
-
def mark_warning(self, module: str) -> bool:
|
|
95
|
-
"""Mark ``module`` as warned and return ``True`` if it was new."""
|
|
96
|
-
|
|
97
|
-
with self.lock:
|
|
98
|
-
if module in self.warned:
|
|
99
|
-
return False
|
|
100
|
-
self.warned.add(module)
|
|
101
|
-
return True
|
|
102
|
-
|
|
103
|
-
def clear(self) -> None:
|
|
104
|
-
"""Remove all failure records and warning markers."""
|
|
105
|
-
|
|
106
|
-
with self.lock:
|
|
107
|
-
self.failed.clear()
|
|
108
|
-
self.warned.clear()
|
|
109
|
-
|
|
110
|
-
def __contains__(self, key: str) -> bool: # pragma: no cover - trivial
|
|
111
|
-
with self.lock:
|
|
112
|
-
return key in self.failed
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
_IMPORT_STATE = ImportRegistry()
|
|
116
|
-
# Public alias to ease direct introspection in tests and diagnostics.
|
|
117
|
-
IMPORT_LOG = _IMPORT_STATE
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@lru_cache(maxsize=_DEFAULT_CACHE_SIZE)
|
|
121
|
-
def _import_cached(module_name: str, attr: str | None) -> tuple[bool, Any]:
|
|
122
|
-
"""Import ``module_name`` (and optional ``attr``) capturing failures."""
|
|
123
|
-
|
|
124
|
-
try:
|
|
125
|
-
module = importlib.import_module(module_name)
|
|
126
|
-
obj = getattr(module, attr) if attr else module
|
|
127
|
-
except (ImportError, AttributeError) as exc:
|
|
128
|
-
return False, exc
|
|
129
|
-
return True, obj
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _warn_failure(
|
|
133
|
-
module: str,
|
|
134
|
-
attr: str | None,
|
|
135
|
-
err: Exception,
|
|
136
|
-
*,
|
|
137
|
-
emit: Literal["warn", "log", "both"] = "warn",
|
|
138
|
-
) -> None:
|
|
139
|
-
"""Emit a warning about a failed import."""
|
|
140
|
-
|
|
141
|
-
msg = _format_failure_message(module, attr, err)
|
|
142
|
-
if _IMPORT_STATE.mark_warning(module):
|
|
143
|
-
EMIT_MAP[emit](msg)
|
|
144
|
-
else:
|
|
145
|
-
logger.debug(msg)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def cached_import(
|
|
149
|
-
module_name: str,
|
|
150
|
-
attr: str | None = None,
|
|
151
|
-
*,
|
|
152
|
-
fallback: Any | None = None,
|
|
153
|
-
emit: Literal["warn", "log", "both"] = "warn",
|
|
154
|
-
) -> Any | None:
|
|
155
|
-
"""Import ``module_name`` (and optional ``attr``) with caching and fallback.
|
|
156
|
-
|
|
157
|
-
Parameters
|
|
158
|
-
----------
|
|
159
|
-
module_name:
|
|
160
|
-
Module to import.
|
|
161
|
-
attr:
|
|
162
|
-
Optional attribute to fetch from the module.
|
|
163
|
-
fallback:
|
|
164
|
-
Value returned when the import fails.
|
|
165
|
-
emit:
|
|
166
|
-
Destination for warnings emitted on failure (``"warn"``/``"log"``/``"both"``).
|
|
167
|
-
"""
|
|
168
|
-
|
|
169
|
-
key = module_name if attr is None else f"{module_name}.{attr}"
|
|
170
|
-
success, result = _import_cached(module_name, attr)
|
|
171
|
-
if success:
|
|
172
|
-
_IMPORT_STATE.discard(key)
|
|
173
|
-
if attr is not None:
|
|
174
|
-
_IMPORT_STATE.discard(module_name)
|
|
175
|
-
return result
|
|
176
|
-
exc = result
|
|
177
|
-
include_module = isinstance(exc, ImportError)
|
|
178
|
-
_warn_failure(module_name, attr, exc, emit=emit)
|
|
179
|
-
_IMPORT_STATE.record_failure(key, module=module_name if include_module else None)
|
|
180
|
-
return fallback
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def _clear_default_cache() -> None:
|
|
184
|
-
global _NP_MISSING_LOGGED
|
|
185
|
-
|
|
186
|
-
_import_cached.cache_clear()
|
|
187
|
-
_NP_MISSING_LOGGED = False
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
cached_import.cache_clear = _clear_default_cache # type: ignore[attr-defined]
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
_NP_MISSING_LOGGED = False
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def get_numpy() -> Any | None:
|
|
197
|
-
"""Return the cached :mod:`numpy` module when available.
|
|
198
|
-
|
|
199
|
-
Import attempts are delegated to :func:`cached_import`, which already caches
|
|
200
|
-
successes and failures. A lightweight flag suppresses duplicate debug logs
|
|
201
|
-
when :mod:`numpy` is unavailable so callers can repeatedly probe without
|
|
202
|
-
spamming the logger.
|
|
203
|
-
"""
|
|
204
|
-
|
|
205
|
-
global _NP_MISSING_LOGGED
|
|
206
|
-
|
|
207
|
-
np = cached_import("numpy")
|
|
208
|
-
if np is None:
|
|
209
|
-
if not _NP_MISSING_LOGGED:
|
|
210
|
-
logger.debug("Failed to import numpy; continuing in non-vectorised mode")
|
|
211
|
-
_NP_MISSING_LOGGED = True
|
|
212
|
-
return None
|
|
213
|
-
|
|
214
|
-
if _NP_MISSING_LOGGED:
|
|
215
|
-
_NP_MISSING_LOGGED = False
|
|
216
|
-
return np
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def get_nodonx() -> type | None:
|
|
220
|
-
"""Return :class:`tnfr.node.NodoNX` using import caching."""
|
|
221
|
-
|
|
222
|
-
return cached_import("tnfr.node", "NodoNX")
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def prune_failed_imports() -> None:
|
|
226
|
-
"""Clear the registry of recorded import failures and warnings."""
|
|
227
|
-
|
|
228
|
-
_IMPORT_STATE.clear()
|
tnfr/json_utils.py
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
"""JSON helpers with optional :mod:`orjson` support.
|
|
2
|
-
|
|
3
|
-
This module lazily imports :mod:`orjson` on first use of :func:`json_dumps`.
|
|
4
|
-
The fast serializer is brought in through
|
|
5
|
-
``tnfr.import_utils.cached_import``; its cache and failure registry can be
|
|
6
|
-
reset using ``cached_import.cache_clear()`` and
|
|
7
|
-
:func:`tnfr.import_utils.prune_failed_imports`.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
import json
|
|
14
|
-
from typing import Any, Callable
|
|
15
|
-
|
|
16
|
-
from .import_utils import cached_import
|
|
17
|
-
from .logging_utils import get_logger, warn_once
|
|
18
|
-
|
|
19
|
-
_ORJSON_PARAMS_MSG = (
|
|
20
|
-
"'ensure_ascii', 'separators', 'cls' and extra kwargs are ignored when using orjson: %s"
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
logger = get_logger(__name__)
|
|
24
|
-
|
|
25
|
-
_warn_ignored_params_once = warn_once(logger, _ORJSON_PARAMS_MSG)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def clear_orjson_param_warnings() -> None:
|
|
29
|
-
"""Reset cached warnings for ignored :mod:`orjson` parameters."""
|
|
30
|
-
|
|
31
|
-
_warn_ignored_params_once.clear()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _format_ignored_params(combo: frozenset[str]) -> str:
|
|
35
|
-
"""Return a stable representation for ignored parameter combinations."""
|
|
36
|
-
return "{" + ", ".join(map(repr, sorted(combo))) + "}"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@dataclass(frozen=True)
|
|
40
|
-
class JsonDumpsParams:
|
|
41
|
-
"""Container describing the parameters used by :func:`json_dumps`."""
|
|
42
|
-
|
|
43
|
-
sort_keys: bool = False
|
|
44
|
-
default: Callable[[Any], Any] | None = None
|
|
45
|
-
ensure_ascii: bool = True
|
|
46
|
-
separators: tuple[str, str] = (",", ":")
|
|
47
|
-
cls: type[json.JSONEncoder] | None = None
|
|
48
|
-
to_bytes: bool = False
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
DEFAULT_PARAMS = JsonDumpsParams()
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _collect_ignored_params(
|
|
55
|
-
params: JsonDumpsParams, extra_kwargs: dict[str, Any]
|
|
56
|
-
) -> frozenset[str]:
|
|
57
|
-
"""Return a stable set of parameters ignored by :mod:`orjson`."""
|
|
58
|
-
|
|
59
|
-
ignored: set[str] = set()
|
|
60
|
-
if params.ensure_ascii is not True:
|
|
61
|
-
ignored.add("ensure_ascii")
|
|
62
|
-
if params.separators != (",", ":"):
|
|
63
|
-
ignored.add("separators")
|
|
64
|
-
if params.cls is not None:
|
|
65
|
-
ignored.add("cls")
|
|
66
|
-
if extra_kwargs:
|
|
67
|
-
ignored.update(extra_kwargs.keys())
|
|
68
|
-
return frozenset(ignored)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _json_dumps_orjson(
|
|
72
|
-
orjson: Any,
|
|
73
|
-
obj: Any,
|
|
74
|
-
params: JsonDumpsParams,
|
|
75
|
-
**kwargs: Any,
|
|
76
|
-
) -> bytes | str:
|
|
77
|
-
"""Serialize using :mod:`orjson` and warn about unsupported parameters."""
|
|
78
|
-
|
|
79
|
-
ignored = _collect_ignored_params(params, kwargs)
|
|
80
|
-
if ignored:
|
|
81
|
-
_warn_ignored_params_once(ignored, _format_ignored_params(ignored))
|
|
82
|
-
|
|
83
|
-
option = orjson.OPT_SORT_KEYS if params.sort_keys else 0
|
|
84
|
-
data = orjson.dumps(obj, option=option, default=params.default)
|
|
85
|
-
return data if params.to_bytes else data.decode("utf-8")
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _json_dumps_std(
|
|
89
|
-
obj: Any,
|
|
90
|
-
params: JsonDumpsParams,
|
|
91
|
-
**kwargs: Any,
|
|
92
|
-
) -> bytes | str:
|
|
93
|
-
"""Serialize using the standard library :func:`json.dumps`."""
|
|
94
|
-
result = json.dumps(
|
|
95
|
-
obj,
|
|
96
|
-
sort_keys=params.sort_keys,
|
|
97
|
-
ensure_ascii=params.ensure_ascii,
|
|
98
|
-
separators=params.separators,
|
|
99
|
-
cls=params.cls,
|
|
100
|
-
default=params.default,
|
|
101
|
-
**kwargs,
|
|
102
|
-
)
|
|
103
|
-
return result if not params.to_bytes else result.encode("utf-8")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def json_dumps(
|
|
107
|
-
obj: Any,
|
|
108
|
-
*,
|
|
109
|
-
sort_keys: bool = False,
|
|
110
|
-
default: Callable[[Any], Any] | None = None,
|
|
111
|
-
ensure_ascii: bool = True,
|
|
112
|
-
separators: tuple[str, str] = (",", ":"),
|
|
113
|
-
cls: type[json.JSONEncoder] | None = None,
|
|
114
|
-
to_bytes: bool = False,
|
|
115
|
-
**kwargs: Any,
|
|
116
|
-
) -> bytes | str:
|
|
117
|
-
"""Serialize ``obj`` to JSON using ``orjson`` when available.
|
|
118
|
-
|
|
119
|
-
Returns a ``str`` by default. Pass ``to_bytes=True`` to obtain a ``bytes``
|
|
120
|
-
result. When :mod:`orjson` is used, the ``ensure_ascii``, ``separators``,
|
|
121
|
-
``cls`` and any additional keyword arguments are ignored because they are
|
|
122
|
-
not supported by :func:`orjson.dumps`. A warning is emitted whenever such
|
|
123
|
-
ignored parameters are detected.
|
|
124
|
-
"""
|
|
125
|
-
if not isinstance(sort_keys, bool):
|
|
126
|
-
raise TypeError("sort_keys must be a boolean")
|
|
127
|
-
if default is not None and not callable(default):
|
|
128
|
-
raise TypeError("default must be callable when provided")
|
|
129
|
-
if not isinstance(ensure_ascii, bool):
|
|
130
|
-
raise TypeError("ensure_ascii must be a boolean")
|
|
131
|
-
if not isinstance(separators, tuple) or len(separators) != 2:
|
|
132
|
-
raise TypeError("separators must be a tuple of two strings")
|
|
133
|
-
if not all(isinstance(part, str) for part in separators):
|
|
134
|
-
raise TypeError("separators must be a tuple of two strings")
|
|
135
|
-
if cls is not None:
|
|
136
|
-
if not isinstance(cls, type) or not issubclass(cls, json.JSONEncoder):
|
|
137
|
-
raise TypeError("cls must be a subclass of json.JSONEncoder")
|
|
138
|
-
if not isinstance(to_bytes, bool):
|
|
139
|
-
raise TypeError("to_bytes must be a boolean")
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
sort_keys is False
|
|
143
|
-
and default is None
|
|
144
|
-
and ensure_ascii is True
|
|
145
|
-
and separators == (",", ":")
|
|
146
|
-
and cls is None
|
|
147
|
-
and to_bytes is False
|
|
148
|
-
):
|
|
149
|
-
params = DEFAULT_PARAMS
|
|
150
|
-
else:
|
|
151
|
-
params = JsonDumpsParams(
|
|
152
|
-
sort_keys=sort_keys,
|
|
153
|
-
default=default,
|
|
154
|
-
ensure_ascii=ensure_ascii,
|
|
155
|
-
separators=separators,
|
|
156
|
-
cls=cls,
|
|
157
|
-
to_bytes=to_bytes,
|
|
158
|
-
)
|
|
159
|
-
orjson = cached_import("orjson", emit="log")
|
|
160
|
-
if orjson is not None:
|
|
161
|
-
return _json_dumps_orjson(orjson, obj, params, **kwargs)
|
|
162
|
-
return _json_dumps_std(obj, params, **kwargs)
|
tnfr/logging_utils.py
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
"""Logging utilities for TNFR.
|
|
2
|
-
|
|
3
|
-
Centralises creation of module-specific loggers so that all TNFR
|
|
4
|
-
modules share a consistent configuration.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import logging
|
|
10
|
-
import threading
|
|
11
|
-
from typing import Any, Hashable, Mapping
|
|
12
|
-
|
|
13
|
-
__all__ = ("_configure_root", "get_logger", "WarnOnce", "warn_once")
|
|
14
|
-
|
|
15
|
-
_LOGGING_CONFIGURED = False
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _configure_root() -> None:
|
|
19
|
-
"""Ensure the root logger has handlers and a default format."""
|
|
20
|
-
|
|
21
|
-
global _LOGGING_CONFIGURED
|
|
22
|
-
if _LOGGING_CONFIGURED:
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
root = logging.getLogger()
|
|
26
|
-
if not root.handlers:
|
|
27
|
-
kwargs = {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"}
|
|
28
|
-
if root.level == logging.NOTSET:
|
|
29
|
-
kwargs["level"] = logging.INFO
|
|
30
|
-
logging.basicConfig(**kwargs)
|
|
31
|
-
|
|
32
|
-
_LOGGING_CONFIGURED = True
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def get_logger(name: str) -> logging.Logger:
|
|
36
|
-
"""Return a module-specific logger."""
|
|
37
|
-
_configure_root()
|
|
38
|
-
return logging.getLogger(name)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class WarnOnce:
|
|
42
|
-
"""Log a warning only once for each unique key.
|
|
43
|
-
|
|
44
|
-
``WarnOnce`` tracks seen keys in a bounded :class:`set`. When ``maxsize`` is
|
|
45
|
-
reached an arbitrary key is evicted to keep memory usage stable; ordered
|
|
46
|
-
eviction is intentionally avoided to keep the implementation lightweight.
|
|
47
|
-
Instances are callable and accept either a mapping of keys to values or a
|
|
48
|
-
single key/value pair. Passing ``maxsize <= 0`` disables caching and logs on
|
|
49
|
-
every invocation.
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
def __init__(self, logger: logging.Logger, msg: str, *, maxsize: int = 1024) -> None:
|
|
53
|
-
self._logger = logger
|
|
54
|
-
self._msg = msg
|
|
55
|
-
self._maxsize = maxsize
|
|
56
|
-
self._seen: set[Hashable] = set()
|
|
57
|
-
self._lock = threading.Lock()
|
|
58
|
-
|
|
59
|
-
def _mark_seen(self, key: Hashable) -> bool:
|
|
60
|
-
"""Return ``True`` when ``key`` has not been seen before."""
|
|
61
|
-
|
|
62
|
-
if self._maxsize <= 0:
|
|
63
|
-
# Caching disabled – always log.
|
|
64
|
-
return True
|
|
65
|
-
if key in self._seen:
|
|
66
|
-
return False
|
|
67
|
-
if len(self._seen) >= self._maxsize:
|
|
68
|
-
# ``set.pop()`` removes an arbitrary element which is acceptable for
|
|
69
|
-
# this lightweight cache.
|
|
70
|
-
self._seen.pop()
|
|
71
|
-
self._seen.add(key)
|
|
72
|
-
return True
|
|
73
|
-
|
|
74
|
-
def __call__(
|
|
75
|
-
self,
|
|
76
|
-
data: Mapping[Hashable, Any] | Hashable,
|
|
77
|
-
value: Any | None = None,
|
|
78
|
-
) -> None:
|
|
79
|
-
"""Log new keys found in ``data``.
|
|
80
|
-
|
|
81
|
-
``data`` may be a mapping of keys to payloads or a single key. When
|
|
82
|
-
called with a single key ``value`` customises the payload passed to the
|
|
83
|
-
logging message; the key itself is used when ``value`` is omitted.
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
if isinstance(data, Mapping):
|
|
87
|
-
new_items: dict[Hashable, Any] = {}
|
|
88
|
-
with self._lock:
|
|
89
|
-
for key, item_value in data.items():
|
|
90
|
-
if self._mark_seen(key):
|
|
91
|
-
new_items[key] = item_value
|
|
92
|
-
if new_items:
|
|
93
|
-
self._logger.warning(self._msg, new_items)
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
key = data
|
|
97
|
-
payload = value if value is not None else data
|
|
98
|
-
with self._lock:
|
|
99
|
-
should_log = self._mark_seen(key)
|
|
100
|
-
if should_log:
|
|
101
|
-
self._logger.warning(self._msg, payload)
|
|
102
|
-
|
|
103
|
-
def clear(self) -> None:
|
|
104
|
-
"""Reset tracked keys."""
|
|
105
|
-
with self._lock:
|
|
106
|
-
self._seen.clear()
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def warn_once(
|
|
110
|
-
logger: logging.Logger,
|
|
111
|
-
msg: str,
|
|
112
|
-
*,
|
|
113
|
-
maxsize: int = 1024,
|
|
114
|
-
) -> WarnOnce:
|
|
115
|
-
"""Return a :class:`WarnOnce` logger."""
|
|
116
|
-
return WarnOnce(logger, msg, maxsize=maxsize)
|