tnfr 4.5.1__py3-none-any.whl → 6.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.
Files changed (170) hide show
  1. tnfr/__init__.py +270 -90
  2. tnfr/__init__.pyi +40 -0
  3. tnfr/_compat.py +11 -0
  4. tnfr/_version.py +7 -0
  5. tnfr/_version.pyi +7 -0
  6. tnfr/alias.py +631 -0
  7. tnfr/alias.pyi +140 -0
  8. tnfr/cache.py +732 -0
  9. tnfr/cache.pyi +232 -0
  10. tnfr/callback_utils.py +381 -0
  11. tnfr/callback_utils.pyi +105 -0
  12. tnfr/cli/__init__.py +89 -0
  13. tnfr/cli/__init__.pyi +47 -0
  14. tnfr/cli/arguments.py +199 -0
  15. tnfr/cli/arguments.pyi +33 -0
  16. tnfr/cli/execution.py +322 -0
  17. tnfr/cli/execution.pyi +80 -0
  18. tnfr/cli/utils.py +34 -0
  19. tnfr/cli/utils.pyi +8 -0
  20. tnfr/config/__init__.py +12 -0
  21. tnfr/config/__init__.pyi +8 -0
  22. tnfr/config/constants.py +104 -0
  23. tnfr/config/constants.pyi +12 -0
  24. tnfr/config/init.py +36 -0
  25. tnfr/config/init.pyi +8 -0
  26. tnfr/config/operator_names.py +106 -0
  27. tnfr/config/operator_names.pyi +28 -0
  28. tnfr/config/presets.py +104 -0
  29. tnfr/config/presets.pyi +7 -0
  30. tnfr/constants/__init__.py +228 -0
  31. tnfr/constants/__init__.pyi +104 -0
  32. tnfr/constants/core.py +158 -0
  33. tnfr/constants/core.pyi +17 -0
  34. tnfr/constants/init.py +31 -0
  35. tnfr/constants/init.pyi +12 -0
  36. tnfr/constants/metric.py +102 -0
  37. tnfr/constants/metric.pyi +19 -0
  38. tnfr/constants_glyphs.py +16 -0
  39. tnfr/constants_glyphs.pyi +12 -0
  40. tnfr/dynamics/__init__.py +136 -0
  41. tnfr/dynamics/__init__.pyi +83 -0
  42. tnfr/dynamics/adaptation.py +201 -0
  43. tnfr/dynamics/aliases.py +22 -0
  44. tnfr/dynamics/coordination.py +343 -0
  45. tnfr/dynamics/dnfr.py +2315 -0
  46. tnfr/dynamics/dnfr.pyi +33 -0
  47. tnfr/dynamics/integrators.py +561 -0
  48. tnfr/dynamics/integrators.pyi +35 -0
  49. tnfr/dynamics/runtime.py +521 -0
  50. tnfr/dynamics/sampling.py +34 -0
  51. tnfr/dynamics/sampling.pyi +7 -0
  52. tnfr/dynamics/selectors.py +680 -0
  53. tnfr/execution.py +216 -0
  54. tnfr/execution.pyi +65 -0
  55. tnfr/flatten.py +283 -0
  56. tnfr/flatten.pyi +28 -0
  57. tnfr/gamma.py +320 -89
  58. tnfr/gamma.pyi +40 -0
  59. tnfr/glyph_history.py +337 -0
  60. tnfr/glyph_history.pyi +53 -0
  61. tnfr/grammar.py +23 -153
  62. tnfr/grammar.pyi +13 -0
  63. tnfr/helpers/__init__.py +151 -0
  64. tnfr/helpers/__init__.pyi +66 -0
  65. tnfr/helpers/numeric.py +88 -0
  66. tnfr/helpers/numeric.pyi +12 -0
  67. tnfr/immutable.py +214 -0
  68. tnfr/immutable.pyi +37 -0
  69. tnfr/initialization.py +199 -0
  70. tnfr/initialization.pyi +73 -0
  71. tnfr/io.py +311 -0
  72. tnfr/io.pyi +11 -0
  73. tnfr/locking.py +37 -0
  74. tnfr/locking.pyi +7 -0
  75. tnfr/metrics/__init__.py +41 -0
  76. tnfr/metrics/__init__.pyi +20 -0
  77. tnfr/metrics/coherence.py +1469 -0
  78. tnfr/metrics/common.py +149 -0
  79. tnfr/metrics/common.pyi +15 -0
  80. tnfr/metrics/core.py +259 -0
  81. tnfr/metrics/core.pyi +13 -0
  82. tnfr/metrics/diagnosis.py +840 -0
  83. tnfr/metrics/diagnosis.pyi +89 -0
  84. tnfr/metrics/export.py +151 -0
  85. tnfr/metrics/glyph_timing.py +369 -0
  86. tnfr/metrics/reporting.py +152 -0
  87. tnfr/metrics/reporting.pyi +12 -0
  88. tnfr/metrics/sense_index.py +294 -0
  89. tnfr/metrics/sense_index.pyi +9 -0
  90. tnfr/metrics/trig.py +216 -0
  91. tnfr/metrics/trig.pyi +12 -0
  92. tnfr/metrics/trig_cache.py +105 -0
  93. tnfr/metrics/trig_cache.pyi +10 -0
  94. tnfr/node.py +255 -177
  95. tnfr/node.pyi +161 -0
  96. tnfr/observers.py +154 -150
  97. tnfr/observers.pyi +46 -0
  98. tnfr/ontosim.py +135 -134
  99. tnfr/ontosim.pyi +33 -0
  100. tnfr/operators/__init__.py +452 -0
  101. tnfr/operators/__init__.pyi +31 -0
  102. tnfr/operators/definitions.py +181 -0
  103. tnfr/operators/definitions.pyi +92 -0
  104. tnfr/operators/jitter.py +266 -0
  105. tnfr/operators/jitter.pyi +11 -0
  106. tnfr/operators/registry.py +80 -0
  107. tnfr/operators/registry.pyi +15 -0
  108. tnfr/operators/remesh.py +569 -0
  109. tnfr/presets.py +10 -23
  110. tnfr/presets.pyi +7 -0
  111. tnfr/py.typed +0 -0
  112. tnfr/rng.py +440 -0
  113. tnfr/rng.pyi +14 -0
  114. tnfr/selector.py +217 -0
  115. tnfr/selector.pyi +19 -0
  116. tnfr/sense.py +307 -142
  117. tnfr/sense.pyi +30 -0
  118. tnfr/structural.py +69 -164
  119. tnfr/structural.pyi +46 -0
  120. tnfr/telemetry/__init__.py +13 -0
  121. tnfr/telemetry/verbosity.py +37 -0
  122. tnfr/tokens.py +61 -0
  123. tnfr/tokens.pyi +41 -0
  124. tnfr/trace.py +520 -95
  125. tnfr/trace.pyi +68 -0
  126. tnfr/types.py +382 -17
  127. tnfr/types.pyi +145 -0
  128. tnfr/utils/__init__.py +158 -0
  129. tnfr/utils/__init__.pyi +133 -0
  130. tnfr/utils/cache.py +755 -0
  131. tnfr/utils/cache.pyi +156 -0
  132. tnfr/utils/data.py +267 -0
  133. tnfr/utils/data.pyi +73 -0
  134. tnfr/utils/graph.py +87 -0
  135. tnfr/utils/graph.pyi +10 -0
  136. tnfr/utils/init.py +746 -0
  137. tnfr/utils/init.pyi +85 -0
  138. tnfr/utils/io.py +157 -0
  139. tnfr/utils/io.pyi +10 -0
  140. tnfr/utils/validators.py +130 -0
  141. tnfr/utils/validators.pyi +19 -0
  142. tnfr/validation/__init__.py +25 -0
  143. tnfr/validation/__init__.pyi +17 -0
  144. tnfr/validation/compatibility.py +59 -0
  145. tnfr/validation/compatibility.pyi +8 -0
  146. tnfr/validation/grammar.py +149 -0
  147. tnfr/validation/grammar.pyi +11 -0
  148. tnfr/validation/rules.py +194 -0
  149. tnfr/validation/rules.pyi +18 -0
  150. tnfr/validation/syntax.py +151 -0
  151. tnfr/validation/syntax.pyi +7 -0
  152. tnfr-6.0.0.dist-info/METADATA +135 -0
  153. tnfr-6.0.0.dist-info/RECORD +157 -0
  154. tnfr/cli.py +0 -322
  155. tnfr/config.py +0 -41
  156. tnfr/constants.py +0 -277
  157. tnfr/dynamics.py +0 -814
  158. tnfr/helpers.py +0 -264
  159. tnfr/main.py +0 -47
  160. tnfr/metrics.py +0 -597
  161. tnfr/operators.py +0 -525
  162. tnfr/program.py +0 -176
  163. tnfr/scenarios.py +0 -34
  164. tnfr/validators.py +0 -38
  165. tnfr-4.5.1.dist-info/METADATA +0 -221
  166. tnfr-4.5.1.dist-info/RECORD +0 -28
  167. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
  168. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
  169. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
  170. {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/initialization.py ADDED
@@ -0,0 +1,199 @@
1
+ """Node initialization."""
2
+
3
+ from __future__ import annotations
4
+ import random
5
+ from typing import TYPE_CHECKING, cast
6
+
7
+ from dataclasses import dataclass
8
+
9
+ from .constants import VF_KEY, THETA_KEY, get_graph_param
10
+ from .helpers.numeric import clamp
11
+ from .rng import make_rng
12
+ from .types import NodeInitAttrMap
13
+
14
+ if TYPE_CHECKING: # pragma: no cover
15
+ import networkx as nx
16
+
17
+ __all__ = ("InitParams", "init_node_attrs")
18
+
19
+
20
+ @dataclass
21
+ class InitParams:
22
+ """Parameters governing node initialisation."""
23
+
24
+ seed: int | None
25
+ init_rand_phase: bool
26
+ th_min: float
27
+ th_max: float
28
+ vf_mode: str
29
+ vf_min_lim: float
30
+ vf_max_lim: float
31
+ vf_uniform_min: float | None
32
+ vf_uniform_max: float | None
33
+ vf_mean: float
34
+ vf_std: float
35
+ clamp_to_limits: bool
36
+ si_min: float
37
+ si_max: float
38
+ epi_val: float
39
+
40
+ @classmethod
41
+ def from_graph(cls, G: "nx.Graph") -> "InitParams":
42
+ """Construct ``InitParams`` from ``G.graph`` configuration."""
43
+
44
+ return cls(
45
+ seed=get_graph_param(G, "RANDOM_SEED", int),
46
+ init_rand_phase=get_graph_param(G, "INIT_RANDOM_PHASE", bool),
47
+ th_min=get_graph_param(G, "INIT_THETA_MIN"),
48
+ th_max=get_graph_param(G, "INIT_THETA_MAX"),
49
+ vf_mode=str(get_graph_param(G, "INIT_VF_MODE", str)).lower(),
50
+ vf_min_lim=get_graph_param(G, "VF_MIN"),
51
+ vf_max_lim=get_graph_param(G, "VF_MAX"),
52
+ vf_uniform_min=get_graph_param(G, "INIT_VF_MIN"),
53
+ vf_uniform_max=get_graph_param(G, "INIT_VF_MAX"),
54
+ vf_mean=get_graph_param(G, "INIT_VF_MEAN"),
55
+ vf_std=get_graph_param(G, "INIT_VF_STD"),
56
+ clamp_to_limits=get_graph_param(
57
+ G, "INIT_VF_CLAMP_TO_LIMITS", bool
58
+ ),
59
+ si_min=get_graph_param(G, "INIT_SI_MIN"),
60
+ si_max=get_graph_param(G, "INIT_SI_MAX"),
61
+ epi_val=get_graph_param(G, "INIT_EPI_VALUE"),
62
+ )
63
+
64
+
65
+ def _init_phase(
66
+ nd: NodeInitAttrMap,
67
+ rng: random.Random,
68
+ *,
69
+ override: bool,
70
+ random_phase: bool,
71
+ th_min: float,
72
+ th_max: float,
73
+ ) -> None:
74
+ """Initialise ``θ`` in ``nd``."""
75
+ if random_phase:
76
+ if override or THETA_KEY not in nd:
77
+ nd[THETA_KEY] = rng.uniform(th_min, th_max)
78
+ else:
79
+ if override:
80
+ nd[THETA_KEY] = 0.0
81
+ else:
82
+ nd.setdefault(THETA_KEY, 0.0)
83
+
84
+
85
+ def _init_vf(
86
+ nd: NodeInitAttrMap,
87
+ rng: random.Random,
88
+ *,
89
+ override: bool,
90
+ mode: str,
91
+ vf_uniform_min: float,
92
+ vf_uniform_max: float,
93
+ vf_mean: float,
94
+ vf_std: float,
95
+ vf_min_lim: float,
96
+ vf_max_lim: float,
97
+ clamp_to_limits: bool,
98
+ ) -> None:
99
+ """Initialise ``νf`` in ``nd``."""
100
+ if mode == "uniform":
101
+ vf = rng.uniform(vf_uniform_min, vf_uniform_max)
102
+ elif mode == "normal":
103
+ for _ in range(16):
104
+ cand = rng.normalvariate(vf_mean, vf_std)
105
+ if vf_min_lim <= cand <= vf_max_lim:
106
+ vf = cand
107
+ break
108
+ else:
109
+ vf = min(
110
+ max(rng.normalvariate(vf_mean, vf_std), vf_min_lim),
111
+ vf_max_lim,
112
+ )
113
+ else:
114
+ vf = float(nd.get(VF_KEY, 0.5))
115
+ if clamp_to_limits:
116
+ vf = clamp(vf, vf_min_lim, vf_max_lim)
117
+ if override or VF_KEY not in nd:
118
+ nd[VF_KEY] = vf
119
+
120
+
121
+ def _init_si_epi(
122
+ nd: NodeInitAttrMap,
123
+ rng: random.Random,
124
+ *,
125
+ override: bool,
126
+ si_min: float,
127
+ si_max: float,
128
+ epi_val: float,
129
+ ) -> None:
130
+ """Initialise ``Si`` and ``EPI`` in ``nd``."""
131
+ if override or "EPI" not in nd:
132
+ nd["EPI"] = epi_val
133
+
134
+ si = rng.uniform(si_min, si_max)
135
+ if override or "Si" not in nd:
136
+ nd["Si"] = si
137
+
138
+
139
+ def init_node_attrs(G: "nx.Graph", *, override: bool = True) -> "nx.Graph":
140
+ """Initialise EPI, θ, νf and Si on the nodes of ``G``.
141
+
142
+ Parameters can be customised via ``G.graph`` entries:
143
+ ``RANDOM_SEED``, ``INIT_RANDOM_PHASE``, ``INIT_THETA_MIN/MAX``,
144
+ ``INIT_VF_MODE``, ``VF_MIN``, ``VF_MAX``, ``INIT_VF_MIN/MAX``,
145
+ ``INIT_VF_MEAN``, ``INIT_VF_STD`` and ``INIT_VF_CLAMP_TO_LIMITS``.
146
+ Ranges for ``Si`` are added via ``INIT_SI_MIN`` and ``INIT_SI_MAX``, and
147
+ for ``EPI`` via ``INIT_EPI_VALUE``. If ``INIT_VF_MIN`` is greater than
148
+ ``INIT_VF_MAX``, values are swapped and clamped to ``VF_MIN``/``VF_MAX``.
149
+ """
150
+ params = InitParams.from_graph(G)
151
+
152
+ vf_uniform_min = params.vf_uniform_min
153
+ vf_uniform_max = params.vf_uniform_max
154
+ vf_min_lim = params.vf_min_lim
155
+ vf_max_lim = params.vf_max_lim
156
+ if vf_uniform_min is None:
157
+ vf_uniform_min = vf_min_lim
158
+ if vf_uniform_max is None:
159
+ vf_uniform_max = vf_max_lim
160
+ if vf_uniform_min > vf_uniform_max:
161
+ vf_uniform_min, vf_uniform_max = vf_uniform_max, vf_uniform_min
162
+ params.vf_uniform_min = max(vf_uniform_min, vf_min_lim)
163
+ params.vf_uniform_max = min(vf_uniform_max, vf_max_lim)
164
+
165
+ rng = make_rng(params.seed, -1, G)
166
+ for _, nd in G.nodes(data=True):
167
+ node_attrs = cast(NodeInitAttrMap, nd)
168
+
169
+ _init_phase(
170
+ node_attrs,
171
+ rng,
172
+ override=override,
173
+ random_phase=params.init_rand_phase,
174
+ th_min=params.th_min,
175
+ th_max=params.th_max,
176
+ )
177
+ _init_vf(
178
+ node_attrs,
179
+ rng,
180
+ override=override,
181
+ mode=params.vf_mode,
182
+ vf_uniform_min=params.vf_uniform_min,
183
+ vf_uniform_max=params.vf_uniform_max,
184
+ vf_mean=params.vf_mean,
185
+ vf_std=params.vf_std,
186
+ vf_min_lim=params.vf_min_lim,
187
+ vf_max_lim=params.vf_max_lim,
188
+ clamp_to_limits=params.clamp_to_limits,
189
+ )
190
+ _init_si_epi(
191
+ node_attrs,
192
+ rng,
193
+ override=override,
194
+ si_min=params.si_min,
195
+ si_max=params.si_max,
196
+ epi_val=params.epi_val,
197
+ )
198
+
199
+ return G
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import random
5
+
6
+ import networkx as nx
7
+
8
+ from .types import NodeInitAttrMap
9
+
10
+ __all__: tuple[str, str] = ("InitParams", "init_node_attrs")
11
+
12
+
13
+ @dataclass
14
+ class InitParams:
15
+ seed: int | None
16
+ init_rand_phase: bool
17
+ th_min: float
18
+ th_max: float
19
+ vf_mode: str
20
+ vf_min_lim: float
21
+ vf_max_lim: float
22
+ vf_uniform_min: float | None
23
+ vf_uniform_max: float | None
24
+ vf_mean: float
25
+ vf_std: float
26
+ clamp_to_limits: bool
27
+ si_min: float
28
+ si_max: float
29
+ epi_val: float
30
+
31
+ @classmethod
32
+ def from_graph(cls, G: nx.Graph) -> InitParams: ...
33
+
34
+
35
+ def _init_phase(
36
+ nd: NodeInitAttrMap,
37
+ rng: random.Random,
38
+ *,
39
+ override: bool,
40
+ random_phase: bool,
41
+ th_min: float,
42
+ th_max: float,
43
+ ) -> None: ...
44
+
45
+
46
+ def _init_vf(
47
+ nd: NodeInitAttrMap,
48
+ rng: random.Random,
49
+ *,
50
+ override: bool,
51
+ mode: str,
52
+ vf_uniform_min: float,
53
+ vf_uniform_max: float,
54
+ vf_mean: float,
55
+ vf_std: float,
56
+ vf_min_lim: float,
57
+ vf_max_lim: float,
58
+ clamp_to_limits: bool,
59
+ ) -> None: ...
60
+
61
+
62
+ def _init_si_epi(
63
+ nd: NodeInitAttrMap,
64
+ rng: random.Random,
65
+ *,
66
+ override: bool,
67
+ si_min: float,
68
+ si_max: float,
69
+ epi_val: float,
70
+ ) -> None: ...
71
+
72
+
73
+ def init_node_attrs(G: nx.Graph, *, override: bool = True) -> nx.Graph: ...
tnfr/io.py ADDED
@@ -0,0 +1,311 @@
1
+ """Structured file I/O utilities.
2
+
3
+ Optional parsers such as ``tomllib``/``tomli`` and ``pyyaml`` are loaded via
4
+ the :func:`tnfr.utils.cached_import` helper. Their import results and
5
+ failure states are cached and can be cleared with
6
+ ``cached_import.cache_clear()`` and :func:`tnfr.utils.prune_failed_imports`
7
+ when needed.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import tempfile
15
+ from pathlib import Path
16
+ from typing import Any, Callable
17
+ from functools import partial
18
+
19
+ from .utils import LazyImportProxy, cached_import, get_logger
20
+
21
+
22
+ def _raise_import_error(name: str, *_: Any, **__: Any) -> Any:
23
+ raise ImportError(f"{name} is not installed")
24
+
25
+
26
+ _MISSING_TOML_ERROR = type(
27
+ "MissingTOMLDependencyError",
28
+ (Exception,),
29
+ {"__doc__": "Fallback error used when tomllib/tomli is missing."},
30
+ )
31
+
32
+ _MISSING_YAML_ERROR = type(
33
+ "MissingPyYAMLDependencyError",
34
+ (Exception,),
35
+ {"__doc__": "Fallback error used when pyyaml is missing."},
36
+ )
37
+
38
+
39
+ def _resolve_lazy(value: Any) -> Any:
40
+ if isinstance(value, LazyImportProxy):
41
+ return value.resolve()
42
+ return value
43
+
44
+
45
+ class _LazyBool:
46
+ __slots__ = ("_value",)
47
+
48
+ def __init__(self, value: Any) -> None:
49
+ self._value = value
50
+
51
+ def __bool__(self) -> bool:
52
+ return _resolve_lazy(self._value) is not None
53
+
54
+
55
+ _TOMLI_MODULE = cached_import("tomli", emit="log", lazy=True)
56
+ tomllib = cached_import(
57
+ "tomllib",
58
+ emit="log",
59
+ lazy=True,
60
+ fallback=_TOMLI_MODULE,
61
+ )
62
+ has_toml = _LazyBool(tomllib)
63
+
64
+
65
+ _TOMLI_TOML_ERROR = cached_import(
66
+ "tomli",
67
+ "TOMLDecodeError",
68
+ emit="log",
69
+ lazy=True,
70
+ fallback=_MISSING_TOML_ERROR,
71
+ )
72
+ TOMLDecodeError = cached_import(
73
+ "tomllib",
74
+ "TOMLDecodeError",
75
+ emit="log",
76
+ lazy=True,
77
+ fallback=_TOMLI_TOML_ERROR,
78
+ )
79
+
80
+
81
+ _TOMLI_LOADS = cached_import(
82
+ "tomli",
83
+ "loads",
84
+ emit="log",
85
+ lazy=True,
86
+ fallback=partial(_raise_import_error, "tomllib/tomli"),
87
+ )
88
+ _TOML_LOADS: Callable[[str], Any] = cached_import(
89
+ "tomllib",
90
+ "loads",
91
+ emit="log",
92
+ lazy=True,
93
+ fallback=_TOMLI_LOADS,
94
+ )
95
+
96
+
97
+ yaml = cached_import("yaml", emit="log", lazy=True)
98
+
99
+
100
+ YAMLError = cached_import(
101
+ "yaml",
102
+ "YAMLError",
103
+ emit="log",
104
+ lazy=True,
105
+ fallback=_MISSING_YAML_ERROR,
106
+ )
107
+
108
+
109
+ _YAML_SAFE_LOAD: Callable[[str], Any] = cached_import(
110
+ "yaml",
111
+ "safe_load",
112
+ emit="log",
113
+ lazy=True,
114
+ fallback=partial(_raise_import_error, "pyyaml"),
115
+ )
116
+
117
+
118
+ def _parse_yaml(text: str) -> Any:
119
+ """Parse YAML ``text`` using ``safe_load`` if available."""
120
+ return _YAML_SAFE_LOAD(text)
121
+
122
+
123
+ def _parse_toml(text: str) -> Any:
124
+ """Parse TOML ``text`` using ``tomllib`` or ``tomli``."""
125
+ return _TOML_LOADS(text)
126
+
127
+
128
+ PARSERS = {
129
+ ".json": json.loads,
130
+ ".yaml": _parse_yaml,
131
+ ".yml": _parse_yaml,
132
+ ".toml": _parse_toml,
133
+ }
134
+
135
+
136
+ def _get_parser(suffix: str) -> Callable[[str], Any]:
137
+ try:
138
+ return PARSERS[suffix]
139
+ except KeyError as exc:
140
+ raise ValueError(f"Unsupported suffix: {suffix}") from exc
141
+
142
+
143
+ _BASE_ERROR_MESSAGES: dict[type[BaseException], str] = {
144
+ OSError: "Could not read {path}: {e}",
145
+ UnicodeDecodeError: "Encoding error while reading {path}: {e}",
146
+ json.JSONDecodeError: "Error parsing JSON file at {path}: {e}",
147
+ ImportError: "Missing dependency parsing {path}: {e}",
148
+ }
149
+
150
+
151
+ def _resolve_exception_type(candidate: Any) -> type[BaseException] | None:
152
+ resolved = _resolve_lazy(candidate)
153
+ if isinstance(resolved, type) and issubclass(resolved, BaseException):
154
+ return resolved
155
+ return None
156
+
157
+
158
+ _OPTIONAL_ERROR_MESSAGE_FACTORIES: tuple[
159
+ tuple[Callable[[], type[BaseException] | None], str],
160
+ ...,
161
+ ] = (
162
+ (lambda: _resolve_exception_type(YAMLError), "Error parsing YAML file at {path}: {e}"),
163
+ (lambda: _resolve_exception_type(TOMLDecodeError), "Error parsing TOML file at {path}: {e}"),
164
+ )
165
+
166
+
167
+ _BASE_STRUCTURED_EXCEPTIONS = (
168
+ OSError,
169
+ UnicodeDecodeError,
170
+ json.JSONDecodeError,
171
+ ImportError,
172
+ )
173
+
174
+
175
+ def _iter_optional_exceptions() -> list[type[BaseException]]:
176
+ errors: list[type[BaseException]] = []
177
+ for resolver, _ in _OPTIONAL_ERROR_MESSAGE_FACTORIES:
178
+ exc_type = resolver()
179
+ if exc_type is not None:
180
+ errors.append(exc_type)
181
+ return errors
182
+
183
+
184
+ def _is_structured_error(exc: Exception) -> bool:
185
+ if isinstance(exc, _BASE_STRUCTURED_EXCEPTIONS):
186
+ return True
187
+ for optional_exc in _iter_optional_exceptions():
188
+ if isinstance(exc, optional_exc):
189
+ return True
190
+ return False
191
+
192
+
193
+ def _format_structured_file_error(path: Path, e: Exception) -> str:
194
+ for exc, msg in _BASE_ERROR_MESSAGES.items():
195
+ if isinstance(e, exc):
196
+ return msg.format(path=path, e=e)
197
+
198
+ for resolver, msg in _OPTIONAL_ERROR_MESSAGE_FACTORIES:
199
+ exc_type = resolver()
200
+ if exc_type is not None and isinstance(e, exc_type):
201
+ return msg.format(path=path, e=e)
202
+
203
+ return f"Error parsing {path}: {e}"
204
+
205
+
206
+ class StructuredFileError(Exception):
207
+ """Error while reading or parsing a structured file."""
208
+
209
+ def __init__(self, path: Path, original: Exception) -> None:
210
+ super().__init__(_format_structured_file_error(path, original))
211
+ self.path = path
212
+
213
+
214
+ def read_structured_file(path: Path) -> Any:
215
+ """Read a JSON, YAML or TOML file and return parsed data."""
216
+ suffix = path.suffix.lower()
217
+ try:
218
+ parser = _get_parser(suffix)
219
+ except ValueError as e:
220
+ raise StructuredFileError(path, e) from e
221
+ try:
222
+ text = path.read_text(encoding="utf-8")
223
+ return parser(text)
224
+ except Exception as e:
225
+ if _is_structured_error(e):
226
+ raise StructuredFileError(path, e) from e
227
+ raise
228
+
229
+
230
+ logger = get_logger(__name__)
231
+
232
+
233
+ def safe_write(
234
+ path: str | Path,
235
+ write: Callable[[Any], Any],
236
+ *,
237
+ mode: str = "w",
238
+ encoding: str | None = "utf-8",
239
+ atomic: bool = True,
240
+ sync: bool | None = None,
241
+ **open_kwargs: Any,
242
+ ) -> None:
243
+ """Write to ``path`` ensuring parent directory exists and handle errors.
244
+
245
+ Parameters
246
+ ----------
247
+ path:
248
+ Destination file path.
249
+ write:
250
+ Callback receiving the opened file object and performing the actual
251
+ write.
252
+ mode:
253
+ File mode passed to :func:`open`. Text modes (default) use UTF-8
254
+ encoding unless ``encoding`` is ``None``. When a binary mode is used
255
+ (``'b'`` in ``mode``) no encoding parameter is supplied so
256
+ ``write`` may write bytes.
257
+ encoding:
258
+ Encoding for text modes. Ignored for binary modes.
259
+ atomic:
260
+ When ``True`` (default) writes to a temporary file and atomically
261
+ replaces the destination after flushing to disk. When ``False``
262
+ writes directly to ``path`` without any atomicity guarantee.
263
+ sync:
264
+ When ``True`` flushes and fsyncs the file descriptor after writing.
265
+ ``None`` uses ``atomic`` to determine syncing behaviour.
266
+ """
267
+ path = Path(path)
268
+ path.parent.mkdir(parents=True, exist_ok=True)
269
+ open_params = dict(mode=mode, **open_kwargs)
270
+ if "b" not in mode and encoding is not None:
271
+ open_params["encoding"] = encoding
272
+ if sync is None:
273
+ sync = atomic
274
+ tmp_path: Path | None = None
275
+ try:
276
+ if atomic:
277
+ tmp_fd = tempfile.NamedTemporaryFile(dir=path.parent, delete=False)
278
+ tmp_path = Path(tmp_fd.name)
279
+ tmp_fd.close()
280
+ with open(tmp_path, **open_params) as fd:
281
+ write(fd)
282
+ if sync:
283
+ fd.flush()
284
+ os.fsync(fd.fileno())
285
+ try:
286
+ os.replace(tmp_path, path)
287
+ except OSError as e:
288
+ logger.error(
289
+ "Atomic replace failed for %s -> %s: %s", tmp_path, path, e
290
+ )
291
+ raise
292
+ else:
293
+ with open(path, **open_params) as fd:
294
+ write(fd)
295
+ if sync:
296
+ fd.flush()
297
+ os.fsync(fd.fileno())
298
+ except (OSError, ValueError, TypeError) as e:
299
+ raise type(e)(f"Failed to write file {path}: {e}") from e
300
+ finally:
301
+ if tmp_path is not None:
302
+ tmp_path.unlink(missing_ok=True)
303
+
304
+
305
+ __all__ = (
306
+ "read_structured_file",
307
+ "safe_write",
308
+ "StructuredFileError",
309
+ "TOMLDecodeError",
310
+ "YAMLError",
311
+ )
tnfr/io.pyi ADDED
@@ -0,0 +1,11 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ StructuredFileError: Any
8
+ TOMLDecodeError: Any
9
+ YAMLError: Any
10
+ read_structured_file: Any
11
+ safe_write: Any
tnfr/locking.py ADDED
@@ -0,0 +1,37 @@
1
+ """Utilities for named locks.
2
+
3
+ This module provides helpers to obtain process-wide ``threading.Lock``
4
+ instances identified by name. Locks are created lazily and reused,
5
+ allowing different modules to synchronise on shared resources without
6
+ redefining locks repeatedly.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import threading
12
+ from weakref import WeakValueDictionary
13
+
14
+ # Registry of locks by name guarded by ``_REGISTRY_LOCK``.
15
+ # Using ``WeakValueDictionary`` ensures that once a lock is no longer
16
+ # referenced elsewhere, it is removed from the registry automatically,
17
+ # keeping the catalogue aligned with active coherence nodes.
18
+ _locks: WeakValueDictionary[str, threading.Lock] = WeakValueDictionary()
19
+ _REGISTRY_LOCK = threading.Lock()
20
+
21
+
22
+ def get_lock(name: str) -> threading.Lock:
23
+ """Return a re-usable lock identified by ``name``.
24
+
25
+ The same lock object is returned for identical names. Locks are
26
+ created on first use and stored in a process-wide registry.
27
+ """
28
+
29
+ with _REGISTRY_LOCK:
30
+ lock = _locks.get(name)
31
+ if lock is None:
32
+ lock = threading.Lock()
33
+ _locks[name] = lock
34
+ return lock
35
+
36
+
37
+ __all__ = ["get_lock"]
tnfr/locking.pyi ADDED
@@ -0,0 +1,7 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ get_lock: Any
@@ -0,0 +1,41 @@
1
+ """Registerable metrics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .core import register_metrics_callbacks
6
+ from .reporting import (
7
+ Tg_global,
8
+ Tg_by_node,
9
+ latency_series,
10
+ glyphogram_series,
11
+ glyph_top,
12
+ build_metrics_summary,
13
+ )
14
+ from .coherence import (
15
+ coherence_matrix,
16
+ local_phase_sync,
17
+ local_phase_sync_weighted,
18
+ register_coherence_callbacks,
19
+ )
20
+ from .diagnosis import (
21
+ register_diagnosis_callbacks,
22
+ dissonance_events,
23
+ )
24
+ from .export import export_metrics
25
+
26
+ __all__ = (
27
+ "register_metrics_callbacks",
28
+ "Tg_global",
29
+ "Tg_by_node",
30
+ "latency_series",
31
+ "glyphogram_series",
32
+ "glyph_top",
33
+ "build_metrics_summary",
34
+ "coherence_matrix",
35
+ "local_phase_sync",
36
+ "local_phase_sync_weighted",
37
+ "register_coherence_callbacks",
38
+ "register_diagnosis_callbacks",
39
+ "dissonance_events",
40
+ "export_metrics",
41
+ )
@@ -0,0 +1,20 @@
1
+ from typing import Any
2
+
3
+ __all__: Any
4
+
5
+ def __getattr__(name: str) -> Any: ...
6
+
7
+ Tg_by_node: Any
8
+ Tg_global: Any
9
+ build_metrics_summary: Any
10
+ coherence_matrix: Any
11
+ dissonance_events: Any
12
+ export_metrics: Any
13
+ glyph_top: Any
14
+ glyphogram_series: Any
15
+ latency_series: Any
16
+ local_phase_sync: Any
17
+ local_phase_sync_weighted: Any
18
+ register_coherence_callbacks: Any
19
+ register_diagnosis_callbacks: Any
20
+ register_metrics_callbacks: Any