quantum-framework 0.9.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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Query Validators - Type validation and conversion for query parameters
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Tuple, Optional
|
|
6
|
+
from datetime import datetime, date, time as datetime_time
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class QueryValidationError(Exception):
|
|
11
|
+
"""Raised when query parameter validation fails"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QueryValidator:
|
|
16
|
+
"""Validates and converts query parameters"""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def validate_param(value: Any, param_type: str, attributes: dict) -> Any:
|
|
20
|
+
"""
|
|
21
|
+
Validate and convert parameter value
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
value: Parameter value
|
|
25
|
+
param_type: Parameter type (string, integer, decimal, boolean, datetime, etc.)
|
|
26
|
+
attributes: Additional constraints (maxLength, scale, null)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Converted and validated value
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
QueryValidationError: If validation fails
|
|
33
|
+
"""
|
|
34
|
+
# Check null
|
|
35
|
+
null_allowed = attributes.get('null', False)
|
|
36
|
+
if value is None:
|
|
37
|
+
if not null_allowed:
|
|
38
|
+
raise QueryValidationError("Parameter cannot be null")
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
# Convert based on type
|
|
42
|
+
try:
|
|
43
|
+
if param_type == 'string':
|
|
44
|
+
return QueryValidator._validate_string(value, attributes)
|
|
45
|
+
elif param_type == 'integer':
|
|
46
|
+
return QueryValidator._validate_integer(value, attributes)
|
|
47
|
+
elif param_type == 'decimal':
|
|
48
|
+
return QueryValidator._validate_decimal(value, attributes)
|
|
49
|
+
elif param_type == 'boolean':
|
|
50
|
+
return QueryValidator._validate_boolean(value, attributes)
|
|
51
|
+
elif param_type == 'datetime':
|
|
52
|
+
return QueryValidator._validate_datetime(value, attributes)
|
|
53
|
+
elif param_type == 'date':
|
|
54
|
+
return QueryValidator._validate_date(value, attributes)
|
|
55
|
+
elif param_type == 'time':
|
|
56
|
+
return QueryValidator._validate_time(value, attributes)
|
|
57
|
+
elif param_type == 'array':
|
|
58
|
+
return QueryValidator._validate_array(value, attributes)
|
|
59
|
+
elif param_type == 'json':
|
|
60
|
+
return QueryValidator._validate_json(value, attributes)
|
|
61
|
+
else:
|
|
62
|
+
raise QueryValidationError(f"Unknown parameter type: {param_type}")
|
|
63
|
+
|
|
64
|
+
except QueryValidationError:
|
|
65
|
+
raise
|
|
66
|
+
except Exception as e:
|
|
67
|
+
raise QueryValidationError(f"Validation error: {e}")
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _validate_string(value: Any, attributes: dict) -> str:
|
|
71
|
+
"""Validate and convert to string"""
|
|
72
|
+
str_value = str(value)
|
|
73
|
+
|
|
74
|
+
# Check maxLength
|
|
75
|
+
max_length = attributes.get('max_length')
|
|
76
|
+
if max_length and len(str_value) > max_length:
|
|
77
|
+
raise QueryValidationError(
|
|
78
|
+
f"String length {len(str_value)} exceeds maximum {max_length}"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return str_value
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _validate_integer(value: Any, attributes: dict) -> int:
|
|
85
|
+
"""Validate and convert to integer"""
|
|
86
|
+
if isinstance(value, int):
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
if isinstance(value, str):
|
|
90
|
+
# Remove whitespace
|
|
91
|
+
value = value.strip()
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
return int(value)
|
|
95
|
+
except (ValueError, TypeError):
|
|
96
|
+
raise QueryValidationError(f"Cannot convert '{value}' to integer")
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _validate_decimal(value: Any, attributes: dict) -> float:
|
|
100
|
+
"""Validate and convert to decimal (float)"""
|
|
101
|
+
if isinstance(value, (int, float)):
|
|
102
|
+
float_value = float(value)
|
|
103
|
+
elif isinstance(value, str):
|
|
104
|
+
try:
|
|
105
|
+
float_value = float(value.strip())
|
|
106
|
+
except ValueError:
|
|
107
|
+
raise QueryValidationError(f"Cannot convert '{value}' to decimal")
|
|
108
|
+
else:
|
|
109
|
+
raise QueryValidationError(f"Cannot convert '{value}' to decimal")
|
|
110
|
+
|
|
111
|
+
# Apply scale if specified
|
|
112
|
+
scale = attributes.get('scale')
|
|
113
|
+
if scale is not None:
|
|
114
|
+
float_value = round(float_value, scale)
|
|
115
|
+
|
|
116
|
+
return float_value
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _validate_boolean(value: Any, attributes: dict) -> bool:
|
|
120
|
+
"""Validate and convert to boolean"""
|
|
121
|
+
if isinstance(value, bool):
|
|
122
|
+
return value
|
|
123
|
+
|
|
124
|
+
if isinstance(value, str):
|
|
125
|
+
value_lower = value.strip().lower()
|
|
126
|
+
if value_lower in ['true', '1', 'yes', 'on']:
|
|
127
|
+
return True
|
|
128
|
+
elif value_lower in ['false', '0', 'no', 'off']:
|
|
129
|
+
return False
|
|
130
|
+
else:
|
|
131
|
+
raise QueryValidationError(f"Cannot convert '{value}' to boolean")
|
|
132
|
+
|
|
133
|
+
if isinstance(value, (int, float)):
|
|
134
|
+
return bool(value)
|
|
135
|
+
|
|
136
|
+
raise QueryValidationError(f"Cannot convert '{value}' to boolean")
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _validate_datetime(value: Any, attributes: dict) -> datetime:
|
|
140
|
+
"""Validate and convert to datetime"""
|
|
141
|
+
if isinstance(value, datetime):
|
|
142
|
+
return value
|
|
143
|
+
|
|
144
|
+
if isinstance(value, str):
|
|
145
|
+
# Try common datetime formats
|
|
146
|
+
formats = [
|
|
147
|
+
'%Y-%m-%d %H:%M:%S',
|
|
148
|
+
'%Y-%m-%d %H:%M:%S.%f',
|
|
149
|
+
'%Y-%m-%dT%H:%M:%S',
|
|
150
|
+
'%Y-%m-%dT%H:%M:%S.%f',
|
|
151
|
+
'%Y-%m-%dT%H:%M:%SZ',
|
|
152
|
+
'%Y-%m-%dT%H:%M:%S.%fZ',
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
for fmt in formats:
|
|
156
|
+
try:
|
|
157
|
+
return datetime.strptime(value.strip(), fmt)
|
|
158
|
+
except ValueError:
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
raise QueryValidationError(
|
|
162
|
+
f"Cannot parse datetime '{value}'. Expected format: YYYY-MM-DD HH:MM:SS"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
raise QueryValidationError(f"Cannot convert '{value}' to datetime")
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _validate_date(value: Any, attributes: dict) -> date:
|
|
169
|
+
"""Validate and convert to date"""
|
|
170
|
+
if isinstance(value, date):
|
|
171
|
+
return value
|
|
172
|
+
|
|
173
|
+
if isinstance(value, datetime):
|
|
174
|
+
return value.date()
|
|
175
|
+
|
|
176
|
+
if isinstance(value, str):
|
|
177
|
+
# Try common date formats
|
|
178
|
+
formats = ['%Y-%m-%d', '%Y/%m/%d', '%d-%m-%Y', '%d/%m/%Y']
|
|
179
|
+
|
|
180
|
+
for fmt in formats:
|
|
181
|
+
try:
|
|
182
|
+
return datetime.strptime(value.strip(), fmt).date()
|
|
183
|
+
except ValueError:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
raise QueryValidationError(
|
|
187
|
+
f"Cannot parse date '{value}'. Expected format: YYYY-MM-DD"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
raise QueryValidationError(f"Cannot convert '{value}' to date")
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _validate_time(value: Any, attributes: dict) -> datetime_time:
|
|
194
|
+
"""Validate and convert to time"""
|
|
195
|
+
if isinstance(value, datetime_time):
|
|
196
|
+
return value
|
|
197
|
+
|
|
198
|
+
if isinstance(value, datetime):
|
|
199
|
+
return value.time()
|
|
200
|
+
|
|
201
|
+
if isinstance(value, str):
|
|
202
|
+
# Try common time formats
|
|
203
|
+
formats = ['%H:%M:%S', '%H:%M:%S.%f', '%H:%M']
|
|
204
|
+
|
|
205
|
+
for fmt in formats:
|
|
206
|
+
try:
|
|
207
|
+
return datetime.strptime(value.strip(), fmt).time()
|
|
208
|
+
except ValueError:
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
raise QueryValidationError(
|
|
212
|
+
f"Cannot parse time '{value}'. Expected format: HH:MM:SS"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
raise QueryValidationError(f"Cannot convert '{value}' to time")
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _validate_array(value: Any, attributes: dict) -> list:
|
|
219
|
+
"""Validate and convert to array"""
|
|
220
|
+
if isinstance(value, list):
|
|
221
|
+
return value
|
|
222
|
+
|
|
223
|
+
if isinstance(value, tuple):
|
|
224
|
+
return list(value)
|
|
225
|
+
|
|
226
|
+
if isinstance(value, str):
|
|
227
|
+
# Try to parse as JSON
|
|
228
|
+
try:
|
|
229
|
+
parsed = json.loads(value)
|
|
230
|
+
if isinstance(parsed, list):
|
|
231
|
+
return parsed
|
|
232
|
+
else:
|
|
233
|
+
raise QueryValidationError("JSON value is not an array")
|
|
234
|
+
except json.JSONDecodeError:
|
|
235
|
+
# Try comma-separated values
|
|
236
|
+
return [item.strip() for item in value.split(',')]
|
|
237
|
+
|
|
238
|
+
raise QueryValidationError(f"Cannot convert '{value}' to array")
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def _validate_json(value: Any, attributes: dict) -> dict:
|
|
242
|
+
"""Validate and convert to JSON object"""
|
|
243
|
+
if isinstance(value, dict):
|
|
244
|
+
return value
|
|
245
|
+
|
|
246
|
+
if isinstance(value, str):
|
|
247
|
+
try:
|
|
248
|
+
return json.loads(value)
|
|
249
|
+
except json.JSONDecodeError as e:
|
|
250
|
+
raise QueryValidationError(f"Invalid JSON: {e}")
|
|
251
|
+
|
|
252
|
+
raise QueryValidationError(f"Cannot convert '{value}' to JSON")
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def sanitize_sql(sql: str) -> str:
|
|
256
|
+
"""
|
|
257
|
+
Basic SQL sanitization to prevent comment injection and other attacks
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
sql: SQL query string
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Sanitized SQL
|
|
264
|
+
|
|
265
|
+
Note:
|
|
266
|
+
This is a basic check. Parameter binding is the primary defense against SQL injection.
|
|
267
|
+
"""
|
|
268
|
+
import re
|
|
269
|
+
|
|
270
|
+
# A regex denylist over SQL text used to live here, matching
|
|
271
|
+
# ';.*DROP TABLE', ';.*DELETE FROM', ';.*INSERT INTO' and
|
|
272
|
+
# 'UNION.*SELECT'. It was removed because it was worse than nothing:
|
|
273
|
+
#
|
|
274
|
+
# BLOCKED "SELECT name FROM a UNION SELECT email FROM b" (legitimate)
|
|
275
|
+
# ALLOWED "SELECT 1 ... UNION\nSELECT password FROM users --"
|
|
276
|
+
# ALLOWED "SELECT 1;\nDROP TABLE users"
|
|
277
|
+
#
|
|
278
|
+
# `.` does not match a newline, so one line break walked past all four
|
|
279
|
+
# patterns — while a perfectly ordinary UNION query was rejected with
|
|
280
|
+
# "SQL contains suspicious pattern". It broke real queries and stopped
|
|
281
|
+
# nothing, which is the shape of defence SECURITY.md already argues
|
|
282
|
+
# against.
|
|
283
|
+
#
|
|
284
|
+
# It was also guarding a threat this architecture prevents by
|
|
285
|
+
# construction, verified by execution: node.sql is NEVER databound
|
|
286
|
+
# (the executor passes it through untouched), q:param values are bound
|
|
287
|
+
# by the driver, and page/page_size are ints read from the template at
|
|
288
|
+
# parse time. A '{form.name}' written into the SQL body reaches the
|
|
289
|
+
# database as literal text and matches nothing.
|
|
290
|
+
#
|
|
291
|
+
# Which is the real problem worth catching, and what this now does.
|
|
292
|
+
# That silent empty result is the mistake people actually make, and it
|
|
293
|
+
# used to give them no signal at all.
|
|
294
|
+
placeholder = re.search(r'\{[^}\n]+\}', sql)
|
|
295
|
+
if placeholder:
|
|
296
|
+
raise QueryValidationError(
|
|
297
|
+
f"SQL contains the databinding placeholder {placeholder.group(0)}. "
|
|
298
|
+
f"Values are not interpolated into SQL — that is what makes "
|
|
299
|
+
f"injection impossible here. Use a bound parameter instead:\n"
|
|
300
|
+
f" WHERE col = :name\n"
|
|
301
|
+
f" <q:param name=\"name\" value=\"{placeholder.group(0)}\" />"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return sql
|