tnfr 3.0.3__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 (360) hide show
  1. tnfr/__init__.py +375 -56
  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 +723 -0
  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 +171 -0
  15. tnfr/cache.pyi +13 -0
  16. tnfr/cli/__init__.py +110 -0
  17. tnfr/cli/__init__.pyi +26 -0
  18. tnfr/cli/arguments.py +489 -0
  19. tnfr/cli/arguments.pyi +29 -0
  20. tnfr/cli/execution.py +914 -0
  21. tnfr/cli/execution.pyi +70 -0
  22. tnfr/cli/interactive_validator.py +614 -0
  23. tnfr/cli/utils.py +51 -0
  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/config/constants.py +104 -0
  34. tnfr/config/constants.pyi +12 -0
  35. tnfr/config/defaults.py +54 -0
  36. tnfr/config/defaults_core.py +212 -0
  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 +92 -0
  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 +33 -0
  57. tnfr/constants/init.pyi +12 -0
  58. tnfr/constants/metric.py +104 -0
  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 +238 -0
  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 +3034 -0
  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 +661 -0
  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 +36 -0
  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 +223 -0
  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 +262 -0
  125. tnfr/flatten.pyi +21 -0
  126. tnfr/gamma.py +354 -0
  127. tnfr/gamma.pyi +36 -0
  128. tnfr/glyph_history.py +377 -0
  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 +218 -0
  133. tnfr/immutable.pyi +36 -0
  134. tnfr/initialization.py +203 -0
  135. tnfr/initialization.pyi +65 -0
  136. tnfr/io.py +10 -0
  137. tnfr/io.pyi +13 -0
  138. tnfr/locking.py +37 -0
  139. tnfr/locking.pyi +7 -0
  140. tnfr/mathematics/__init__.py +79 -0
  141. tnfr/mathematics/backend.py +453 -0
  142. tnfr/mathematics/backend.pyi +99 -0
  143. tnfr/mathematics/dynamics.py +408 -0
  144. tnfr/mathematics/dynamics.pyi +90 -0
  145. tnfr/mathematics/epi.py +391 -0
  146. tnfr/mathematics/epi.pyi +65 -0
  147. tnfr/mathematics/generators.py +242 -0
  148. tnfr/mathematics/generators.pyi +29 -0
  149. tnfr/mathematics/metrics.py +119 -0
  150. tnfr/mathematics/metrics.pyi +16 -0
  151. tnfr/mathematics/operators.py +239 -0
  152. tnfr/mathematics/operators.pyi +59 -0
  153. tnfr/mathematics/operators_factory.py +124 -0
  154. tnfr/mathematics/operators_factory.pyi +11 -0
  155. tnfr/mathematics/projection.py +87 -0
  156. tnfr/mathematics/projection.pyi +33 -0
  157. tnfr/mathematics/runtime.py +182 -0
  158. tnfr/mathematics/runtime.pyi +64 -0
  159. tnfr/mathematics/spaces.py +256 -0
  160. tnfr/mathematics/spaces.pyi +83 -0
  161. tnfr/mathematics/transforms.py +305 -0
  162. tnfr/mathematics/transforms.pyi +62 -0
  163. tnfr/metrics/__init__.py +79 -0
  164. tnfr/metrics/__init__.pyi +20 -0
  165. tnfr/metrics/buffer_cache.py +163 -0
  166. tnfr/metrics/buffer_cache.pyi +24 -0
  167. tnfr/metrics/cache_utils.py +214 -0
  168. tnfr/metrics/coherence.py +2009 -0
  169. tnfr/metrics/coherence.pyi +129 -0
  170. tnfr/metrics/common.py +158 -0
  171. tnfr/metrics/common.pyi +35 -0
  172. tnfr/metrics/core.py +316 -0
  173. tnfr/metrics/core.pyi +13 -0
  174. tnfr/metrics/diagnosis.py +833 -0
  175. tnfr/metrics/diagnosis.pyi +86 -0
  176. tnfr/metrics/emergence.py +245 -0
  177. tnfr/metrics/export.py +179 -0
  178. tnfr/metrics/export.pyi +7 -0
  179. tnfr/metrics/glyph_timing.py +379 -0
  180. tnfr/metrics/glyph_timing.pyi +81 -0
  181. tnfr/metrics/learning_metrics.py +280 -0
  182. tnfr/metrics/learning_metrics.pyi +21 -0
  183. tnfr/metrics/phase_coherence.py +351 -0
  184. tnfr/metrics/phase_compatibility.py +349 -0
  185. tnfr/metrics/reporting.py +183 -0
  186. tnfr/metrics/reporting.pyi +25 -0
  187. tnfr/metrics/sense_index.py +1203 -0
  188. tnfr/metrics/sense_index.pyi +9 -0
  189. tnfr/metrics/trig.py +373 -0
  190. tnfr/metrics/trig.pyi +13 -0
  191. tnfr/metrics/trig_cache.py +233 -0
  192. tnfr/metrics/trig_cache.pyi +10 -0
  193. tnfr/multiscale/__init__.py +32 -0
  194. tnfr/multiscale/hierarchical.py +517 -0
  195. tnfr/node.py +763 -0
  196. tnfr/node.pyi +139 -0
  197. tnfr/observers.py +255 -130
  198. tnfr/observers.pyi +31 -0
  199. tnfr/ontosim.py +144 -137
  200. tnfr/ontosim.pyi +28 -0
  201. tnfr/operators/__init__.py +1672 -0
  202. tnfr/operators/__init__.pyi +31 -0
  203. tnfr/operators/algebra.py +277 -0
  204. tnfr/operators/canonical_patterns.py +420 -0
  205. tnfr/operators/cascade.py +267 -0
  206. tnfr/operators/cycle_detection.py +358 -0
  207. tnfr/operators/definitions.py +4108 -0
  208. tnfr/operators/definitions.pyi +78 -0
  209. tnfr/operators/grammar.py +1164 -0
  210. tnfr/operators/grammar.pyi +140 -0
  211. tnfr/operators/hamiltonian.py +710 -0
  212. tnfr/operators/health_analyzer.py +809 -0
  213. tnfr/operators/jitter.py +272 -0
  214. tnfr/operators/jitter.pyi +11 -0
  215. tnfr/operators/lifecycle.py +314 -0
  216. tnfr/operators/metabolism.py +618 -0
  217. tnfr/operators/metrics.py +2138 -0
  218. tnfr/operators/network_analysis/__init__.py +27 -0
  219. tnfr/operators/network_analysis/source_detection.py +186 -0
  220. tnfr/operators/nodal_equation.py +395 -0
  221. tnfr/operators/pattern_detection.py +660 -0
  222. tnfr/operators/patterns.py +669 -0
  223. tnfr/operators/postconditions/__init__.py +38 -0
  224. tnfr/operators/postconditions/mutation.py +236 -0
  225. tnfr/operators/preconditions/__init__.py +1226 -0
  226. tnfr/operators/preconditions/coherence.py +305 -0
  227. tnfr/operators/preconditions/dissonance.py +236 -0
  228. tnfr/operators/preconditions/emission.py +128 -0
  229. tnfr/operators/preconditions/mutation.py +580 -0
  230. tnfr/operators/preconditions/reception.py +125 -0
  231. tnfr/operators/preconditions/resonance.py +364 -0
  232. tnfr/operators/registry.py +74 -0
  233. tnfr/operators/registry.pyi +9 -0
  234. tnfr/operators/remesh.py +1809 -0
  235. tnfr/operators/remesh.pyi +26 -0
  236. tnfr/operators/structural_units.py +268 -0
  237. tnfr/operators/unified_grammar.py +105 -0
  238. tnfr/parallel/__init__.py +54 -0
  239. tnfr/parallel/auto_scaler.py +234 -0
  240. tnfr/parallel/distributed.py +384 -0
  241. tnfr/parallel/engine.py +238 -0
  242. tnfr/parallel/gpu_engine.py +420 -0
  243. tnfr/parallel/monitoring.py +248 -0
  244. tnfr/parallel/partitioner.py +459 -0
  245. tnfr/py.typed +0 -0
  246. tnfr/recipes/__init__.py +22 -0
  247. tnfr/recipes/cookbook.py +743 -0
  248. tnfr/rng.py +178 -0
  249. tnfr/rng.pyi +26 -0
  250. tnfr/schemas/__init__.py +8 -0
  251. tnfr/schemas/grammar.json +94 -0
  252. tnfr/sdk/__init__.py +107 -0
  253. tnfr/sdk/__init__.pyi +19 -0
  254. tnfr/sdk/adaptive_system.py +173 -0
  255. tnfr/sdk/adaptive_system.pyi +21 -0
  256. tnfr/sdk/builders.py +370 -0
  257. tnfr/sdk/builders.pyi +51 -0
  258. tnfr/sdk/fluent.py +1121 -0
  259. tnfr/sdk/fluent.pyi +74 -0
  260. tnfr/sdk/templates.py +342 -0
  261. tnfr/sdk/templates.pyi +41 -0
  262. tnfr/sdk/utils.py +341 -0
  263. tnfr/secure_config.py +46 -0
  264. tnfr/security/__init__.py +70 -0
  265. tnfr/security/database.py +514 -0
  266. tnfr/security/subprocess.py +503 -0
  267. tnfr/security/validation.py +290 -0
  268. tnfr/selector.py +247 -0
  269. tnfr/selector.pyi +19 -0
  270. tnfr/sense.py +378 -0
  271. tnfr/sense.pyi +23 -0
  272. tnfr/services/__init__.py +17 -0
  273. tnfr/services/orchestrator.py +325 -0
  274. tnfr/sparse/__init__.py +39 -0
  275. tnfr/sparse/representations.py +492 -0
  276. tnfr/structural.py +705 -0
  277. tnfr/structural.pyi +83 -0
  278. tnfr/telemetry/__init__.py +35 -0
  279. tnfr/telemetry/cache_metrics.py +226 -0
  280. tnfr/telemetry/cache_metrics.pyi +64 -0
  281. tnfr/telemetry/nu_f.py +422 -0
  282. tnfr/telemetry/nu_f.pyi +108 -0
  283. tnfr/telemetry/verbosity.py +36 -0
  284. tnfr/telemetry/verbosity.pyi +15 -0
  285. tnfr/tokens.py +58 -0
  286. tnfr/tokens.pyi +36 -0
  287. tnfr/tools/__init__.py +20 -0
  288. tnfr/tools/domain_templates.py +478 -0
  289. tnfr/tools/sequence_generator.py +846 -0
  290. tnfr/topology/__init__.py +13 -0
  291. tnfr/topology/asymmetry.py +151 -0
  292. tnfr/trace.py +543 -0
  293. tnfr/trace.pyi +42 -0
  294. tnfr/tutorials/__init__.py +38 -0
  295. tnfr/tutorials/autonomous_evolution.py +285 -0
  296. tnfr/tutorials/interactive.py +1576 -0
  297. tnfr/tutorials/structural_metabolism.py +238 -0
  298. tnfr/types.py +775 -0
  299. tnfr/types.pyi +357 -0
  300. tnfr/units.py +68 -0
  301. tnfr/units.pyi +13 -0
  302. tnfr/utils/__init__.py +282 -0
  303. tnfr/utils/__init__.pyi +215 -0
  304. tnfr/utils/cache.py +4223 -0
  305. tnfr/utils/cache.pyi +470 -0
  306. tnfr/utils/callbacks.py +375 -0
  307. tnfr/utils/callbacks.pyi +49 -0
  308. tnfr/utils/chunks.py +108 -0
  309. tnfr/utils/chunks.pyi +22 -0
  310. tnfr/utils/data.py +428 -0
  311. tnfr/utils/data.pyi +74 -0
  312. tnfr/utils/graph.py +85 -0
  313. tnfr/utils/graph.pyi +10 -0
  314. tnfr/utils/init.py +821 -0
  315. tnfr/utils/init.pyi +80 -0
  316. tnfr/utils/io.py +559 -0
  317. tnfr/utils/io.pyi +66 -0
  318. tnfr/utils/numeric.py +114 -0
  319. tnfr/utils/numeric.pyi +21 -0
  320. tnfr/validation/__init__.py +257 -0
  321. tnfr/validation/__init__.pyi +85 -0
  322. tnfr/validation/compatibility.py +460 -0
  323. tnfr/validation/compatibility.pyi +6 -0
  324. tnfr/validation/config.py +73 -0
  325. tnfr/validation/graph.py +139 -0
  326. tnfr/validation/graph.pyi +18 -0
  327. tnfr/validation/input_validation.py +755 -0
  328. tnfr/validation/invariants.py +712 -0
  329. tnfr/validation/rules.py +253 -0
  330. tnfr/validation/rules.pyi +44 -0
  331. tnfr/validation/runtime.py +279 -0
  332. tnfr/validation/runtime.pyi +28 -0
  333. tnfr/validation/sequence_validator.py +162 -0
  334. tnfr/validation/soft_filters.py +170 -0
  335. tnfr/validation/soft_filters.pyi +32 -0
  336. tnfr/validation/spectral.py +164 -0
  337. tnfr/validation/spectral.pyi +42 -0
  338. tnfr/validation/validator.py +1266 -0
  339. tnfr/validation/window.py +39 -0
  340. tnfr/validation/window.pyi +1 -0
  341. tnfr/visualization/__init__.py +98 -0
  342. tnfr/visualization/cascade_viz.py +256 -0
  343. tnfr/visualization/hierarchy.py +284 -0
  344. tnfr/visualization/sequence_plotter.py +784 -0
  345. tnfr/viz/__init__.py +60 -0
  346. tnfr/viz/matplotlib.py +278 -0
  347. tnfr/viz/matplotlib.pyi +35 -0
  348. tnfr-8.5.0.dist-info/METADATA +573 -0
  349. tnfr-8.5.0.dist-info/RECORD +353 -0
  350. tnfr-8.5.0.dist-info/entry_points.txt +3 -0
  351. tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-8.5.0.dist-info/licenses/LICENSE.md +1 -1
  352. tnfr/constants.py +0 -183
  353. tnfr/dynamics.py +0 -543
  354. tnfr/helpers.py +0 -198
  355. tnfr/main.py +0 -37
  356. tnfr/operators.py +0 -296
  357. tnfr-3.0.3.dist-info/METADATA +0 -35
  358. tnfr-3.0.3.dist-info/RECORD +0 -13
  359. {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
  360. {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,514 @@
1
+ """Secure database query utilities for SQL injection prevention.
2
+
3
+ This module provides utilities for safe database interactions, ensuring that
4
+ SQL queries are properly parameterized and identifiers are validated. These
5
+ utilities should be used whenever database functionality is added to TNFR.
6
+
7
+ Security Principles
8
+ -------------------
9
+ 1. **Parameterized Queries**: Always use placeholders (?, :name) for values
10
+ 2. **Identifier Validation**: Validate table/column names against whitelist
11
+ 3. **No String Concatenation**: Never build queries with string concatenation
12
+ 4. **Input Sanitization**: Validate and sanitize all user inputs
13
+
14
+ TNFR Structural Context
15
+ ------------------------
16
+ These utilities preserve TNFR structural coherence when persisting:
17
+ - EPI (Estructura Primaria de Información)
18
+ - NFR (Nodo Fractal Resonante) metadata
19
+ - Structural frequency (νf) measurements
20
+ - Phase relationships (φ) between nodes
21
+ - Coherence metrics C(t) and sense index Si
22
+
23
+ Example
24
+ -------
25
+ >>> # Safe parameterized query
26
+ >>> builder = SecureQueryBuilder()
27
+ >>> query, params = builder.select("nfr_nodes", ["id", "epi", "nu_f"])\\
28
+ ... .where("nu_f > ?", 0.5)\\
29
+ ... .order_by("nu_f", "DESC")\\
30
+ ... .build()
31
+ >>> print(query)
32
+ SELECT id, epi, nu_f FROM nfr_nodes WHERE nu_f > ? ORDER BY nu_f DESC
33
+ >>> print(params)
34
+ [0.5]
35
+
36
+ >>> # Validate identifiers before use
37
+ >>> table_name = validate_identifier("nfr_nodes")
38
+ >>> column_name = validate_identifier("nu_f")
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import re
44
+ from typing import Any
45
+
46
+ # Valid SQL identifier pattern: alphanumeric and underscores only, 1-64 chars
47
+ _VALID_IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]{0,63}$")
48
+
49
+ # Common SQL keywords that should not be used as identifiers
50
+ _SQL_KEYWORDS = frozenset(
51
+ {
52
+ "SELECT",
53
+ "INSERT",
54
+ "UPDATE",
55
+ "DELETE",
56
+ "DROP",
57
+ "CREATE",
58
+ "ALTER",
59
+ "TRUNCATE",
60
+ "EXEC",
61
+ "EXECUTE",
62
+ "UNION",
63
+ "FROM",
64
+ "WHERE",
65
+ "AND",
66
+ "OR",
67
+ "NOT",
68
+ "IN",
69
+ "EXISTS",
70
+ "JOIN",
71
+ "TABLE",
72
+ "DATABASE",
73
+ "GRANT",
74
+ "REVOKE",
75
+ }
76
+ )
77
+
78
+
79
+ class SQLInjectionError(ValueError):
80
+ """Exception raised when potential SQL injection is detected."""
81
+
82
+ pass
83
+
84
+
85
+ def validate_identifier(identifier: str, *, allow_keywords: bool = False) -> str:
86
+ """Validate a SQL identifier (table or column name).
87
+
88
+ This function ensures that identifiers are safe to use in SQL queries
89
+ by checking against a whitelist pattern and optionally rejecting SQL keywords.
90
+
91
+ Parameters
92
+ ----------
93
+ identifier : str
94
+ The identifier to validate (table name, column name, etc.)
95
+ allow_keywords : bool, optional
96
+ If False (default), reject SQL keywords as identifiers
97
+
98
+ Returns
99
+ -------
100
+ str
101
+ The validated identifier
102
+
103
+ Raises
104
+ ------
105
+ SQLInjectionError
106
+ If the identifier is invalid or potentially unsafe
107
+
108
+ Example
109
+ -------
110
+ >>> validate_identifier("nfr_nodes")
111
+ 'nfr_nodes'
112
+ >>> validate_identifier("nu_f_measurements")
113
+ 'nu_f_measurements'
114
+ >>> validate_identifier("DROP") # doctest: +SKIP
115
+ Traceback (most recent call last):
116
+ ...
117
+ SQLInjectionError: Identifier 'DROP' is a SQL keyword
118
+ >>> validate_identifier("invalid-name") # doctest: +SKIP
119
+ Traceback (most recent call last):
120
+ ...
121
+ SQLInjectionError: Invalid identifier 'invalid-name'
122
+ """
123
+ if not isinstance(identifier, str):
124
+ raise SQLInjectionError(
125
+ f"Identifier must be a string, got {type(identifier).__name__}"
126
+ )
127
+
128
+ if not identifier:
129
+ raise SQLInjectionError("Identifier cannot be empty")
130
+
131
+ if not _VALID_IDENTIFIER_PATTERN.match(identifier):
132
+ raise SQLInjectionError(
133
+ f"Invalid identifier '{identifier}': must contain only alphanumeric "
134
+ f"characters and underscores, start with letter or underscore, "
135
+ f"and be 1-64 characters long"
136
+ )
137
+
138
+ if not allow_keywords and identifier.upper() in _SQL_KEYWORDS:
139
+ raise SQLInjectionError(
140
+ f"Identifier '{identifier}' is a SQL keyword and cannot be used"
141
+ )
142
+
143
+ return identifier
144
+
145
+
146
+ def sanitize_string_input(value: str, *, max_length: int = 1000) -> str:
147
+ """Sanitize string input for safe database operations.
148
+
149
+ This function validates string inputs to prevent SQL injection and ensure
150
+ reasonable length constraints. Note: This is NOT a replacement for
151
+ parameterized queries, but an additional validation layer.
152
+
153
+ Parameters
154
+ ----------
155
+ value : str
156
+ The string value to sanitize
157
+ max_length : int, optional
158
+ Maximum allowed length (default: 1000)
159
+
160
+ Returns
161
+ -------
162
+ str
163
+ The validated string value
164
+
165
+ Raises
166
+ ------
167
+ SQLInjectionError
168
+ If the value is too long or contains suspicious patterns
169
+
170
+ Example
171
+ -------
172
+ >>> sanitize_string_input("valid string")
173
+ 'valid string'
174
+ >>> sanitize_string_input("a" * 10000) # doctest: +SKIP
175
+ Traceback (most recent call last):
176
+ ...
177
+ SQLInjectionError: Input exceeds maximum length...
178
+ """
179
+ if not isinstance(value, str):
180
+ raise SQLInjectionError(f"Value must be a string, got {type(value).__name__}")
181
+
182
+ if len(value) > max_length:
183
+ raise SQLInjectionError(
184
+ f"Input exceeds maximum length of {max_length} characters "
185
+ f"(got {len(value)})"
186
+ )
187
+
188
+ # Check for null bytes which can truncate strings in some contexts
189
+ if "\x00" in value:
190
+ raise SQLInjectionError("Input contains null bytes which are not allowed")
191
+
192
+ return value
193
+
194
+
195
+ class SecureQueryBuilder:
196
+ """Builder for constructing safe, parameterized SQL queries.
197
+
198
+ This class provides a fluent interface for building SQL queries with
199
+ proper parameterization to prevent SQL injection. All values are
200
+ automatically parameterized, and identifiers are validated.
201
+
202
+ TNFR Context
203
+ ------------
204
+ Use this builder when persisting TNFR structural data:
205
+ - NFR node states (EPI, νf, phase)
206
+ - Network topology and couplings
207
+ - Coherence measurements C(t)
208
+ - Sense index Si values
209
+ - Operator application history
210
+
211
+ Example
212
+ -------
213
+ >>> builder = SecureQueryBuilder()
214
+ >>> query, params = builder.select("nfr_nodes", ["id", "epi"])\\
215
+ ... .where("nu_f > ?", 0.5)\\
216
+ ... .where("phase BETWEEN ? AND ?", 0.0, 3.14)\\
217
+ ... .order_by("nu_f", "DESC")\\
218
+ ... .limit(10)\\
219
+ ... .build()
220
+ >>> print(query) # doctest: +NORMALIZE_WHITESPACE
221
+ SELECT id, epi FROM nfr_nodes WHERE nu_f > ? AND phase BETWEEN ? AND ?
222
+ ORDER BY nu_f DESC LIMIT 10
223
+ >>> print(params)
224
+ [0.5, 0.0, 3.14]
225
+ """
226
+
227
+ def __init__(self) -> None:
228
+ """Initialize a new query builder."""
229
+ self._query_parts: list[str] = []
230
+ self._params: list[Any] = []
231
+ self._operation: str | None = None
232
+
233
+ def select(
234
+ self, table: str, columns: list[str] | None = None
235
+ ) -> SecureQueryBuilder:
236
+ """Start a SELECT query.
237
+
238
+ Parameters
239
+ ----------
240
+ table : str
241
+ Table name (will be validated)
242
+ columns : list[str], optional
243
+ List of column names to select (default: all columns with *)
244
+
245
+ Returns
246
+ -------
247
+ SecureQueryBuilder
248
+ Self for method chaining
249
+ """
250
+ self._operation = "SELECT"
251
+ table = validate_identifier(table)
252
+
253
+ if columns is None:
254
+ cols = "*"
255
+ else:
256
+ validated_cols = [validate_identifier(col) for col in columns]
257
+ cols = ", ".join(validated_cols)
258
+
259
+ self._query_parts.append(f"SELECT {cols} FROM {table}")
260
+ return self
261
+
262
+ def insert(self, table: str, columns: list[str]) -> SecureQueryBuilder:
263
+ """Start an INSERT query.
264
+
265
+ Parameters
266
+ ----------
267
+ table : str
268
+ Table name (will be validated)
269
+ columns : list[str]
270
+ List of column names
271
+
272
+ Returns
273
+ -------
274
+ SecureQueryBuilder
275
+ Self for method chaining
276
+ """
277
+ self._operation = "INSERT"
278
+ table = validate_identifier(table)
279
+ validated_cols = [validate_identifier(col) for col in columns]
280
+ cols_str = ", ".join(validated_cols)
281
+ placeholders = ", ".join(["?"] * len(columns))
282
+ self._query_parts.append(
283
+ f"INSERT INTO {table} ({cols_str}) VALUES ({placeholders})"
284
+ )
285
+ return self
286
+
287
+ def update(self, table: str) -> SecureQueryBuilder:
288
+ """Start an UPDATE query.
289
+
290
+ Parameters
291
+ ----------
292
+ table : str
293
+ Table name (will be validated)
294
+
295
+ Returns
296
+ -------
297
+ SecureQueryBuilder
298
+ Self for method chaining
299
+ """
300
+ self._operation = "UPDATE"
301
+ table = validate_identifier(table)
302
+ self._query_parts.append(f"UPDATE {table}")
303
+ return self
304
+
305
+ def set(self, **columns: Any) -> SecureQueryBuilder:
306
+ """Add SET clause for UPDATE query.
307
+
308
+ Parameters
309
+ ----------
310
+ **columns
311
+ Column-value pairs to update
312
+
313
+ Returns
314
+ -------
315
+ SecureQueryBuilder
316
+ Self for method chaining
317
+ """
318
+ if self._operation != "UPDATE":
319
+ raise SQLInjectionError("SET clause can only be used with UPDATE")
320
+
321
+ validated_cols = [validate_identifier(col) for col in columns.keys()]
322
+ set_parts = [f"{col} = ?" for col in validated_cols]
323
+ self._params.extend(columns.values())
324
+ self._query_parts.append("SET " + ", ".join(set_parts))
325
+ return self
326
+
327
+ def delete(self, table: str) -> SecureQueryBuilder:
328
+ """Start a DELETE query.
329
+
330
+ Parameters
331
+ ----------
332
+ table : str
333
+ Table name (will be validated)
334
+
335
+ Returns
336
+ -------
337
+ SecureQueryBuilder
338
+ Self for method chaining
339
+ """
340
+ self._operation = "DELETE"
341
+ table = validate_identifier(table)
342
+ self._query_parts.append(f"DELETE FROM {table}")
343
+ return self
344
+
345
+ def where(self, condition: str, *values: Any) -> SecureQueryBuilder:
346
+ """Add a WHERE clause with parameterized values.
347
+
348
+ The condition should use '?' placeholders for values.
349
+
350
+ Parameters
351
+ ----------
352
+ condition : str
353
+ WHERE condition with '?' placeholders
354
+ *values
355
+ Values to bind to placeholders
356
+
357
+ Returns
358
+ -------
359
+ SecureQueryBuilder
360
+ Self for method chaining
361
+ """
362
+ # Validate that condition doesn't contain suspicious patterns
363
+ if ";" in condition or "--" in condition or "/*" in condition:
364
+ raise SQLInjectionError("WHERE condition contains suspicious SQL patterns")
365
+
366
+ # Count expected placeholders
367
+ expected_params = condition.count("?")
368
+ if len(values) != expected_params:
369
+ raise SQLInjectionError(
370
+ f"WHERE condition expects {expected_params} parameters, "
371
+ f"got {len(values)}"
372
+ )
373
+
374
+ prefix = (
375
+ "WHERE" if not any("WHERE" in part for part in self._query_parts) else "AND"
376
+ )
377
+ self._query_parts.append(f"{prefix} {condition}")
378
+ self._params.extend(values)
379
+ return self
380
+
381
+ def order_by(self, column: str, direction: str = "ASC") -> SecureQueryBuilder:
382
+ """Add an ORDER BY clause.
383
+
384
+ Parameters
385
+ ----------
386
+ column : str
387
+ Column name to order by (will be validated)
388
+ direction : str, optional
389
+ Sort direction: 'ASC' or 'DESC' (default: 'ASC')
390
+
391
+ Returns
392
+ -------
393
+ SecureQueryBuilder
394
+ Self for method chaining
395
+ """
396
+ column = validate_identifier(column)
397
+ direction = direction.upper()
398
+ if direction not in ("ASC", "DESC"):
399
+ raise SQLInjectionError(f"Invalid sort direction: {direction}")
400
+
401
+ self._query_parts.append(f"ORDER BY {column} {direction}")
402
+ return self
403
+
404
+ def limit(self, count: int) -> SecureQueryBuilder:
405
+ """Add a LIMIT clause.
406
+
407
+ Parameters
408
+ ----------
409
+ count : int
410
+ Maximum number of rows to return
411
+
412
+ Returns
413
+ -------
414
+ SecureQueryBuilder
415
+ Self for method chaining
416
+ """
417
+ if not isinstance(count, int) or count < 0:
418
+ raise SQLInjectionError("LIMIT count must be a non-negative integer")
419
+
420
+ self._query_parts.append(f"LIMIT {count}")
421
+ return self
422
+
423
+ def build(self) -> tuple[str, list[Any]]:
424
+ """Build the final query and parameter list.
425
+
426
+ Returns
427
+ -------
428
+ tuple[str, list[Any]]
429
+ A tuple of (query_string, parameters)
430
+ """
431
+ if not self._query_parts:
432
+ raise SQLInjectionError("Cannot build empty query")
433
+
434
+ query = " ".join(self._query_parts)
435
+ return query, self._params
436
+
437
+
438
+ def execute_parameterized_query(
439
+ query: str, params: list[Any] | tuple[Any, ...] | None = None
440
+ ) -> None:
441
+ """Execute a parameterized query safely.
442
+
443
+ This is a placeholder function that demonstrates the pattern for
444
+ executing parameterized queries. In actual usage, this would be
445
+ implemented with a real database connection.
446
+
447
+ Parameters
448
+ ----------
449
+ query : str
450
+ The SQL query with '?' placeholders
451
+ params : list or tuple, optional
452
+ Parameters to bind to the query
453
+
454
+ Raises
455
+ ------
456
+ SQLInjectionError
457
+ If the query appears to contain unparameterized values
458
+
459
+ Warning
460
+ -------
461
+ This is a demonstration function. Real implementations should:
462
+ 1. Use database-specific parameterization (e.g., sqlite3.execute)
463
+ 2. Handle connection pooling and transactions
464
+ 3. Implement proper error handling and logging
465
+ 4. Use context managers for resource cleanup
466
+
467
+ Example
468
+ -------
469
+ >>> # Safe parameterized query
470
+ >>> execute_parameterized_query(
471
+ ... "SELECT * FROM nfr_nodes WHERE nu_f > ?",
472
+ ... [0.5]
473
+ ... ) # doctest: +SKIP
474
+
475
+ >>> # UNSAFE: Never do this!
476
+ >>> # execute_parameterized_query(
477
+ >>> # f"SELECT * FROM nfr_nodes WHERE id = {user_input}"
478
+ >>> # )
479
+ """
480
+ if params is None:
481
+ params = []
482
+
483
+ # Basic validation: check for suspicious patterns
484
+ if "'" in query or '"' in query:
485
+ # Allow quoted identifiers if query has standard SQL keywords
486
+ if not any(
487
+ keyword in query.upper()
488
+ for keyword in ["SELECT", "INSERT", "UPDATE", "DELETE"]
489
+ ):
490
+ raise SQLInjectionError(
491
+ "Query contains quoted strings. Use parameterized queries instead."
492
+ )
493
+
494
+ # Verify that the number of placeholders matches the number of parameters
495
+ placeholder_count = query.count("?")
496
+ if placeholder_count != len(params):
497
+ raise SQLInjectionError(
498
+ f"Query has {placeholder_count} placeholders but {len(params)} "
499
+ f"parameters provided"
500
+ )
501
+
502
+ # In a real implementation, this would execute the query
503
+ # For example, with sqlite3:
504
+ # cursor.execute(query, params)
505
+ pass
506
+
507
+
508
+ __all__ = (
509
+ "SQLInjectionError",
510
+ "SecureQueryBuilder",
511
+ "execute_parameterized_query",
512
+ "sanitize_string_input",
513
+ "validate_identifier",
514
+ )