fast-clean-architecture 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fast_clean_architecture/__init__.py +24 -0
- fast_clean_architecture/cli.py +480 -0
- fast_clean_architecture/config.py +506 -0
- fast_clean_architecture/exceptions.py +63 -0
- fast_clean_architecture/generators/__init__.py +11 -0
- fast_clean_architecture/generators/component_generator.py +1039 -0
- fast_clean_architecture/generators/config_updater.py +308 -0
- fast_clean_architecture/generators/package_generator.py +174 -0
- fast_clean_architecture/generators/template_validator.py +546 -0
- fast_clean_architecture/generators/validation_config.py +75 -0
- fast_clean_architecture/generators/validation_metrics.py +193 -0
- fast_clean_architecture/templates/__init__.py +7 -0
- fast_clean_architecture/templates/__init__.py.j2 +26 -0
- fast_clean_architecture/templates/api.py.j2 +65 -0
- fast_clean_architecture/templates/command.py.j2 +26 -0
- fast_clean_architecture/templates/entity.py.j2 +49 -0
- fast_clean_architecture/templates/external.py.j2 +61 -0
- fast_clean_architecture/templates/infrastructure_repository.py.j2 +69 -0
- fast_clean_architecture/templates/model.py.j2 +38 -0
- fast_clean_architecture/templates/query.py.j2 +26 -0
- fast_clean_architecture/templates/repository.py.j2 +57 -0
- fast_clean_architecture/templates/schemas.py.j2 +32 -0
- fast_clean_architecture/templates/service.py.j2 +109 -0
- fast_clean_architecture/templates/value_object.py.j2 +34 -0
- fast_clean_architecture/utils.py +553 -0
- fast_clean_architecture-1.0.0.dist-info/METADATA +541 -0
- fast_clean_architecture-1.0.0.dist-info/RECORD +30 -0
- fast_clean_architecture-1.0.0.dist-info/WHEEL +4 -0
- fast_clean_architecture-1.0.0.dist-info/entry_points.txt +2 -0
- fast_clean_architecture-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,546 @@
|
|
1
|
+
"""Template validation module with simplified architecture."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import time
|
5
|
+
from abc import ABC, abstractmethod
|
6
|
+
from typing import Dict, Any, Set, Optional, List, Protocol, TypeVar
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
import jinja2
|
10
|
+
from jinja2 import meta, Environment, TemplateSyntaxError, UndefinedError
|
11
|
+
from jinja2.sandbox import SandboxedEnvironment
|
12
|
+
|
13
|
+
from ..exceptions import (
|
14
|
+
TemplateError,
|
15
|
+
TemplateValidationError,
|
16
|
+
TemplateMissingVariablesError,
|
17
|
+
TemplateUndefinedVariableError,
|
18
|
+
)
|
19
|
+
from .validation_config import ValidationConfig
|
20
|
+
from .validation_metrics import (
|
21
|
+
timed_validation,
|
22
|
+
get_metrics_collector,
|
23
|
+
ValidationMetrics,
|
24
|
+
validation_timeout,
|
25
|
+
)
|
26
|
+
|
27
|
+
logger = logging.getLogger(__name__)
|
28
|
+
|
29
|
+
# Type variables for enhanced type safety
|
30
|
+
T = TypeVar("T", bound="TemplateValidationStrategy")
|
31
|
+
|
32
|
+
|
33
|
+
class ValidatorFactory(Protocol):
|
34
|
+
"""Protocol for validator factory implementations."""
|
35
|
+
|
36
|
+
def create(self, config: ValidationConfig) -> "TemplateValidator":
|
37
|
+
"""Create a validator with the given configuration."""
|
38
|
+
...
|
39
|
+
|
40
|
+
def create_default(self) -> "TemplateValidator":
|
41
|
+
"""Create a validator with default configuration."""
|
42
|
+
...
|
43
|
+
|
44
|
+
|
45
|
+
class TemplateValidationStrategy(ABC):
|
46
|
+
"""Abstract base class for template validation strategies."""
|
47
|
+
|
48
|
+
@abstractmethod
|
49
|
+
def validate(self, template_source: str, template_vars: Dict[str, Any]) -> None:
|
50
|
+
"""Validate template with given variables.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
template_source: The template source code
|
54
|
+
template_vars: Variables to validate against
|
55
|
+
|
56
|
+
Raises:
|
57
|
+
TemplateError: If validation fails
|
58
|
+
"""
|
59
|
+
pass
|
60
|
+
|
61
|
+
|
62
|
+
class SimpleTemplateValidator:
|
63
|
+
"""Simplified template validator with essential security features.
|
64
|
+
|
65
|
+
This replaces the complex validation strategies with a single, secure approach.
|
66
|
+
"""
|
67
|
+
|
68
|
+
def __init__(
|
69
|
+
self, template_env: Environment, config: Optional[ValidationConfig] = None
|
70
|
+
):
|
71
|
+
self.template_env = template_env
|
72
|
+
self.config = config or ValidationConfig()
|
73
|
+
self._metrics_collector = (
|
74
|
+
get_metrics_collector() if config and config.enable_metrics else None
|
75
|
+
)
|
76
|
+
|
77
|
+
def validate(self, template_source: str, template_vars: Dict[str, Any]) -> None:
|
78
|
+
"""Validate template with essential security checks.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
template_source: The template source code
|
82
|
+
template_vars: Variables to validate against
|
83
|
+
|
84
|
+
Raises:
|
85
|
+
TemplateSyntaxError: If template has syntax errors
|
86
|
+
TemplateMissingVariablesError: If required variables are missing
|
87
|
+
TemplateValidationError: For other validation errors
|
88
|
+
"""
|
89
|
+
template_size = len(template_source.encode("utf-8"))
|
90
|
+
|
91
|
+
# Check template size limits (security)
|
92
|
+
if template_size > self.config.max_template_size_bytes:
|
93
|
+
raise TemplateValidationError(
|
94
|
+
f"Template size ({template_size} bytes) exceeds limit "
|
95
|
+
f"({self.config.max_template_size_bytes} bytes)"
|
96
|
+
)
|
97
|
+
|
98
|
+
try:
|
99
|
+
# Parse template to check for syntax errors
|
100
|
+
try:
|
101
|
+
parsed = self.template_env.parse(template_source)
|
102
|
+
except TemplateSyntaxError as e:
|
103
|
+
if self._metrics_collector:
|
104
|
+
self._metrics_collector.record_validation(
|
105
|
+
False, "TemplateSyntaxError"
|
106
|
+
)
|
107
|
+
raise
|
108
|
+
|
109
|
+
# Find undefined variables
|
110
|
+
undefined_vars = meta.find_undeclared_variables(parsed)
|
111
|
+
missing_vars = undefined_vars - set(template_vars.keys())
|
112
|
+
|
113
|
+
# Filter out variables that are optional (have defaults or in conditionals)
|
114
|
+
if missing_vars:
|
115
|
+
optional_vars = self._find_optional_variables(
|
116
|
+
template_source, missing_vars
|
117
|
+
)
|
118
|
+
truly_missing = missing_vars - optional_vars
|
119
|
+
|
120
|
+
if truly_missing:
|
121
|
+
error = TemplateMissingVariablesError(truly_missing)
|
122
|
+
if self._metrics_collector:
|
123
|
+
self._metrics_collector.record_validation(
|
124
|
+
False, "TemplateMissingVariablesError"
|
125
|
+
)
|
126
|
+
raise error
|
127
|
+
|
128
|
+
# Record successful validation
|
129
|
+
if self._metrics_collector:
|
130
|
+
self._metrics_collector.record_validation(True)
|
131
|
+
|
132
|
+
except (
|
133
|
+
TemplateSyntaxError,
|
134
|
+
TemplateValidationError,
|
135
|
+
TemplateMissingVariablesError,
|
136
|
+
):
|
137
|
+
# These exceptions are already handled above or should propagate
|
138
|
+
raise
|
139
|
+
except Exception as e:
|
140
|
+
logger.error(f"Unexpected error during validation: {e}")
|
141
|
+
if self._metrics_collector:
|
142
|
+
self._metrics_collector.record_validation(False, type(e).__name__)
|
143
|
+
raise TemplateValidationError(f"Template validation error: {e}")
|
144
|
+
|
145
|
+
def _find_optional_variables(self, template_source: str, missing_vars: set) -> set:
|
146
|
+
"""Find variables that are optional (have default filters or are in conditionals)."""
|
147
|
+
import re
|
148
|
+
|
149
|
+
optional_vars = set()
|
150
|
+
|
151
|
+
# Simplified and secure patterns to prevent ReDoS attacks
|
152
|
+
# Only match simple variable names with basic default filters
|
153
|
+
default_pattern = re.compile(
|
154
|
+
r"{{\s*([a-zA-Z_][a-zA-Z0-9_]{0,50})\s*\|\s*default\b", re.MULTILINE
|
155
|
+
)
|
156
|
+
# Simple conditional pattern without nested matching
|
157
|
+
conditional_pattern = re.compile(
|
158
|
+
r"{%\s*if\s+([a-zA-Z_][a-zA-Z0-9_]{0,50})(?:\s+is\s+defined)?\s*%}",
|
159
|
+
re.MULTILINE,
|
160
|
+
)
|
161
|
+
|
162
|
+
# Find variables with default filters
|
163
|
+
for match in default_pattern.finditer(template_source):
|
164
|
+
var_name = match.group(1).strip()
|
165
|
+
if var_name in missing_vars:
|
166
|
+
optional_vars.add(var_name)
|
167
|
+
|
168
|
+
# Find variables in conditionals (simplified detection)
|
169
|
+
for match in conditional_pattern.finditer(template_source):
|
170
|
+
var_name = match.group(1).strip()
|
171
|
+
if var_name in missing_vars:
|
172
|
+
optional_vars.add(var_name)
|
173
|
+
|
174
|
+
return optional_vars
|
175
|
+
|
176
|
+
|
177
|
+
class RuntimeValidator(TemplateValidationStrategy):
|
178
|
+
"""Validates templates by attempting to render them.
|
179
|
+
|
180
|
+
Examples:
|
181
|
+
>>> env = Environment()
|
182
|
+
>>> validator = RuntimeValidator(env)
|
183
|
+
>>> validator.validate("Hello {{ name }}!", {"name": "World"})
|
184
|
+
>>> # Raises TemplateUndefinedVariableError for undefined variables
|
185
|
+
>>> validator.validate("Hello {{ missing }}!", {"name": "World"})
|
186
|
+
"""
|
187
|
+
|
188
|
+
def __init__(
|
189
|
+
self, template_env: Environment, config: Optional[ValidationConfig] = None
|
190
|
+
):
|
191
|
+
self.template_env = template_env
|
192
|
+
self.config = config or ValidationConfig()
|
193
|
+
self._metrics_collector = get_metrics_collector()
|
194
|
+
|
195
|
+
def validate(self, template_source: str, template_vars: Dict[str, Any]) -> None:
|
196
|
+
"""Validate by attempting to render the template.
|
197
|
+
|
198
|
+
Args:
|
199
|
+
template_source: The template source code
|
200
|
+
template_vars: Variables to use for rendering
|
201
|
+
|
202
|
+
Raises:
|
203
|
+
TemplateSyntaxError: If template has syntax errors
|
204
|
+
TemplateUndefinedVariableError: If undefined variables are encountered
|
205
|
+
TemplateValidationError: For other validation errors
|
206
|
+
"""
|
207
|
+
template_size = len(template_source.encode("utf-8"))
|
208
|
+
|
209
|
+
with timed_validation(
|
210
|
+
"RuntimeValidator",
|
211
|
+
template_size,
|
212
|
+
len(template_vars),
|
213
|
+
True, # Always enable timing for simplified approach
|
214
|
+
) as metrics:
|
215
|
+
try:
|
216
|
+
logger.debug(
|
217
|
+
f"Starting runtime validation for template of size {template_size} bytes"
|
218
|
+
)
|
219
|
+
|
220
|
+
template = self.template_env.from_string(template_source)
|
221
|
+
|
222
|
+
# Attempt to render with timeout if configured
|
223
|
+
if self.config.render_timeout_seconds > 0:
|
224
|
+
with validation_timeout(self.config.render_timeout_seconds):
|
225
|
+
rendered = template.render(**template_vars)
|
226
|
+
else:
|
227
|
+
rendered = template.render(**template_vars)
|
228
|
+
|
229
|
+
rendered_size = len(rendered.encode("utf-8"))
|
230
|
+
|
231
|
+
logger.debug(
|
232
|
+
f"Runtime validation successful: rendered {rendered_size} bytes"
|
233
|
+
)
|
234
|
+
self._metrics_collector.record_validation(True)
|
235
|
+
|
236
|
+
except TemplateSyntaxError as e:
|
237
|
+
logger.error(f"Template syntax error during runtime validation: {e}")
|
238
|
+
metrics.errors_encountered = 1
|
239
|
+
self._metrics_collector.record_validation(False, "TemplateSyntaxError")
|
240
|
+
# Let TemplateSyntaxError propagate for proper error handling
|
241
|
+
raise
|
242
|
+
except UndefinedError as e:
|
243
|
+
logger.error(f"Undefined variable during runtime validation: {e}")
|
244
|
+
metrics.errors_encountered = 1
|
245
|
+
metrics.undefined_variables_found = 1
|
246
|
+
error = TemplateUndefinedVariableError(str(e))
|
247
|
+
self._metrics_collector.record_validation(
|
248
|
+
False, "TemplateUndefinedVariableError"
|
249
|
+
)
|
250
|
+
raise error
|
251
|
+
except Exception as e:
|
252
|
+
logger.error(f"Unexpected error during runtime validation: {e}")
|
253
|
+
metrics.errors_encountered = 1
|
254
|
+
self._metrics_collector.record_validation(False, type(e).__name__)
|
255
|
+
raise TemplateValidationError(f"Template rendering error: {e}")
|
256
|
+
|
257
|
+
|
258
|
+
class TemplateSourceResolver:
|
259
|
+
"""Resolves template source from various inputs."""
|
260
|
+
|
261
|
+
def __init__(self, template_env: Environment):
|
262
|
+
self.template_env = template_env
|
263
|
+
|
264
|
+
def resolve_source(self, template_input: str) -> str:
|
265
|
+
"""Resolve template source from input (content or filename).
|
266
|
+
|
267
|
+
Args:
|
268
|
+
template_input: Either template content or template filename
|
269
|
+
|
270
|
+
Returns:
|
271
|
+
str: The template source code
|
272
|
+
|
273
|
+
Raises:
|
274
|
+
TemplateError: If template cannot be resolved
|
275
|
+
"""
|
276
|
+
try:
|
277
|
+
# Check if it's template content or template name
|
278
|
+
if self._is_template_content(template_input):
|
279
|
+
return template_input
|
280
|
+
else:
|
281
|
+
# It's a template name, load from file
|
282
|
+
template = self.template_env.get_template(template_input)
|
283
|
+
if template.environment.loader is None:
|
284
|
+
raise TemplateError(
|
285
|
+
f"No loader available for template: {template_input}"
|
286
|
+
)
|
287
|
+
return template.environment.loader.get_source(
|
288
|
+
template.environment, template_input
|
289
|
+
)[0]
|
290
|
+
except jinja2.TemplateNotFound:
|
291
|
+
raise TemplateError(f"Template not found: {template_input}")
|
292
|
+
except Exception as e:
|
293
|
+
if isinstance(e, TemplateError):
|
294
|
+
raise
|
295
|
+
raise TemplateError(f"Error resolving template source: {e}")
|
296
|
+
|
297
|
+
def _is_template_content(self, template_input: str) -> bool:
|
298
|
+
"""Check if input is template content or filename."""
|
299
|
+
return (
|
300
|
+
"{{" in template_input
|
301
|
+
or "{%" in template_input
|
302
|
+
or not template_input.endswith(".j2")
|
303
|
+
)
|
304
|
+
|
305
|
+
|
306
|
+
class TemplateValidator:
|
307
|
+
"""Main template validator that orchestrates different validation strategies.
|
308
|
+
|
309
|
+
This class provides a unified interface for template validation using
|
310
|
+
configurable strategies and comprehensive error handling.
|
311
|
+
|
312
|
+
Examples:
|
313
|
+
>>> env = Environment()
|
314
|
+
>>> config = ValidationConfig(sandbox_mode=True)
|
315
|
+
>>> validator = TemplateValidator(env, config=config)
|
316
|
+
>>> validator.validate("Hello {{ name }}!", {"name": "World"})
|
317
|
+
|
318
|
+
>>> # With custom validators
|
319
|
+
>>> static = SimpleTemplateValidator(env)
|
320
|
+
>>> runtime = RuntimeValidator(env)
|
321
|
+
>>> validator = TemplateValidator(env, static_validator=static, runtime_validator=runtime)
|
322
|
+
"""
|
323
|
+
|
324
|
+
def __init__(
|
325
|
+
self,
|
326
|
+
template_env: Environment,
|
327
|
+
config: Optional[ValidationConfig] = None,
|
328
|
+
static_validator: Optional[TemplateValidationStrategy] = None,
|
329
|
+
runtime_validator: Optional[TemplateValidationStrategy] = None,
|
330
|
+
fallback_strategy: Optional[RuntimeValidator] = None,
|
331
|
+
):
|
332
|
+
"""Initialize the template validator.
|
333
|
+
|
334
|
+
Args:
|
335
|
+
template_env: Jinja2 environment for template processing
|
336
|
+
config: Validation configuration settings
|
337
|
+
static_validator: Custom static analysis validator
|
338
|
+
runtime_validator: Custom runtime validator
|
339
|
+
fallback_strategy: Fallback validator for error recovery
|
340
|
+
"""
|
341
|
+
self.template_env = template_env
|
342
|
+
self.config = config or ValidationConfig()
|
343
|
+
self._metrics_collector = get_metrics_collector()
|
344
|
+
|
345
|
+
# Initialize validators with configuration
|
346
|
+
self.static_validator = static_validator or SimpleTemplateValidator(
|
347
|
+
template_env, self.config
|
348
|
+
)
|
349
|
+
self.runtime_validator = runtime_validator or RuntimeValidator(
|
350
|
+
template_env, self.config
|
351
|
+
)
|
352
|
+
self.fallback_strategy = fallback_strategy
|
353
|
+
|
354
|
+
# Logging is configured at application level
|
355
|
+
|
356
|
+
def validate(
|
357
|
+
self, template_source: str, template_vars: Dict[str, Any]
|
358
|
+
) -> ValidationMetrics:
|
359
|
+
"""Validate template using configured strategies.
|
360
|
+
|
361
|
+
Args:
|
362
|
+
template_source: The template source code to validate
|
363
|
+
template_vars: Variables to validate against
|
364
|
+
|
365
|
+
Returns:
|
366
|
+
ValidationMetrics: Metrics from the validation process
|
367
|
+
|
368
|
+
Raises:
|
369
|
+
TemplateSyntaxError: If template has syntax errors
|
370
|
+
TemplateMissingVariablesError: If required variables are missing
|
371
|
+
TemplateUndefinedVariableError: If undefined variables are encountered
|
372
|
+
TemplateValidationError: For other validation errors
|
373
|
+
"""
|
374
|
+
validation_start = time.time()
|
375
|
+
template_size = len(template_source.encode("utf-8"))
|
376
|
+
|
377
|
+
logger.info("Starting template validation")
|
378
|
+
|
379
|
+
try:
|
380
|
+
# Use simplified validation approach
|
381
|
+
self.static_validator.validate(template_source, template_vars)
|
382
|
+
|
383
|
+
# Create success metrics
|
384
|
+
validation_time = time.time() - validation_start
|
385
|
+
metrics = ValidationMetrics(
|
386
|
+
strategy_used="simple",
|
387
|
+
validation_time_ms=validation_time * 1000,
|
388
|
+
template_size_bytes=template_size,
|
389
|
+
variables_count=len(template_vars),
|
390
|
+
undefined_variables_found=0,
|
391
|
+
fallback_used=False,
|
392
|
+
)
|
393
|
+
|
394
|
+
logger.info(f"Template validation successful in {validation_time:.3f}s")
|
395
|
+
if self._metrics_collector:
|
396
|
+
self._metrics_collector.record_validation(True)
|
397
|
+
return metrics
|
398
|
+
|
399
|
+
except (
|
400
|
+
TemplateSyntaxError,
|
401
|
+
TemplateValidationError,
|
402
|
+
TemplateMissingVariablesError,
|
403
|
+
TemplateUndefinedVariableError,
|
404
|
+
) as e:
|
405
|
+
validation_time = time.time() - validation_start
|
406
|
+
metrics = ValidationMetrics(
|
407
|
+
strategy_used="simple",
|
408
|
+
validation_time_ms=validation_time * 1000,
|
409
|
+
template_size_bytes=template_size,
|
410
|
+
variables_count=len(template_vars),
|
411
|
+
undefined_variables_found=0,
|
412
|
+
fallback_used=False,
|
413
|
+
errors_encountered=1,
|
414
|
+
)
|
415
|
+
|
416
|
+
logger.error(f"Template validation failed in {validation_time:.3f}s: {e}")
|
417
|
+
if self._metrics_collector:
|
418
|
+
self._metrics_collector.record_validation(False, type(e).__name__)
|
419
|
+
raise
|
420
|
+
|
421
|
+
def get_metrics(self) -> Dict[str, Any]:
|
422
|
+
"""Get validation metrics from the metrics collector.
|
423
|
+
|
424
|
+
Returns:
|
425
|
+
Dict containing validation statistics and metrics
|
426
|
+
"""
|
427
|
+
return self._metrics_collector.get_stats().to_dict()
|
428
|
+
|
429
|
+
def reset_metrics(self) -> None:
|
430
|
+
"""Reset validation metrics."""
|
431
|
+
self._metrics_collector.reset()
|
432
|
+
|
433
|
+
|
434
|
+
class TemplateValidatorFactory:
|
435
|
+
"""Factory for creating template validators with different configurations.
|
436
|
+
|
437
|
+
This factory implements the ValidatorFactory protocol and provides
|
438
|
+
convenient methods for creating validators with common configurations.
|
439
|
+
|
440
|
+
Examples:
|
441
|
+
>>> env = Environment()
|
442
|
+
>>> factory = TemplateValidatorFactory()
|
443
|
+
>>>
|
444
|
+
>>> # Create with default configuration
|
445
|
+
>>> validator = factory.create_default(env)
|
446
|
+
>>>
|
447
|
+
>>> # Create with custom configuration
|
448
|
+
>>> config = ValidationConfig(sandbox_mode=True)
|
449
|
+
>>> validator = factory.create(env, config)
|
450
|
+
>>>
|
451
|
+
>>> # Create with predefined configurations
|
452
|
+
>>> static_validator = factory.create_static_only(env)
|
453
|
+
>>> runtime_validator = factory.create_runtime_only(env)
|
454
|
+
"""
|
455
|
+
|
456
|
+
def create(
|
457
|
+
self, template_env: Environment, config: ValidationConfig
|
458
|
+
) -> TemplateValidator:
|
459
|
+
"""Create a validator with the given configuration.
|
460
|
+
|
461
|
+
Args:
|
462
|
+
template_env: Jinja2 environment for template processing
|
463
|
+
config: Validation configuration settings
|
464
|
+
|
465
|
+
Returns:
|
466
|
+
TemplateValidator: Configured validator instance
|
467
|
+
"""
|
468
|
+
return TemplateValidator(template_env=template_env, config=config)
|
469
|
+
|
470
|
+
def create_default(self, template_env: Environment) -> TemplateValidator:
|
471
|
+
"""Create a validator with default configuration.
|
472
|
+
|
473
|
+
Args:
|
474
|
+
template_env: Jinja2 environment for template processing
|
475
|
+
|
476
|
+
Returns:
|
477
|
+
TemplateValidator: Validator with default settings
|
478
|
+
"""
|
479
|
+
return TemplateValidator(template_env=template_env)
|
480
|
+
|
481
|
+
@staticmethod
|
482
|
+
def create_static_only(
|
483
|
+
template_env: Environment, strict_mode: bool = False
|
484
|
+
) -> TemplateValidator:
|
485
|
+
"""Create a validator that only uses static analysis.
|
486
|
+
|
487
|
+
Args:
|
488
|
+
template_env: Jinja2 environment for template processing
|
489
|
+
strict_mode: Whether to enable strict validation mode
|
490
|
+
|
491
|
+
Returns:
|
492
|
+
TemplateValidator: Static-only validator
|
493
|
+
"""
|
494
|
+
config = ValidationConfig(sandbox_mode=True)
|
495
|
+
return TemplateValidator(template_env=template_env, config=config)
|
496
|
+
|
497
|
+
@staticmethod
|
498
|
+
def create_runtime_only(
|
499
|
+
template_env: Environment, timeout_seconds: int = 30
|
500
|
+
) -> TemplateValidator:
|
501
|
+
"""Create a validator that only uses runtime validation.
|
502
|
+
|
503
|
+
Args:
|
504
|
+
template_env: Jinja2 environment for template processing
|
505
|
+
timeout_seconds: Timeout for template rendering
|
506
|
+
|
507
|
+
Returns:
|
508
|
+
TemplateValidator: Runtime-only validator
|
509
|
+
"""
|
510
|
+
config = ValidationConfig(
|
511
|
+
sandbox_mode=True, render_timeout_seconds=timeout_seconds
|
512
|
+
)
|
513
|
+
return TemplateValidator(template_env=template_env, config=config)
|
514
|
+
|
515
|
+
@staticmethod
|
516
|
+
def create_with_fallback(
|
517
|
+
template_env: Environment, enable_metrics: bool = True
|
518
|
+
) -> TemplateValidator:
|
519
|
+
"""Create a validator with static analysis and runtime fallback.
|
520
|
+
|
521
|
+
Args:
|
522
|
+
template_env: Jinja2 environment for template processing
|
523
|
+
enable_metrics: Whether to enable performance metrics collection
|
524
|
+
|
525
|
+
Returns:
|
526
|
+
TemplateValidator: Validator with fallback strategy
|
527
|
+
"""
|
528
|
+
config = ValidationConfig(sandbox_mode=True)
|
529
|
+
return TemplateValidator(
|
530
|
+
template_env=template_env,
|
531
|
+
config=config,
|
532
|
+
fallback_strategy=RuntimeValidator(template_env, config),
|
533
|
+
)
|
534
|
+
|
535
|
+
@staticmethod
|
536
|
+
def create_comprehensive(template_env: Environment) -> TemplateValidator:
|
537
|
+
"""Create a validator that runs both static and runtime validation.
|
538
|
+
|
539
|
+
Args:
|
540
|
+
template_env: Jinja2 environment for template processing
|
541
|
+
|
542
|
+
Returns:
|
543
|
+
TemplateValidator: Comprehensive validator
|
544
|
+
"""
|
545
|
+
config = ValidationConfig(sandbox_mode=True)
|
546
|
+
return TemplateValidator(template_env=template_env, config=config)
|
@@ -0,0 +1,75 @@
|
|
1
|
+
"""Configuration classes for template validation."""
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Optional, Dict, Any
|
5
|
+
from enum import Enum
|
6
|
+
|
7
|
+
|
8
|
+
class ValidationStrategy(Enum):
|
9
|
+
"""Available validation strategies."""
|
10
|
+
|
11
|
+
STATIC_ONLY = "static_only"
|
12
|
+
RUNTIME_ONLY = "runtime_only"
|
13
|
+
BOTH = "both"
|
14
|
+
STATIC_WITH_RUNTIME_FALLBACK = "static_with_runtime_fallback"
|
15
|
+
|
16
|
+
|
17
|
+
class LogLevel(Enum):
|
18
|
+
"""Logging levels for validation."""
|
19
|
+
|
20
|
+
DEBUG = "debug"
|
21
|
+
INFO = "info"
|
22
|
+
WARNING = "warning"
|
23
|
+
ERROR = "error"
|
24
|
+
|
25
|
+
|
26
|
+
@dataclass
|
27
|
+
class ValidationConfig:
|
28
|
+
"""Simplified configuration for template validation with essential security."""
|
29
|
+
|
30
|
+
# Core validation settings (simplified)
|
31
|
+
strict_mode: bool = False
|
32
|
+
allow_undefined: bool = True
|
33
|
+
|
34
|
+
# Security settings (essential only)
|
35
|
+
sandbox_mode: bool = True
|
36
|
+
max_template_size_bytes: int = 64 * 1024 # 64KB
|
37
|
+
render_timeout_seconds: int = 10
|
38
|
+
max_variable_nesting_depth: int = 10 # Prevent deep recursion DoS
|
39
|
+
|
40
|
+
# Basic monitoring
|
41
|
+
enable_metrics: bool = False # Disabled by default to reduce complexity
|
42
|
+
log_level: str = "WARNING" # Reduced logging
|
43
|
+
|
44
|
+
def __post_init__(self):
|
45
|
+
"""Validate configuration after initialization."""
|
46
|
+
if self.max_variable_nesting_depth < 1:
|
47
|
+
raise ValueError("max_variable_nesting_depth must be at least 1")
|
48
|
+
|
49
|
+
if self.max_template_size_bytes <= 0:
|
50
|
+
raise ValueError("max_template_size_bytes must be positive")
|
51
|
+
|
52
|
+
|
53
|
+
@dataclass
|
54
|
+
class ValidationMetrics:
|
55
|
+
"""Metrics collected during template validation."""
|
56
|
+
|
57
|
+
strategy_used: str
|
58
|
+
validation_time_ms: float
|
59
|
+
template_size_bytes: int
|
60
|
+
variables_count: int
|
61
|
+
undefined_variables_found: int
|
62
|
+
fallback_used: bool = False
|
63
|
+
errors_encountered: int = 0
|
64
|
+
|
65
|
+
def to_dict(self) -> Dict[str, Any]:
|
66
|
+
"""Convert metrics to dictionary for logging."""
|
67
|
+
return {
|
68
|
+
"strategy_used": self.strategy_used,
|
69
|
+
"validation_time_ms": round(self.validation_time_ms, 3),
|
70
|
+
"template_size_bytes": self.template_size_bytes,
|
71
|
+
"variables_count": self.variables_count,
|
72
|
+
"undefined_variables_found": self.undefined_variables_found,
|
73
|
+
"fallback_used": self.fallback_used,
|
74
|
+
"errors_encountered": self.errors_encountered,
|
75
|
+
}
|