tnfr 4.5.2__py3-none-any.whl → 8.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tnfr might be problematic. Click here for more details.
- tnfr/__init__.py +334 -50
- tnfr/__init__.pyi +33 -0
- tnfr/_compat.py +10 -0
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +49 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +214 -37
- tnfr/alias.pyi +108 -0
- tnfr/backends/__init__.py +354 -0
- tnfr/backends/jax_backend.py +173 -0
- tnfr/backends/numpy_backend.py +238 -0
- tnfr/backends/optimized_numpy.py +420 -0
- tnfr/backends/torch_backend.py +408 -0
- tnfr/cache.py +149 -556
- tnfr/cache.pyi +13 -0
- tnfr/cli/__init__.py +51 -16
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +344 -32
- tnfr/cli/arguments.pyi +29 -0
- tnfr/cli/execution.py +676 -50
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/interactive_validator.py +614 -0
- tnfr/cli/utils.py +18 -3
- tnfr/cli/utils.pyi +7 -0
- tnfr/cli/validate.py +236 -0
- tnfr/compat/__init__.py +85 -0
- tnfr/compat/dataclass.py +136 -0
- tnfr/compat/jsonschema_stub.py +61 -0
- tnfr/compat/matplotlib_stub.py +73 -0
- tnfr/compat/numpy_stub.py +155 -0
- tnfr/config/__init__.py +224 -0
- tnfr/config/__init__.pyi +10 -0
- tnfr/{constants_glyphs.py → config/constants.py} +26 -20
- tnfr/config/constants.pyi +12 -0
- tnfr/config/defaults.py +54 -0
- tnfr/{constants/core.py → config/defaults_core.py} +59 -6
- tnfr/config/defaults_init.py +33 -0
- tnfr/config/defaults_metric.py +104 -0
- tnfr/config/feature_flags.py +81 -0
- tnfr/config/feature_flags.pyi +16 -0
- tnfr/config/glyph_constants.py +31 -0
- tnfr/config/init.py +77 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +254 -0
- tnfr/config/operator_names.pyi +36 -0
- tnfr/config/physics_derivation.py +354 -0
- tnfr/config/presets.py +83 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/config/security.py +927 -0
- tnfr/config/thresholds.py +114 -0
- tnfr/config/tnfr_config.py +498 -0
- tnfr/constants/__init__.py +51 -133
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +33 -0
- tnfr/constants/aliases.pyi +27 -0
- tnfr/constants/init.py +3 -1
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +9 -15
- tnfr/constants/metric.pyi +19 -0
- tnfr/core/__init__.py +33 -0
- tnfr/core/container.py +226 -0
- tnfr/core/default_implementations.py +329 -0
- tnfr/core/interfaces.py +279 -0
- tnfr/dynamics/__init__.py +213 -633
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +267 -0
- tnfr/dynamics/adaptation.pyi +7 -0
- tnfr/dynamics/adaptive_sequences.py +189 -0
- tnfr/dynamics/adaptive_sequences.pyi +14 -0
- tnfr/dynamics/aliases.py +23 -0
- tnfr/dynamics/aliases.pyi +19 -0
- tnfr/dynamics/bifurcation.py +232 -0
- tnfr/dynamics/canonical.py +229 -0
- tnfr/dynamics/canonical.pyi +48 -0
- tnfr/dynamics/coordination.py +385 -0
- tnfr/dynamics/coordination.pyi +25 -0
- tnfr/dynamics/dnfr.py +2699 -398
- tnfr/dynamics/dnfr.pyi +26 -0
- tnfr/dynamics/dynamic_limits.py +225 -0
- tnfr/dynamics/feedback.py +252 -0
- tnfr/dynamics/feedback.pyi +24 -0
- tnfr/dynamics/fused_dnfr.py +454 -0
- tnfr/dynamics/homeostasis.py +157 -0
- tnfr/dynamics/homeostasis.pyi +14 -0
- tnfr/dynamics/integrators.py +496 -102
- tnfr/dynamics/integrators.pyi +36 -0
- tnfr/dynamics/learning.py +310 -0
- tnfr/dynamics/learning.pyi +33 -0
- tnfr/dynamics/metabolism.py +254 -0
- tnfr/dynamics/nbody.py +796 -0
- tnfr/dynamics/nbody_tnfr.py +783 -0
- tnfr/dynamics/propagation.py +326 -0
- tnfr/dynamics/runtime.py +908 -0
- tnfr/dynamics/runtime.pyi +77 -0
- tnfr/dynamics/sampling.py +10 -5
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +711 -0
- tnfr/dynamics/selectors.pyi +85 -0
- tnfr/dynamics/structural_clip.py +207 -0
- tnfr/errors/__init__.py +37 -0
- tnfr/errors/contextual.py +492 -0
- tnfr/execution.py +77 -55
- tnfr/execution.pyi +45 -0
- tnfr/extensions/__init__.py +205 -0
- tnfr/extensions/__init__.pyi +18 -0
- tnfr/extensions/base.py +173 -0
- tnfr/extensions/base.pyi +35 -0
- tnfr/extensions/business/__init__.py +71 -0
- tnfr/extensions/business/__init__.pyi +11 -0
- tnfr/extensions/business/cookbook.py +88 -0
- tnfr/extensions/business/cookbook.pyi +8 -0
- tnfr/extensions/business/health_analyzers.py +202 -0
- tnfr/extensions/business/health_analyzers.pyi +9 -0
- tnfr/extensions/business/patterns.py +183 -0
- tnfr/extensions/business/patterns.pyi +8 -0
- tnfr/extensions/medical/__init__.py +73 -0
- tnfr/extensions/medical/__init__.pyi +11 -0
- tnfr/extensions/medical/cookbook.py +88 -0
- tnfr/extensions/medical/cookbook.pyi +8 -0
- tnfr/extensions/medical/health_analyzers.py +181 -0
- tnfr/extensions/medical/health_analyzers.pyi +9 -0
- tnfr/extensions/medical/patterns.py +163 -0
- tnfr/extensions/medical/patterns.pyi +8 -0
- tnfr/flatten.py +29 -50
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +66 -53
- tnfr/gamma.pyi +36 -0
- tnfr/glyph_history.py +144 -57
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +19 -0
- tnfr/glyph_runtime.pyi +8 -0
- tnfr/immutable.py +70 -30
- tnfr/immutable.pyi +36 -0
- tnfr/initialization.py +22 -16
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +5 -241
- tnfr/io.pyi +13 -0
- tnfr/locking.pyi +7 -0
- tnfr/mathematics/__init__.py +79 -0
- tnfr/mathematics/backend.py +453 -0
- tnfr/mathematics/backend.pyi +99 -0
- tnfr/mathematics/dynamics.py +408 -0
- tnfr/mathematics/dynamics.pyi +90 -0
- tnfr/mathematics/epi.py +391 -0
- tnfr/mathematics/epi.pyi +65 -0
- tnfr/mathematics/generators.py +242 -0
- tnfr/mathematics/generators.pyi +29 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/metrics.pyi +16 -0
- tnfr/mathematics/operators.py +239 -0
- tnfr/mathematics/operators.pyi +59 -0
- tnfr/mathematics/operators_factory.py +124 -0
- tnfr/mathematics/operators_factory.pyi +11 -0
- tnfr/mathematics/projection.py +87 -0
- tnfr/mathematics/projection.pyi +33 -0
- tnfr/mathematics/runtime.py +182 -0
- tnfr/mathematics/runtime.pyi +64 -0
- tnfr/mathematics/spaces.py +256 -0
- tnfr/mathematics/spaces.pyi +83 -0
- tnfr/mathematics/transforms.py +305 -0
- tnfr/mathematics/transforms.pyi +62 -0
- tnfr/metrics/__init__.py +47 -9
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/buffer_cache.py +163 -0
- tnfr/metrics/buffer_cache.pyi +24 -0
- tnfr/metrics/cache_utils.py +214 -0
- tnfr/metrics/coherence.py +1510 -330
- tnfr/metrics/coherence.pyi +129 -0
- tnfr/metrics/common.py +23 -16
- tnfr/metrics/common.pyi +35 -0
- tnfr/metrics/core.py +251 -36
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +709 -110
- tnfr/metrics/diagnosis.pyi +86 -0
- tnfr/metrics/emergence.py +245 -0
- tnfr/metrics/export.py +60 -18
- tnfr/metrics/export.pyi +7 -0
- tnfr/metrics/glyph_timing.py +233 -43
- tnfr/metrics/glyph_timing.pyi +81 -0
- tnfr/metrics/learning_metrics.py +280 -0
- tnfr/metrics/learning_metrics.pyi +21 -0
- tnfr/metrics/phase_coherence.py +351 -0
- tnfr/metrics/phase_compatibility.py +349 -0
- tnfr/metrics/reporting.py +63 -28
- tnfr/metrics/reporting.pyi +25 -0
- tnfr/metrics/sense_index.py +1126 -43
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +215 -23
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +148 -24
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/multiscale/__init__.py +32 -0
- tnfr/multiscale/hierarchical.py +517 -0
- tnfr/node.py +646 -140
- tnfr/node.pyi +139 -0
- tnfr/observers.py +160 -45
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +23 -19
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +1358 -106
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/algebra.py +277 -0
- tnfr/operators/canonical_patterns.py +420 -0
- tnfr/operators/cascade.py +267 -0
- tnfr/operators/cycle_detection.py +358 -0
- tnfr/operators/definitions.py +4108 -0
- tnfr/operators/definitions.pyi +78 -0
- tnfr/operators/grammar.py +1164 -0
- tnfr/operators/grammar.pyi +140 -0
- tnfr/operators/hamiltonian.py +710 -0
- tnfr/operators/health_analyzer.py +809 -0
- tnfr/operators/jitter.py +107 -38
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/lifecycle.py +314 -0
- tnfr/operators/metabolism.py +618 -0
- tnfr/operators/metrics.py +2138 -0
- tnfr/operators/network_analysis/__init__.py +27 -0
- tnfr/operators/network_analysis/source_detection.py +186 -0
- tnfr/operators/nodal_equation.py +395 -0
- tnfr/operators/pattern_detection.py +660 -0
- tnfr/operators/patterns.py +669 -0
- tnfr/operators/postconditions/__init__.py +38 -0
- tnfr/operators/postconditions/mutation.py +236 -0
- tnfr/operators/preconditions/__init__.py +1226 -0
- tnfr/operators/preconditions/coherence.py +305 -0
- tnfr/operators/preconditions/dissonance.py +236 -0
- tnfr/operators/preconditions/emission.py +128 -0
- tnfr/operators/preconditions/mutation.py +580 -0
- tnfr/operators/preconditions/reception.py +125 -0
- tnfr/operators/preconditions/resonance.py +364 -0
- tnfr/operators/registry.py +74 -0
- tnfr/operators/registry.pyi +9 -0
- tnfr/operators/remesh.py +1415 -91
- tnfr/operators/remesh.pyi +26 -0
- tnfr/operators/structural_units.py +268 -0
- tnfr/operators/unified_grammar.py +105 -0
- tnfr/parallel/__init__.py +54 -0
- tnfr/parallel/auto_scaler.py +234 -0
- tnfr/parallel/distributed.py +384 -0
- tnfr/parallel/engine.py +238 -0
- tnfr/parallel/gpu_engine.py +420 -0
- tnfr/parallel/monitoring.py +248 -0
- tnfr/parallel/partitioner.py +459 -0
- tnfr/py.typed +0 -0
- tnfr/recipes/__init__.py +22 -0
- tnfr/recipes/cookbook.py +743 -0
- tnfr/rng.py +75 -151
- tnfr/rng.pyi +26 -0
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/sdk/__init__.py +107 -0
- tnfr/sdk/__init__.pyi +19 -0
- tnfr/sdk/adaptive_system.py +173 -0
- tnfr/sdk/adaptive_system.pyi +21 -0
- tnfr/sdk/builders.py +370 -0
- tnfr/sdk/builders.pyi +51 -0
- tnfr/sdk/fluent.py +1121 -0
- tnfr/sdk/fluent.pyi +74 -0
- tnfr/sdk/templates.py +342 -0
- tnfr/sdk/templates.pyi +41 -0
- tnfr/sdk/utils.py +341 -0
- tnfr/secure_config.py +46 -0
- tnfr/security/__init__.py +70 -0
- tnfr/security/database.py +514 -0
- tnfr/security/subprocess.py +503 -0
- tnfr/security/validation.py +290 -0
- tnfr/selector.py +59 -22
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +92 -67
- tnfr/sense.pyi +23 -0
- tnfr/services/__init__.py +17 -0
- tnfr/services/orchestrator.py +325 -0
- tnfr/sparse/__init__.py +39 -0
- tnfr/sparse/representations.py +492 -0
- tnfr/structural.py +639 -263
- tnfr/structural.pyi +83 -0
- tnfr/telemetry/__init__.py +35 -0
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/cache_metrics.pyi +64 -0
- tnfr/telemetry/nu_f.py +422 -0
- tnfr/telemetry/nu_f.pyi +108 -0
- tnfr/telemetry/verbosity.py +36 -0
- tnfr/telemetry/verbosity.pyi +15 -0
- tnfr/tokens.py +2 -4
- tnfr/tokens.pyi +36 -0
- tnfr/tools/__init__.py +20 -0
- tnfr/tools/domain_templates.py +478 -0
- tnfr/tools/sequence_generator.py +846 -0
- tnfr/topology/__init__.py +13 -0
- tnfr/topology/asymmetry.py +151 -0
- tnfr/trace.py +300 -126
- tnfr/trace.pyi +42 -0
- tnfr/tutorials/__init__.py +38 -0
- tnfr/tutorials/autonomous_evolution.py +285 -0
- tnfr/tutorials/interactive.py +1576 -0
- tnfr/tutorials/structural_metabolism.py +238 -0
- tnfr/types.py +743 -12
- tnfr/types.pyi +357 -0
- tnfr/units.py +68 -0
- tnfr/units.pyi +13 -0
- tnfr/utils/__init__.py +282 -0
- tnfr/utils/__init__.pyi +215 -0
- tnfr/utils/cache.py +4223 -0
- tnfr/utils/cache.pyi +470 -0
- tnfr/{callback_utils.py → utils/callbacks.py} +26 -39
- tnfr/utils/callbacks.pyi +49 -0
- tnfr/utils/chunks.py +108 -0
- tnfr/utils/chunks.pyi +22 -0
- tnfr/utils/data.py +428 -0
- tnfr/utils/data.pyi +74 -0
- tnfr/utils/graph.py +85 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +821 -0
- tnfr/utils/init.pyi +80 -0
- tnfr/utils/io.py +559 -0
- tnfr/utils/io.pyi +66 -0
- tnfr/{helpers → utils}/numeric.py +51 -24
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +257 -0
- tnfr/validation/__init__.pyi +85 -0
- tnfr/validation/compatibility.py +460 -0
- tnfr/validation/compatibility.pyi +6 -0
- tnfr/validation/config.py +73 -0
- tnfr/validation/graph.py +139 -0
- tnfr/validation/graph.pyi +18 -0
- tnfr/validation/input_validation.py +755 -0
- tnfr/validation/invariants.py +712 -0
- tnfr/validation/rules.py +253 -0
- tnfr/validation/rules.pyi +44 -0
- tnfr/validation/runtime.py +279 -0
- tnfr/validation/runtime.pyi +28 -0
- tnfr/validation/sequence_validator.py +162 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +32 -0
- tnfr/validation/spectral.py +164 -0
- tnfr/validation/spectral.pyi +42 -0
- tnfr/validation/validator.py +1266 -0
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/visualization/__init__.py +98 -0
- tnfr/visualization/cascade_viz.py +256 -0
- tnfr/visualization/hierarchy.py +284 -0
- tnfr/visualization/sequence_plotter.py +784 -0
- tnfr/viz/__init__.py +60 -0
- tnfr/viz/matplotlib.py +278 -0
- tnfr/viz/matplotlib.pyi +35 -0
- tnfr-8.5.0.dist-info/METADATA +573 -0
- tnfr-8.5.0.dist-info/RECORD +353 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/entry_points.txt +1 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/collections_utils.py +0 -300
- tnfr/config.py +0 -32
- tnfr/grammar.py +0 -344
- tnfr/graph_utils.py +0 -84
- tnfr/helpers/__init__.py +0 -71
- tnfr/import_utils.py +0 -228
- tnfr/json_utils.py +0 -162
- tnfr/logging_utils.py +0 -116
- tnfr/presets.py +0 -60
- tnfr/validators.py +0 -84
- tnfr/value_utils.py +0 -59
- tnfr-4.5.2.dist-info/METADATA +0 -379
- tnfr-4.5.2.dist-info/RECORD +0 -67
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
)
|