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.

Files changed (365) hide show
  1. tnfr/__init__.py +334 -50
  2. tnfr/__init__.pyi +33 -0
  3. tnfr/_compat.py +10 -0
  4. tnfr/_generated_version.py +34 -0
  5. tnfr/_version.py +49 -0
  6. tnfr/_version.pyi +7 -0
  7. tnfr/alias.py +214 -37
  8. tnfr/alias.pyi +108 -0
  9. tnfr/backends/__init__.py +354 -0
  10. tnfr/backends/jax_backend.py +173 -0
  11. tnfr/backends/numpy_backend.py +238 -0
  12. tnfr/backends/optimized_numpy.py +420 -0
  13. tnfr/backends/torch_backend.py +408 -0
  14. tnfr/cache.py +149 -556
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +51 -16
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +344 -32
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +676 -50
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +18 -3
  24. tnfr/cli/utils.pyi +7 -0
  25. tnfr/cli/validate.py +236 -0
  26. tnfr/compat/__init__.py +85 -0
  27. tnfr/compat/dataclass.py +136 -0
  28. tnfr/compat/jsonschema_stub.py +61 -0
  29. tnfr/compat/matplotlib_stub.py +73 -0
  30. tnfr/compat/numpy_stub.py +155 -0
  31. tnfr/config/__init__.py +224 -0
  32. tnfr/config/__init__.pyi +10 -0
  33. tnfr/{constants_glyphs.py → config/constants.py} +26 -20
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/{constants/core.py → config/defaults_core.py} +59 -6
  37. tnfr/config/defaults_init.py +33 -0
  38. tnfr/config/defaults_metric.py +104 -0
  39. tnfr/config/feature_flags.py +81 -0
  40. tnfr/config/feature_flags.pyi +16 -0
  41. tnfr/config/glyph_constants.py +31 -0
  42. tnfr/config/init.py +77 -0
  43. tnfr/config/init.pyi +8 -0
  44. tnfr/config/operator_names.py +254 -0
  45. tnfr/config/operator_names.pyi +36 -0
  46. tnfr/config/physics_derivation.py +354 -0
  47. tnfr/config/presets.py +83 -0
  48. tnfr/config/presets.pyi +7 -0
  49. tnfr/config/security.py +927 -0
  50. tnfr/config/thresholds.py +114 -0
  51. tnfr/config/tnfr_config.py +498 -0
  52. tnfr/constants/__init__.py +51 -133
  53. tnfr/constants/__init__.pyi +92 -0
  54. tnfr/constants/aliases.py +33 -0
  55. tnfr/constants/aliases.pyi +27 -0
  56. tnfr/constants/init.py +3 -1
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +9 -15
  59. tnfr/constants/metric.pyi +19 -0
  60. tnfr/core/__init__.py +33 -0
  61. tnfr/core/container.py +226 -0
  62. tnfr/core/default_implementations.py +329 -0
  63. tnfr/core/interfaces.py +279 -0
  64. tnfr/dynamics/__init__.py +213 -633
  65. tnfr/dynamics/__init__.pyi +83 -0
  66. tnfr/dynamics/adaptation.py +267 -0
  67. tnfr/dynamics/adaptation.pyi +7 -0
  68. tnfr/dynamics/adaptive_sequences.py +189 -0
  69. tnfr/dynamics/adaptive_sequences.pyi +14 -0
  70. tnfr/dynamics/aliases.py +23 -0
  71. tnfr/dynamics/aliases.pyi +19 -0
  72. tnfr/dynamics/bifurcation.py +232 -0
  73. tnfr/dynamics/canonical.py +229 -0
  74. tnfr/dynamics/canonical.pyi +48 -0
  75. tnfr/dynamics/coordination.py +385 -0
  76. tnfr/dynamics/coordination.pyi +25 -0
  77. tnfr/dynamics/dnfr.py +2699 -398
  78. tnfr/dynamics/dnfr.pyi +26 -0
  79. tnfr/dynamics/dynamic_limits.py +225 -0
  80. tnfr/dynamics/feedback.py +252 -0
  81. tnfr/dynamics/feedback.pyi +24 -0
  82. tnfr/dynamics/fused_dnfr.py +454 -0
  83. tnfr/dynamics/homeostasis.py +157 -0
  84. tnfr/dynamics/homeostasis.pyi +14 -0
  85. tnfr/dynamics/integrators.py +496 -102
  86. tnfr/dynamics/integrators.pyi +36 -0
  87. tnfr/dynamics/learning.py +310 -0
  88. tnfr/dynamics/learning.pyi +33 -0
  89. tnfr/dynamics/metabolism.py +254 -0
  90. tnfr/dynamics/nbody.py +796 -0
  91. tnfr/dynamics/nbody_tnfr.py +783 -0
  92. tnfr/dynamics/propagation.py +326 -0
  93. tnfr/dynamics/runtime.py +908 -0
  94. tnfr/dynamics/runtime.pyi +77 -0
  95. tnfr/dynamics/sampling.py +10 -5
  96. tnfr/dynamics/sampling.pyi +7 -0
  97. tnfr/dynamics/selectors.py +711 -0
  98. tnfr/dynamics/selectors.pyi +85 -0
  99. tnfr/dynamics/structural_clip.py +207 -0
  100. tnfr/errors/__init__.py +37 -0
  101. tnfr/errors/contextual.py +492 -0
  102. tnfr/execution.py +77 -55
  103. tnfr/execution.pyi +45 -0
  104. tnfr/extensions/__init__.py +205 -0
  105. tnfr/extensions/__init__.pyi +18 -0
  106. tnfr/extensions/base.py +173 -0
  107. tnfr/extensions/base.pyi +35 -0
  108. tnfr/extensions/business/__init__.py +71 -0
  109. tnfr/extensions/business/__init__.pyi +11 -0
  110. tnfr/extensions/business/cookbook.py +88 -0
  111. tnfr/extensions/business/cookbook.pyi +8 -0
  112. tnfr/extensions/business/health_analyzers.py +202 -0
  113. tnfr/extensions/business/health_analyzers.pyi +9 -0
  114. tnfr/extensions/business/patterns.py +183 -0
  115. tnfr/extensions/business/patterns.pyi +8 -0
  116. tnfr/extensions/medical/__init__.py +73 -0
  117. tnfr/extensions/medical/__init__.pyi +11 -0
  118. tnfr/extensions/medical/cookbook.py +88 -0
  119. tnfr/extensions/medical/cookbook.pyi +8 -0
  120. tnfr/extensions/medical/health_analyzers.py +181 -0
  121. tnfr/extensions/medical/health_analyzers.pyi +9 -0
  122. tnfr/extensions/medical/patterns.py +163 -0
  123. tnfr/extensions/medical/patterns.pyi +8 -0
  124. tnfr/flatten.py +29 -50
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +66 -53
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +144 -57
  129. tnfr/glyph_history.pyi +35 -0
  130. tnfr/glyph_runtime.py +19 -0
  131. tnfr/glyph_runtime.pyi +8 -0
  132. tnfr/immutable.py +70 -30
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +22 -16
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +5 -241
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.pyi +7 -0
  139. tnfr/mathematics/__init__.py +79 -0
  140. tnfr/mathematics/backend.py +453 -0
  141. tnfr/mathematics/backend.pyi +99 -0
  142. tnfr/mathematics/dynamics.py +408 -0
  143. tnfr/mathematics/dynamics.pyi +90 -0
  144. tnfr/mathematics/epi.py +391 -0
  145. tnfr/mathematics/epi.pyi +65 -0
  146. tnfr/mathematics/generators.py +242 -0
  147. tnfr/mathematics/generators.pyi +29 -0
  148. tnfr/mathematics/metrics.py +119 -0
  149. tnfr/mathematics/metrics.pyi +16 -0
  150. tnfr/mathematics/operators.py +239 -0
  151. tnfr/mathematics/operators.pyi +59 -0
  152. tnfr/mathematics/operators_factory.py +124 -0
  153. tnfr/mathematics/operators_factory.pyi +11 -0
  154. tnfr/mathematics/projection.py +87 -0
  155. tnfr/mathematics/projection.pyi +33 -0
  156. tnfr/mathematics/runtime.py +182 -0
  157. tnfr/mathematics/runtime.pyi +64 -0
  158. tnfr/mathematics/spaces.py +256 -0
  159. tnfr/mathematics/spaces.pyi +83 -0
  160. tnfr/mathematics/transforms.py +305 -0
  161. tnfr/mathematics/transforms.pyi +62 -0
  162. tnfr/metrics/__init__.py +47 -9
  163. tnfr/metrics/__init__.pyi +20 -0
  164. tnfr/metrics/buffer_cache.py +163 -0
  165. tnfr/metrics/buffer_cache.pyi +24 -0
  166. tnfr/metrics/cache_utils.py +214 -0
  167. tnfr/metrics/coherence.py +1510 -330
  168. tnfr/metrics/coherence.pyi +129 -0
  169. tnfr/metrics/common.py +23 -16
  170. tnfr/metrics/common.pyi +35 -0
  171. tnfr/metrics/core.py +251 -36
  172. tnfr/metrics/core.pyi +13 -0
  173. tnfr/metrics/diagnosis.py +709 -110
  174. tnfr/metrics/diagnosis.pyi +86 -0
  175. tnfr/metrics/emergence.py +245 -0
  176. tnfr/metrics/export.py +60 -18
  177. tnfr/metrics/export.pyi +7 -0
  178. tnfr/metrics/glyph_timing.py +233 -43
  179. tnfr/metrics/glyph_timing.pyi +81 -0
  180. tnfr/metrics/learning_metrics.py +280 -0
  181. tnfr/metrics/learning_metrics.pyi +21 -0
  182. tnfr/metrics/phase_coherence.py +351 -0
  183. tnfr/metrics/phase_compatibility.py +349 -0
  184. tnfr/metrics/reporting.py +63 -28
  185. tnfr/metrics/reporting.pyi +25 -0
  186. tnfr/metrics/sense_index.py +1126 -43
  187. tnfr/metrics/sense_index.pyi +9 -0
  188. tnfr/metrics/trig.py +215 -23
  189. tnfr/metrics/trig.pyi +13 -0
  190. tnfr/metrics/trig_cache.py +148 -24
  191. tnfr/metrics/trig_cache.pyi +10 -0
  192. tnfr/multiscale/__init__.py +32 -0
  193. tnfr/multiscale/hierarchical.py +517 -0
  194. tnfr/node.py +646 -140
  195. tnfr/node.pyi +139 -0
  196. tnfr/observers.py +160 -45
  197. tnfr/observers.pyi +31 -0
  198. tnfr/ontosim.py +23 -19
  199. tnfr/ontosim.pyi +28 -0
  200. tnfr/operators/__init__.py +1358 -106
  201. tnfr/operators/__init__.pyi +31 -0
  202. tnfr/operators/algebra.py +277 -0
  203. tnfr/operators/canonical_patterns.py +420 -0
  204. tnfr/operators/cascade.py +267 -0
  205. tnfr/operators/cycle_detection.py +358 -0
  206. tnfr/operators/definitions.py +4108 -0
  207. tnfr/operators/definitions.pyi +78 -0
  208. tnfr/operators/grammar.py +1164 -0
  209. tnfr/operators/grammar.pyi +140 -0
  210. tnfr/operators/hamiltonian.py +710 -0
  211. tnfr/operators/health_analyzer.py +809 -0
  212. tnfr/operators/jitter.py +107 -38
  213. tnfr/operators/jitter.pyi +11 -0
  214. tnfr/operators/lifecycle.py +314 -0
  215. tnfr/operators/metabolism.py +618 -0
  216. tnfr/operators/metrics.py +2138 -0
  217. tnfr/operators/network_analysis/__init__.py +27 -0
  218. tnfr/operators/network_analysis/source_detection.py +186 -0
  219. tnfr/operators/nodal_equation.py +395 -0
  220. tnfr/operators/pattern_detection.py +660 -0
  221. tnfr/operators/patterns.py +669 -0
  222. tnfr/operators/postconditions/__init__.py +38 -0
  223. tnfr/operators/postconditions/mutation.py +236 -0
  224. tnfr/operators/preconditions/__init__.py +1226 -0
  225. tnfr/operators/preconditions/coherence.py +305 -0
  226. tnfr/operators/preconditions/dissonance.py +236 -0
  227. tnfr/operators/preconditions/emission.py +128 -0
  228. tnfr/operators/preconditions/mutation.py +580 -0
  229. tnfr/operators/preconditions/reception.py +125 -0
  230. tnfr/operators/preconditions/resonance.py +364 -0
  231. tnfr/operators/registry.py +74 -0
  232. tnfr/operators/registry.pyi +9 -0
  233. tnfr/operators/remesh.py +1415 -91
  234. tnfr/operators/remesh.pyi +26 -0
  235. tnfr/operators/structural_units.py +268 -0
  236. tnfr/operators/unified_grammar.py +105 -0
  237. tnfr/parallel/__init__.py +54 -0
  238. tnfr/parallel/auto_scaler.py +234 -0
  239. tnfr/parallel/distributed.py +384 -0
  240. tnfr/parallel/engine.py +238 -0
  241. tnfr/parallel/gpu_engine.py +420 -0
  242. tnfr/parallel/monitoring.py +248 -0
  243. tnfr/parallel/partitioner.py +459 -0
  244. tnfr/py.typed +0 -0
  245. tnfr/recipes/__init__.py +22 -0
  246. tnfr/recipes/cookbook.py +743 -0
  247. tnfr/rng.py +75 -151
  248. tnfr/rng.pyi +26 -0
  249. tnfr/schemas/__init__.py +8 -0
  250. tnfr/schemas/grammar.json +94 -0
  251. tnfr/sdk/__init__.py +107 -0
  252. tnfr/sdk/__init__.pyi +19 -0
  253. tnfr/sdk/adaptive_system.py +173 -0
  254. tnfr/sdk/adaptive_system.pyi +21 -0
  255. tnfr/sdk/builders.py +370 -0
  256. tnfr/sdk/builders.pyi +51 -0
  257. tnfr/sdk/fluent.py +1121 -0
  258. tnfr/sdk/fluent.pyi +74 -0
  259. tnfr/sdk/templates.py +342 -0
  260. tnfr/sdk/templates.pyi +41 -0
  261. tnfr/sdk/utils.py +341 -0
  262. tnfr/secure_config.py +46 -0
  263. tnfr/security/__init__.py +70 -0
  264. tnfr/security/database.py +514 -0
  265. tnfr/security/subprocess.py +503 -0
  266. tnfr/security/validation.py +290 -0
  267. tnfr/selector.py +59 -22
  268. tnfr/selector.pyi +19 -0
  269. tnfr/sense.py +92 -67
  270. tnfr/sense.pyi +23 -0
  271. tnfr/services/__init__.py +17 -0
  272. tnfr/services/orchestrator.py +325 -0
  273. tnfr/sparse/__init__.py +39 -0
  274. tnfr/sparse/representations.py +492 -0
  275. tnfr/structural.py +639 -263
  276. tnfr/structural.pyi +83 -0
  277. tnfr/telemetry/__init__.py +35 -0
  278. tnfr/telemetry/cache_metrics.py +226 -0
  279. tnfr/telemetry/cache_metrics.pyi +64 -0
  280. tnfr/telemetry/nu_f.py +422 -0
  281. tnfr/telemetry/nu_f.pyi +108 -0
  282. tnfr/telemetry/verbosity.py +36 -0
  283. tnfr/telemetry/verbosity.pyi +15 -0
  284. tnfr/tokens.py +2 -4
  285. tnfr/tokens.pyi +36 -0
  286. tnfr/tools/__init__.py +20 -0
  287. tnfr/tools/domain_templates.py +478 -0
  288. tnfr/tools/sequence_generator.py +846 -0
  289. tnfr/topology/__init__.py +13 -0
  290. tnfr/topology/asymmetry.py +151 -0
  291. tnfr/trace.py +300 -126
  292. tnfr/trace.pyi +42 -0
  293. tnfr/tutorials/__init__.py +38 -0
  294. tnfr/tutorials/autonomous_evolution.py +285 -0
  295. tnfr/tutorials/interactive.py +1576 -0
  296. tnfr/tutorials/structural_metabolism.py +238 -0
  297. tnfr/types.py +743 -12
  298. tnfr/types.pyi +357 -0
  299. tnfr/units.py +68 -0
  300. tnfr/units.pyi +13 -0
  301. tnfr/utils/__init__.py +282 -0
  302. tnfr/utils/__init__.pyi +215 -0
  303. tnfr/utils/cache.py +4223 -0
  304. tnfr/utils/cache.pyi +470 -0
  305. tnfr/{callback_utils.py → utils/callbacks.py} +26 -39
  306. tnfr/utils/callbacks.pyi +49 -0
  307. tnfr/utils/chunks.py +108 -0
  308. tnfr/utils/chunks.pyi +22 -0
  309. tnfr/utils/data.py +428 -0
  310. tnfr/utils/data.pyi +74 -0
  311. tnfr/utils/graph.py +85 -0
  312. tnfr/utils/graph.pyi +10 -0
  313. tnfr/utils/init.py +821 -0
  314. tnfr/utils/init.pyi +80 -0
  315. tnfr/utils/io.py +559 -0
  316. tnfr/utils/io.pyi +66 -0
  317. tnfr/{helpers → utils}/numeric.py +51 -24
  318. tnfr/utils/numeric.pyi +21 -0
  319. tnfr/validation/__init__.py +257 -0
  320. tnfr/validation/__init__.pyi +85 -0
  321. tnfr/validation/compatibility.py +460 -0
  322. tnfr/validation/compatibility.pyi +6 -0
  323. tnfr/validation/config.py +73 -0
  324. tnfr/validation/graph.py +139 -0
  325. tnfr/validation/graph.pyi +18 -0
  326. tnfr/validation/input_validation.py +755 -0
  327. tnfr/validation/invariants.py +712 -0
  328. tnfr/validation/rules.py +253 -0
  329. tnfr/validation/rules.pyi +44 -0
  330. tnfr/validation/runtime.py +279 -0
  331. tnfr/validation/runtime.pyi +28 -0
  332. tnfr/validation/sequence_validator.py +162 -0
  333. tnfr/validation/soft_filters.py +170 -0
  334. tnfr/validation/soft_filters.pyi +32 -0
  335. tnfr/validation/spectral.py +164 -0
  336. tnfr/validation/spectral.pyi +42 -0
  337. tnfr/validation/validator.py +1266 -0
  338. tnfr/validation/window.py +39 -0
  339. tnfr/validation/window.pyi +1 -0
  340. tnfr/visualization/__init__.py +98 -0
  341. tnfr/visualization/cascade_viz.py +256 -0
  342. tnfr/visualization/hierarchy.py +284 -0
  343. tnfr/visualization/sequence_plotter.py +784 -0
  344. tnfr/viz/__init__.py +60 -0
  345. tnfr/viz/matplotlib.py +278 -0
  346. tnfr/viz/matplotlib.pyi +35 -0
  347. tnfr-8.5.0.dist-info/METADATA +573 -0
  348. tnfr-8.5.0.dist-info/RECORD +353 -0
  349. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
  350. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
  351. tnfr/collections_utils.py +0 -300
  352. tnfr/config.py +0 -32
  353. tnfr/grammar.py +0 -344
  354. tnfr/graph_utils.py +0 -84
  355. tnfr/helpers/__init__.py +0 -71
  356. tnfr/import_utils.py +0 -228
  357. tnfr/json_utils.py +0 -162
  358. tnfr/logging_utils.py +0 -116
  359. tnfr/presets.py +0 -60
  360. tnfr/validators.py +0 -84
  361. tnfr/value_utils.py +0 -59
  362. tnfr-4.5.2.dist-info/METADATA +0 -379
  363. tnfr-4.5.2.dist-info/RECORD +0 -67
  364. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  365. {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/utils/init.pyi ADDED
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from collections.abc import Callable, Iterable, Iterator, Mapping
5
+ from typing import Any, Hashable, Literal
6
+
7
+ __all__: tuple[str, ...]
8
+
9
+ def __getattr__(name: str) -> Any: ...
10
+
11
+ class WarnOnce:
12
+ def __init__(
13
+ self, logger: logging.Logger, msg: str, *, maxsize: int = ...
14
+ ) -> None: ...
15
+ def __call__(
16
+ self,
17
+ data: Mapping[Hashable, Any] | Hashable,
18
+ value: Any | None = ...,
19
+ ) -> None: ...
20
+ def clear(self) -> None: ...
21
+
22
+ class LazyImportProxy:
23
+ def __init__(
24
+ self,
25
+ module_name: str,
26
+ attr: str | None,
27
+ emit: Literal["warn", "log", "both"],
28
+ fallback: Any | None,
29
+ ) -> None: ...
30
+ def __getattr__(self, name: str) -> Any: ...
31
+ def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
32
+ def __bool__(self) -> bool: ...
33
+ def __iter__(self) -> Iterator[Any]: ...
34
+ def resolve(self) -> Any: ...
35
+
36
+ class ImportRegistry:
37
+ limit: int
38
+ failed: Mapping[str, None]
39
+ warned: set[str]
40
+ lock: Any
41
+
42
+ def record_failure(self, key: str, *, module: str | None = ...) -> None: ...
43
+ def discard(self, key: str) -> None: ...
44
+ def mark_warning(self, module: str) -> bool: ...
45
+ def clear(self) -> None: ...
46
+ def __contains__(self, key: str) -> bool: ...
47
+
48
+ EMIT_MAP: Mapping[str, Callable[[str], None]]
49
+ IMPORT_LOG: ImportRegistry
50
+ _IMPORT_STATE: ImportRegistry
51
+ _LOGGING_CONFIGURED: bool
52
+ _DEFAULT_CACHE_SIZE: int
53
+ _FAILED_IMPORT_LIMIT: int
54
+
55
+ def _configure_root() -> None: ...
56
+ def _reset_import_state() -> None: ...
57
+ def _reset_logging_state() -> None: ...
58
+ def _warn_failure(module: str, attr: str | None, err: Exception) -> None: ...
59
+ def cached_import(
60
+ module_name: str,
61
+ attr: str | None = ...,
62
+ *,
63
+ fallback: Any | None = ...,
64
+ emit: Literal["warn", "log", "both"] = ...,
65
+ lazy: bool = ...,
66
+ ) -> Any | None: ...
67
+ def warm_cached_import(
68
+ module: str | tuple[str, str | None] | Iterable[str | tuple[str, str | None]],
69
+ *extra: str | tuple[str, str | None],
70
+ attr: str | None = ...,
71
+ fallback: Any | None = ...,
72
+ emit: Literal["warn", "log", "both"] = ...,
73
+ lazy: bool = ...,
74
+ resolve: bool = ...,
75
+ ) -> Any | dict[str, Any | None]: ...
76
+ def get_logger(name: str) -> logging.Logger: ...
77
+ def get_numpy() -> Any | None: ...
78
+ def get_nodenx() -> Any | None: ...
79
+ def prune_failed_imports(*modules: str) -> None: ...
80
+ def warn_once(logger: logging.Logger, msg: str, *, maxsize: int = ...) -> WarnOnce: ...
tnfr/utils/io.py ADDED
@@ -0,0 +1,559 @@
1
+ """Structured file and JSON utilities shared across the TNFR engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import tempfile
8
+ from dataclasses import dataclass
9
+ from functools import partial
10
+ from pathlib import Path
11
+ from typing import Any, Callable
12
+
13
+ from .init import LazyImportProxy, cached_import, get_logger, warn_once
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ _ORJSON_PARAMS_MSG = "'ensure_ascii', 'separators', 'cls' and extra kwargs are ignored when using orjson: %s"
18
+
19
+ _warn_ignored_params_once = warn_once(logger, _ORJSON_PARAMS_MSG)
20
+
21
+
22
+ def clear_orjson_param_warnings() -> None:
23
+ """Reset cached warnings for ignored :mod:`orjson` parameters."""
24
+
25
+ _warn_ignored_params_once.clear()
26
+
27
+
28
+ def _format_ignored_params(combo: frozenset[str]) -> str:
29
+ """Return a stable representation for ignored parameter combinations."""
30
+
31
+ return "{" + ", ".join(map(repr, sorted(combo))) + "}"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class JsonDumpsParams:
36
+ """Container describing the parameters used by :func:`json_dumps`."""
37
+
38
+ sort_keys: bool = False
39
+ default: Callable[[Any], Any] | None = None
40
+ ensure_ascii: bool = True
41
+ separators: tuple[str, str] = (",", ":")
42
+ cls: type[json.JSONEncoder] | None = None
43
+ to_bytes: bool = False
44
+
45
+
46
+ DEFAULT_PARAMS = JsonDumpsParams()
47
+
48
+
49
+ def _collect_ignored_params(
50
+ params: JsonDumpsParams, extra_kwargs: dict[str, Any]
51
+ ) -> frozenset[str]:
52
+ """Return a stable set of parameters ignored by :mod:`orjson`."""
53
+
54
+ ignored: set[str] = set()
55
+ if params.ensure_ascii is not True:
56
+ ignored.add("ensure_ascii")
57
+ if params.separators != (",", ":"):
58
+ ignored.add("separators")
59
+ if params.cls is not None:
60
+ ignored.add("cls")
61
+ if extra_kwargs:
62
+ ignored.update(extra_kwargs.keys())
63
+ return frozenset(ignored)
64
+
65
+
66
+ def _json_dumps_orjson(
67
+ orjson: Any,
68
+ obj: Any,
69
+ params: JsonDumpsParams,
70
+ **kwargs: Any,
71
+ ) -> bytes | str:
72
+ """Serialize using :mod:`orjson` and warn about unsupported parameters."""
73
+
74
+ ignored = _collect_ignored_params(params, kwargs)
75
+ if ignored:
76
+ _warn_ignored_params_once(ignored, _format_ignored_params(ignored))
77
+
78
+ option = orjson.OPT_SORT_KEYS if params.sort_keys else 0
79
+ data = orjson.dumps(obj, option=option, default=params.default)
80
+ return data if params.to_bytes else data.decode("utf-8")
81
+
82
+
83
+ def _json_dumps_std(
84
+ obj: Any,
85
+ params: JsonDumpsParams,
86
+ **kwargs: Any,
87
+ ) -> bytes | str:
88
+ """Serialize using the standard library :func:`json.dumps`."""
89
+
90
+ result = json.dumps(
91
+ obj,
92
+ sort_keys=params.sort_keys,
93
+ ensure_ascii=params.ensure_ascii,
94
+ separators=params.separators,
95
+ cls=params.cls,
96
+ default=params.default,
97
+ **kwargs,
98
+ )
99
+ return result if not params.to_bytes else result.encode("utf-8")
100
+
101
+
102
+ def json_dumps(
103
+ obj: Any,
104
+ *,
105
+ sort_keys: bool = False,
106
+ default: Callable[[Any], Any] | None = None,
107
+ ensure_ascii: bool = True,
108
+ separators: tuple[str, str] = (",", ":"),
109
+ cls: type[json.JSONEncoder] | None = None,
110
+ to_bytes: bool = False,
111
+ **kwargs: Any,
112
+ ) -> bytes | str:
113
+ """Serialize ``obj`` to JSON using ``orjson`` when available."""
114
+
115
+ if not isinstance(sort_keys, bool):
116
+ raise TypeError("sort_keys must be a boolean")
117
+ if default is not None and not callable(default):
118
+ raise TypeError("default must be callable when provided")
119
+ if not isinstance(ensure_ascii, bool):
120
+ raise TypeError("ensure_ascii must be a boolean")
121
+ if not isinstance(separators, tuple) or len(separators) != 2:
122
+ raise TypeError("separators must be a tuple of two strings")
123
+ if not all(isinstance(part, str) for part in separators):
124
+ raise TypeError("separators must be a tuple of two strings")
125
+ if cls is not None:
126
+ if not isinstance(cls, type) or not issubclass(cls, json.JSONEncoder):
127
+ raise TypeError("cls must be a subclass of json.JSONEncoder")
128
+ if not isinstance(to_bytes, bool):
129
+ raise TypeError("to_bytes must be a boolean")
130
+
131
+ if (
132
+ sort_keys is False
133
+ and default is None
134
+ and ensure_ascii is True
135
+ and separators == (",", ":")
136
+ and cls is None
137
+ and to_bytes is False
138
+ ):
139
+ params = DEFAULT_PARAMS
140
+ else:
141
+ params = JsonDumpsParams(
142
+ sort_keys=sort_keys,
143
+ default=default,
144
+ ensure_ascii=ensure_ascii,
145
+ separators=separators,
146
+ cls=cls,
147
+ to_bytes=to_bytes,
148
+ )
149
+ orjson = cached_import("orjson", emit="log")
150
+ if orjson is not None:
151
+ return _json_dumps_orjson(orjson, obj, params, **kwargs)
152
+ return _json_dumps_std(obj, params, **kwargs)
153
+
154
+
155
+ def _raise_import_error(name: str, *_: Any, **__: Any) -> Any:
156
+ raise ImportError(f"{name} is not installed")
157
+
158
+
159
+ _MISSING_TOML_ERROR = type(
160
+ "MissingTOMLDependencyError",
161
+ (Exception,),
162
+ {"__doc__": "Fallback error used when tomllib/tomli is missing."},
163
+ )
164
+
165
+ _MISSING_YAML_ERROR = type(
166
+ "MissingPyYAMLDependencyError",
167
+ (Exception,),
168
+ {"__doc__": "Fallback error used when pyyaml is missing."},
169
+ )
170
+
171
+
172
+ def _resolve_lazy(value: Any) -> Any:
173
+ if isinstance(value, LazyImportProxy):
174
+ return value.resolve()
175
+ return value
176
+
177
+
178
+ class _LazyBool:
179
+ __slots__ = ("_value",)
180
+
181
+ def __init__(self, value: Any) -> None:
182
+ self._value = value
183
+
184
+ def __bool__(self) -> bool:
185
+ return _resolve_lazy(self._value) is not None
186
+
187
+
188
+ _TOMLI_MODULE = cached_import("tomli", emit="log", lazy=True)
189
+ tomllib = cached_import(
190
+ "tomllib",
191
+ emit="log",
192
+ lazy=True,
193
+ fallback=_TOMLI_MODULE,
194
+ )
195
+ has_toml = _LazyBool(tomllib)
196
+
197
+ _TOMLI_TOML_ERROR = cached_import(
198
+ "tomli",
199
+ "TOMLDecodeError",
200
+ emit="log",
201
+ lazy=True,
202
+ fallback=_MISSING_TOML_ERROR,
203
+ )
204
+ TOMLDecodeError = cached_import(
205
+ "tomllib",
206
+ "TOMLDecodeError",
207
+ emit="log",
208
+ lazy=True,
209
+ fallback=_TOMLI_TOML_ERROR,
210
+ )
211
+
212
+ _TOMLI_LOADS = cached_import(
213
+ "tomli",
214
+ "loads",
215
+ emit="log",
216
+ lazy=True,
217
+ fallback=partial(_raise_import_error, "tomllib/tomli"),
218
+ )
219
+ _TOML_LOADS: Callable[[str], Any] = cached_import(
220
+ "tomllib",
221
+ "loads",
222
+ emit="log",
223
+ lazy=True,
224
+ fallback=_TOMLI_LOADS,
225
+ )
226
+
227
+ yaml = cached_import("yaml", emit="log", lazy=True)
228
+
229
+ YAMLError = cached_import(
230
+ "yaml",
231
+ "YAMLError",
232
+ emit="log",
233
+ lazy=True,
234
+ fallback=_MISSING_YAML_ERROR,
235
+ )
236
+
237
+ _YAML_SAFE_LOAD: Callable[[str], Any] = cached_import(
238
+ "yaml",
239
+ "safe_load",
240
+ emit="log",
241
+ lazy=True,
242
+ fallback=partial(_raise_import_error, "pyyaml"),
243
+ )
244
+
245
+
246
+ def _parse_yaml(text: str) -> Any:
247
+ """Parse YAML ``text`` using ``safe_load`` if available."""
248
+
249
+ return _YAML_SAFE_LOAD(text)
250
+
251
+
252
+ def _parse_toml(text: str) -> Any:
253
+ """Parse TOML ``text`` using ``tomllib`` or ``tomli``."""
254
+
255
+ return _TOML_LOADS(text)
256
+
257
+
258
+ PARSERS = {
259
+ ".json": json.loads,
260
+ ".yaml": _parse_yaml,
261
+ ".yml": _parse_yaml,
262
+ ".toml": _parse_toml,
263
+ }
264
+
265
+
266
+ def _get_parser(suffix: str) -> Callable[[str], Any]:
267
+ try:
268
+ return PARSERS[suffix]
269
+ except KeyError as exc:
270
+ raise ValueError(f"Unsupported suffix: {suffix}") from exc
271
+
272
+
273
+ _BASE_ERROR_MESSAGES: dict[type[BaseException], str] = {
274
+ OSError: "Could not read {path}: {e}",
275
+ UnicodeDecodeError: "Encoding error while reading {path}: {e}",
276
+ json.JSONDecodeError: "Error parsing JSON file at {path}: {e}",
277
+ ImportError: "Missing dependency parsing {path}: {e}",
278
+ }
279
+
280
+
281
+ def _resolve_exception_type(candidate: Any) -> type[BaseException] | None:
282
+ resolved = _resolve_lazy(candidate)
283
+ if isinstance(resolved, type) and issubclass(resolved, BaseException):
284
+ return resolved
285
+ return None
286
+
287
+
288
+ _OPTIONAL_ERROR_MESSAGE_FACTORIES: tuple[
289
+ tuple[Callable[[], type[BaseException] | None], str],
290
+ ...,
291
+ ] = (
292
+ (
293
+ lambda: _resolve_exception_type(YAMLError),
294
+ "Error parsing YAML file at {path}: {e}",
295
+ ),
296
+ (
297
+ lambda: _resolve_exception_type(TOMLDecodeError),
298
+ "Error parsing TOML file at {path}: {e}",
299
+ ),
300
+ )
301
+
302
+ _BASE_STRUCTURED_EXCEPTIONS = (
303
+ OSError,
304
+ UnicodeDecodeError,
305
+ json.JSONDecodeError,
306
+ ImportError,
307
+ )
308
+
309
+
310
+ def _iter_optional_exceptions() -> list[type[BaseException]]:
311
+ errors: list[type[BaseException]] = []
312
+ for resolver, _ in _OPTIONAL_ERROR_MESSAGE_FACTORIES:
313
+ exc_type = resolver()
314
+ if exc_type is not None:
315
+ errors.append(exc_type)
316
+ return errors
317
+
318
+
319
+ def _is_structured_error(exc: Exception) -> bool:
320
+ if isinstance(exc, _BASE_STRUCTURED_EXCEPTIONS):
321
+ return True
322
+ for optional_exc in _iter_optional_exceptions():
323
+ if isinstance(exc, optional_exc):
324
+ return True
325
+ return False
326
+
327
+
328
+ def _format_structured_file_error(path: Path, e: Exception) -> str:
329
+ for exc, msg in _BASE_ERROR_MESSAGES.items():
330
+ if isinstance(e, exc):
331
+ return msg.format(path=path, e=e)
332
+
333
+ for resolver, msg in _OPTIONAL_ERROR_MESSAGE_FACTORIES:
334
+ exc_type = resolver()
335
+ if exc_type is not None and isinstance(e, exc_type):
336
+ return msg.format(path=path, e=e)
337
+
338
+ return f"Error parsing {path}: {e}"
339
+
340
+
341
+ class StructuredFileError(Exception):
342
+ """Error while reading or parsing a structured file."""
343
+
344
+ def __init__(self, path: Path, original: Exception) -> None:
345
+ super().__init__(_format_structured_file_error(path, original))
346
+ self.path = path
347
+
348
+
349
+ def read_structured_file(
350
+ path: Path | str,
351
+ *,
352
+ base_dir: Path | str | None = None,
353
+ allowed_extensions: tuple[str, ...] | None = (".json", ".yaml", ".yml", ".toml"),
354
+ ) -> Any:
355
+ """Read a JSON, YAML or TOML file and return parsed data.
356
+
357
+ This function includes path traversal protection. When ``base_dir`` is
358
+ provided, the resolved path must stay within that directory.
359
+
360
+ Parameters
361
+ ----------
362
+ path : Path | str
363
+ Path to the structured file to read.
364
+ base_dir : Path | str | None, optional
365
+ Base directory to restrict file access. If provided, the resolved
366
+ path must stay within this directory (prevents path traversal).
367
+ allowed_extensions : tuple[str, ...] | None, optional
368
+ Tuple of allowed file extensions. Default is JSON, YAML, and TOML.
369
+ Pass None to allow any extension (not recommended for user input).
370
+
371
+ Returns
372
+ -------
373
+ Any
374
+ Parsed data from the file.
375
+
376
+ Raises
377
+ ------
378
+ StructuredFileError
379
+ If the file cannot be read or parsed.
380
+ ValueError
381
+ If the path is invalid or contains unsafe patterns.
382
+ PathTraversalError
383
+ If path traversal is detected.
384
+
385
+ Examples
386
+ --------
387
+ >>> from pathlib import Path
388
+ >>> # Read config file with path validation
389
+ >>> data = read_structured_file("config.json") # doctest: +SKIP
390
+
391
+ >>> # Read with base directory restriction
392
+ >>> data = read_structured_file(
393
+ ... "settings.yaml",
394
+ ... base_dir="/home/user/configs"
395
+ ... ) # doctest: +SKIP
396
+ """
397
+ # Import here to avoid circular dependency
398
+ from ..security import resolve_safe_path, validate_file_path, PathTraversalError
399
+
400
+ # Validate and resolve the path
401
+ try:
402
+ if base_dir is not None:
403
+ # Resolve path within base directory (prevents traversal)
404
+ validated_path = resolve_safe_path(
405
+ path,
406
+ base_dir,
407
+ must_exist=True,
408
+ allowed_extensions=allowed_extensions,
409
+ )
410
+ else:
411
+ # Validate path without base directory restriction
412
+ path_obj = Path(path) if not isinstance(path, Path) else path
413
+ validated_path = validate_file_path(
414
+ path_obj,
415
+ allow_absolute=True,
416
+ allowed_extensions=allowed_extensions,
417
+ ).resolve()
418
+
419
+ # Check existence
420
+ if not validated_path.exists():
421
+ raise FileNotFoundError(f"File not found: {validated_path}")
422
+ except (ValueError, PathTraversalError) as e:
423
+ raise StructuredFileError(Path(path), e) from e
424
+ except FileNotFoundError as e:
425
+ raise StructuredFileError(Path(path), e) from e
426
+
427
+ suffix = validated_path.suffix.lower()
428
+ try:
429
+ parser = _get_parser(suffix)
430
+ except ValueError as e:
431
+ raise StructuredFileError(validated_path, e) from e
432
+ try:
433
+ text = validated_path.read_text(encoding="utf-8")
434
+ return parser(text)
435
+ except Exception as e:
436
+ if _is_structured_error(e):
437
+ raise StructuredFileError(validated_path, e) from e
438
+ raise
439
+
440
+
441
+ def safe_write(
442
+ path: str | Path,
443
+ write: Callable[[Any], Any],
444
+ *,
445
+ mode: str = "w",
446
+ encoding: str | None = "utf-8",
447
+ atomic: bool = True,
448
+ sync: bool | None = None,
449
+ base_dir: str | Path | None = None,
450
+ **open_kwargs: Any,
451
+ ) -> None:
452
+ """Write to ``path`` ensuring parent directory exists and handle errors.
453
+
454
+ This function includes path traversal protection. When ``base_dir`` is
455
+ provided, the resolved path must stay within that directory.
456
+
457
+ Parameters
458
+ ----------
459
+ path:
460
+ Destination file path.
461
+ write:
462
+ Callback receiving the opened file object and performing the actual
463
+ write.
464
+ mode:
465
+ File mode passed to :func:`open`. Text modes (default) use UTF-8
466
+ encoding unless ``encoding`` is ``None``. When a binary mode is used
467
+ (``'b'`` in ``mode``) no encoding parameter is supplied so
468
+ ``write`` may write bytes.
469
+ encoding:
470
+ Encoding for text modes. Ignored for binary modes.
471
+ atomic:
472
+ When ``True`` (default) writes to a temporary file and atomically
473
+ replaces the destination after flushing to disk. When ``False``
474
+ writes directly to ``path`` without any atomicity guarantee.
475
+ sync:
476
+ When ``True`` flushes and fsyncs the file descriptor after writing.
477
+ ``None`` uses ``atomic`` to determine syncing behaviour.
478
+ base_dir:
479
+ Optional base directory to restrict file writes. If provided, the
480
+ resolved path must stay within this directory (prevents path traversal).
481
+
482
+ Raises
483
+ ------
484
+ ValueError
485
+ If the path is invalid or contains unsafe patterns.
486
+ PathTraversalError
487
+ If path traversal is detected when base_dir is provided.
488
+ """
489
+ # Import here to avoid circular dependency
490
+ from ..security import resolve_safe_path, validate_file_path, PathTraversalError
491
+
492
+ # Validate and resolve the path
493
+ try:
494
+ if base_dir is not None:
495
+ # Resolve path within base directory (prevents traversal)
496
+ validated_path = resolve_safe_path(
497
+ path,
498
+ base_dir,
499
+ must_exist=False,
500
+ )
501
+ else:
502
+ # Validate path without base directory restriction
503
+ path_obj = Path(path) if not isinstance(path, Path) else path
504
+ validated_path = validate_file_path(
505
+ path_obj,
506
+ allow_absolute=True,
507
+ ).resolve()
508
+ except (ValueError, PathTraversalError) as e:
509
+ raise type(e)(f"Invalid path {path!r}: {e}") from e
510
+
511
+ path = validated_path
512
+ path.parent.mkdir(parents=True, exist_ok=True)
513
+ open_params = dict(mode=mode, **open_kwargs)
514
+ if "b" not in mode and encoding is not None:
515
+ open_params["encoding"] = encoding
516
+ if sync is None:
517
+ sync = atomic
518
+ tmp_path: Path | None = None
519
+ try:
520
+ if atomic:
521
+ tmp_fd = tempfile.NamedTemporaryFile(dir=path.parent, delete=False)
522
+ tmp_path = Path(tmp_fd.name)
523
+ tmp_fd.close()
524
+ with open(tmp_path, **open_params) as fd:
525
+ write(fd)
526
+ if sync:
527
+ fd.flush()
528
+ os.fsync(fd.fileno())
529
+ try:
530
+ os.replace(tmp_path, path)
531
+ except OSError as e:
532
+ logger.error(
533
+ "Atomic replace failed for %s -> %s: %s", tmp_path, path, e
534
+ )
535
+ raise
536
+ else:
537
+ with open(path, **open_params) as fd:
538
+ write(fd)
539
+ if sync:
540
+ fd.flush()
541
+ os.fsync(fd.fileno())
542
+ except (OSError, ValueError, TypeError) as e:
543
+ raise type(e)(f"Failed to write file {path}: {e}") from e
544
+ finally:
545
+ if tmp_path is not None:
546
+ tmp_path.unlink(missing_ok=True)
547
+
548
+
549
+ __all__ = (
550
+ "JsonDumpsParams",
551
+ "DEFAULT_PARAMS",
552
+ "clear_orjson_param_warnings",
553
+ "json_dumps",
554
+ "read_structured_file",
555
+ "safe_write",
556
+ "StructuredFileError",
557
+ "TOMLDecodeError",
558
+ "YAMLError",
559
+ )
tnfr/utils/io.pyi ADDED
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from _typeshed import Incomplete
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any, Callable
8
+
9
+ __all__ = [
10
+ "JsonDumpsParams",
11
+ "DEFAULT_PARAMS",
12
+ "clear_orjson_param_warnings",
13
+ "json_dumps",
14
+ "read_structured_file",
15
+ "safe_write",
16
+ "StructuredFileError",
17
+ "TOMLDecodeError",
18
+ "YAMLError",
19
+ ]
20
+
21
+ def clear_orjson_param_warnings() -> None: ...
22
+ @dataclass(frozen=True)
23
+ class JsonDumpsParams:
24
+ sort_keys: bool = ...
25
+ default: Callable[[Any], Any] | None = ...
26
+ ensure_ascii: bool = ...
27
+ separators: tuple[str, str] = ...
28
+ cls: type[json.JSONEncoder] | None = ...
29
+ to_bytes: bool = ...
30
+
31
+ DEFAULT_PARAMS: Incomplete
32
+
33
+ def json_dumps(
34
+ obj: Any,
35
+ *,
36
+ sort_keys: bool = False,
37
+ default: Callable[[Any], Any] | None = None,
38
+ ensure_ascii: bool = True,
39
+ separators: tuple[str, str] = (",", ":"),
40
+ cls: type[json.JSONEncoder] | None = None,
41
+ to_bytes: bool = False,
42
+ **kwargs: Any,
43
+ ) -> bytes | str: ...
44
+
45
+ class _LazyBool:
46
+ def __init__(self, value: Any) -> None: ...
47
+ def __bool__(self) -> bool: ...
48
+
49
+ TOMLDecodeError: Incomplete
50
+ YAMLError: Incomplete
51
+
52
+ class StructuredFileError(Exception):
53
+ path: Incomplete
54
+ def __init__(self, path: Path, original: Exception) -> None: ...
55
+
56
+ def read_structured_file(path: Path) -> Any: ...
57
+ def safe_write(
58
+ path: str | Path,
59
+ write: Callable[[Any], Any],
60
+ *,
61
+ mode: str = "w",
62
+ encoding: str | None = "utf-8",
63
+ atomic: bool = True,
64
+ sync: bool | None = None,
65
+ **open_kwargs: Any,
66
+ ) -> None: ...