pydpm_xl 0.1.10__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.
- py_dpm/AST/ASTConstructor.py +503 -0
- py_dpm/AST/ASTObjects.py +827 -0
- py_dpm/AST/ASTTemplate.py +101 -0
- py_dpm/AST/ASTVisitor.py +13 -0
- py_dpm/AST/MLGeneration.py +588 -0
- py_dpm/AST/ModuleAnalyzer.py +79 -0
- py_dpm/AST/ModuleDependencies.py +203 -0
- py_dpm/AST/WhereClauseChecker.py +12 -0
- py_dpm/AST/__init__.py +0 -0
- py_dpm/AST/check_operands.py +302 -0
- py_dpm/DataTypes/ScalarTypes.py +324 -0
- py_dpm/DataTypes/TimeClasses.py +370 -0
- py_dpm/DataTypes/TypePromotion.py +195 -0
- py_dpm/DataTypes/__init__.py +0 -0
- py_dpm/Exceptions/__init__.py +0 -0
- py_dpm/Exceptions/exceptions.py +84 -0
- py_dpm/Exceptions/messages.py +114 -0
- py_dpm/OperationScopes/OperationScopeService.py +247 -0
- py_dpm/OperationScopes/__init__.py +0 -0
- py_dpm/Operators/AggregateOperators.py +138 -0
- py_dpm/Operators/BooleanOperators.py +30 -0
- py_dpm/Operators/ClauseOperators.py +159 -0
- py_dpm/Operators/ComparisonOperators.py +69 -0
- py_dpm/Operators/ConditionalOperators.py +362 -0
- py_dpm/Operators/NumericOperators.py +101 -0
- py_dpm/Operators/Operator.py +388 -0
- py_dpm/Operators/StringOperators.py +27 -0
- py_dpm/Operators/TimeOperators.py +53 -0
- py_dpm/Operators/__init__.py +0 -0
- py_dpm/Utils/ValidationsGenerationUtils.py +429 -0
- py_dpm/Utils/__init__.py +0 -0
- py_dpm/Utils/operands_mapping.py +73 -0
- py_dpm/Utils/operator_mapping.py +89 -0
- py_dpm/Utils/tokens.py +172 -0
- py_dpm/Utils/utils.py +2 -0
- py_dpm/ValidationsGeneration/PropertiesConstraintsProcessor.py +190 -0
- py_dpm/ValidationsGeneration/Utils.py +364 -0
- py_dpm/ValidationsGeneration/VariantsProcessor.py +265 -0
- py_dpm/ValidationsGeneration/__init__.py +0 -0
- py_dpm/ValidationsGeneration/auxiliary_functions.py +98 -0
- py_dpm/__init__.py +61 -0
- py_dpm/api/__init__.py +140 -0
- py_dpm/api/ast_generator.py +438 -0
- py_dpm/api/complete_ast.py +241 -0
- py_dpm/api/data_dictionary_validation.py +577 -0
- py_dpm/api/migration.py +77 -0
- py_dpm/api/semantic.py +224 -0
- py_dpm/api/syntax.py +182 -0
- py_dpm/client.py +106 -0
- py_dpm/data_handlers.py +99 -0
- py_dpm/db_utils.py +117 -0
- py_dpm/grammar/__init__.py +0 -0
- py_dpm/grammar/dist/__init__.py +0 -0
- py_dpm/grammar/dist/dpm_xlLexer.interp +428 -0
- py_dpm/grammar/dist/dpm_xlLexer.py +804 -0
- py_dpm/grammar/dist/dpm_xlLexer.tokens +106 -0
- py_dpm/grammar/dist/dpm_xlParser.interp +249 -0
- py_dpm/grammar/dist/dpm_xlParser.py +5224 -0
- py_dpm/grammar/dist/dpm_xlParser.tokens +106 -0
- py_dpm/grammar/dist/dpm_xlParserListener.py +742 -0
- py_dpm/grammar/dist/dpm_xlParserVisitor.py +419 -0
- py_dpm/grammar/dist/listeners.py +10 -0
- py_dpm/grammar/dpm_xlLexer.g4 +435 -0
- py_dpm/grammar/dpm_xlParser.g4 +260 -0
- py_dpm/migration.py +282 -0
- py_dpm/models.py +2139 -0
- py_dpm/semantics/DAG/DAGAnalyzer.py +158 -0
- py_dpm/semantics/DAG/__init__.py +0 -0
- py_dpm/semantics/SemanticAnalyzer.py +320 -0
- py_dpm/semantics/Symbols.py +223 -0
- py_dpm/semantics/__init__.py +0 -0
- py_dpm/utils/__init__.py +0 -0
- py_dpm/utils/ast_serialization.py +481 -0
- py_dpm/views/data_types.sql +12 -0
- py_dpm/views/datapoints.sql +65 -0
- py_dpm/views/hierarchy_operand_reference.sql +11 -0
- py_dpm/views/hierarchy_preconditions.sql +13 -0
- py_dpm/views/hierarchy_variables.sql +26 -0
- py_dpm/views/hierarchy_variables_context.sql +14 -0
- py_dpm/views/key_components.sql +18 -0
- py_dpm/views/module_from_table.sql +11 -0
- py_dpm/views/open_keys.sql +13 -0
- py_dpm/views/operation_info.sql +27 -0
- py_dpm/views/operation_list.sql +18 -0
- py_dpm/views/operations_versions_from_module_version.sql +30 -0
- py_dpm/views/precondition_info.sql +17 -0
- py_dpm/views/report_type_operand_reference_info.sql +18 -0
- py_dpm/views/subcategory_info.sql +17 -0
- py_dpm/views/table_info.sql +19 -0
- pydpm_xl-0.1.10.dist-info/LICENSE +674 -0
- pydpm_xl-0.1.10.dist-info/METADATA +50 -0
- pydpm_xl-0.1.10.dist-info/RECORD +94 -0
- pydpm_xl-0.1.10.dist-info/WHEEL +4 -0
- pydpm_xl-0.1.10.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
AST Generator API - Simplified interface for external packages
|
|
4
|
+
|
|
5
|
+
This module provides a clean, abstracted interface for generating ASTs from DPM-XL expressions
|
|
6
|
+
without exposing internal complexity or version compatibility issues.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, Any, Optional, List, Union
|
|
10
|
+
import json
|
|
11
|
+
from py_dpm.api.syntax import SyntaxAPI
|
|
12
|
+
from py_dpm.api.semantic import SemanticAPI
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ASTGenerator:
|
|
16
|
+
"""
|
|
17
|
+
Simplified AST Generator for external packages.
|
|
18
|
+
|
|
19
|
+
Handles all internal complexity including:
|
|
20
|
+
- Version compatibility
|
|
21
|
+
- Context processing
|
|
22
|
+
- Database integration
|
|
23
|
+
- Error handling
|
|
24
|
+
- JSON serialization
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, database_path: Optional[str] = None,
|
|
28
|
+
compatibility_mode: str = "auto",
|
|
29
|
+
enable_semantic_validation: bool = False):
|
|
30
|
+
"""
|
|
31
|
+
Initialize AST Generator.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
database_path: Optional path to data dictionary database
|
|
35
|
+
compatibility_mode: "auto", "3.1.0", "4.0.0", or "current"
|
|
36
|
+
enable_semantic_validation: Enable semantic validation (requires database)
|
|
37
|
+
"""
|
|
38
|
+
self.syntax_api = SyntaxAPI()
|
|
39
|
+
self.semantic_api = SemanticAPI() if enable_semantic_validation else None
|
|
40
|
+
self.database_path = database_path
|
|
41
|
+
self.compatibility_mode = compatibility_mode
|
|
42
|
+
self.enable_semantic = enable_semantic_validation
|
|
43
|
+
|
|
44
|
+
# Internal version handling
|
|
45
|
+
self._version_normalizers = self._setup_version_normalizers()
|
|
46
|
+
|
|
47
|
+
def parse_expression(self, expression: str) -> Dict[str, Any]:
|
|
48
|
+
"""
|
|
49
|
+
Parse DPM-XL expression into clean AST format.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
expression: DPM-XL expression string
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dictionary containing:
|
|
56
|
+
- success: bool
|
|
57
|
+
- ast: AST dictionary (if successful)
|
|
58
|
+
- context: Context information (if WITH clause present)
|
|
59
|
+
- error: Error message (if failed)
|
|
60
|
+
- metadata: Additional information
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
# Parse with syntax API
|
|
64
|
+
raw_ast = self.syntax_api.parse_expression(expression)
|
|
65
|
+
|
|
66
|
+
# Extract context and expression
|
|
67
|
+
context, expr_ast = self._extract_components(raw_ast)
|
|
68
|
+
|
|
69
|
+
# Convert to clean JSON format
|
|
70
|
+
ast_dict = self._to_clean_json(expr_ast, context)
|
|
71
|
+
|
|
72
|
+
# Apply version normalization
|
|
73
|
+
normalized_ast = self._normalize_for_compatibility(ast_dict)
|
|
74
|
+
|
|
75
|
+
# Optional semantic validation
|
|
76
|
+
semantic_info = None
|
|
77
|
+
if self.enable_semantic and self.semantic_api:
|
|
78
|
+
semantic_info = self._validate_semantics(expression)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
'success': True,
|
|
82
|
+
'ast': normalized_ast,
|
|
83
|
+
'context': self._serialize_context(context),
|
|
84
|
+
'error': None,
|
|
85
|
+
'metadata': {
|
|
86
|
+
'has_context': context is not None,
|
|
87
|
+
'expression_type': normalized_ast.get('class_name', 'Unknown'),
|
|
88
|
+
'semantic_info': semantic_info,
|
|
89
|
+
'compatibility_mode': self.compatibility_mode
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return {
|
|
95
|
+
'success': False,
|
|
96
|
+
'ast': None,
|
|
97
|
+
'context': None,
|
|
98
|
+
'error': str(e),
|
|
99
|
+
'metadata': {
|
|
100
|
+
'error_type': type(e).__name__,
|
|
101
|
+
'original_expression': expression[:100] + "..." if len(expression) > 100 else expression
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def parse_batch(self, expressions: List[str]) -> List[Dict[str, Any]]:
|
|
106
|
+
"""
|
|
107
|
+
Parse multiple expressions efficiently.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
expressions: List of DPM-XL expression strings
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of parse results (same format as parse_expression)
|
|
114
|
+
"""
|
|
115
|
+
results = []
|
|
116
|
+
for i, expr in enumerate(expressions):
|
|
117
|
+
result = self.parse_expression(expr)
|
|
118
|
+
result['metadata']['batch_index'] = i
|
|
119
|
+
results.append(result)
|
|
120
|
+
|
|
121
|
+
return results
|
|
122
|
+
|
|
123
|
+
def validate_expression(self, expression: str) -> Dict[str, Any]:
|
|
124
|
+
"""
|
|
125
|
+
Validate expression syntax without full parsing.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
expression: DPM-XL expression string
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Dictionary containing validation result
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
self.syntax_api.parse_expression(expression)
|
|
135
|
+
return {
|
|
136
|
+
'valid': True,
|
|
137
|
+
'error': None,
|
|
138
|
+
'expression': expression
|
|
139
|
+
}
|
|
140
|
+
except Exception as e:
|
|
141
|
+
return {
|
|
142
|
+
'valid': False,
|
|
143
|
+
'error': str(e),
|
|
144
|
+
'error_type': type(e).__name__,
|
|
145
|
+
'expression': expression
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def get_expression_info(self, expression: str) -> Dict[str, Any]:
|
|
149
|
+
"""
|
|
150
|
+
Get comprehensive information about an expression.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
expression: DPM-XL expression string
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dictionary with expression analysis
|
|
157
|
+
"""
|
|
158
|
+
result = self.parse_expression(expression)
|
|
159
|
+
if not result['success']:
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
ast = result['ast']
|
|
163
|
+
context = result['context']
|
|
164
|
+
|
|
165
|
+
# Analyze AST structure
|
|
166
|
+
analysis = {
|
|
167
|
+
'variable_references': self._extract_variables(ast),
|
|
168
|
+
'constants': self._extract_constants(ast),
|
|
169
|
+
'operations': self._extract_operations(ast),
|
|
170
|
+
'has_aggregations': self._has_aggregations(ast),
|
|
171
|
+
'has_conditionals': self._has_conditionals(ast),
|
|
172
|
+
'complexity_score': self._calculate_complexity(ast),
|
|
173
|
+
'context_info': context
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
result['analysis'] = analysis
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
# Internal helper methods
|
|
180
|
+
|
|
181
|
+
def _extract_components(self, raw_ast):
|
|
182
|
+
"""Extract context and expression from raw AST."""
|
|
183
|
+
if hasattr(raw_ast, 'children') and len(raw_ast.children) > 0:
|
|
184
|
+
child = raw_ast.children[0]
|
|
185
|
+
if hasattr(child, 'expression') and hasattr(child, 'partial_selection'):
|
|
186
|
+
return child.partial_selection, child.expression
|
|
187
|
+
else:
|
|
188
|
+
return None, child
|
|
189
|
+
return None, raw_ast
|
|
190
|
+
|
|
191
|
+
def _to_clean_json(self, ast_node, context=None):
|
|
192
|
+
"""Convert AST node to clean JSON format."""
|
|
193
|
+
# Import the serialization function from utils
|
|
194
|
+
from py_dpm.utils.ast_serialization import serialize_ast
|
|
195
|
+
|
|
196
|
+
# Use the serialize_ast function which handles all AST node types properly
|
|
197
|
+
return serialize_ast(ast_node)
|
|
198
|
+
|
|
199
|
+
def _serialize_context(self, context):
|
|
200
|
+
"""Serialize context to clean dictionary."""
|
|
201
|
+
if not context:
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
'table': getattr(context, 'table', None),
|
|
206
|
+
'rows': getattr(context, 'rows', None),
|
|
207
|
+
'columns': getattr(context, 'cols', None),
|
|
208
|
+
'sheets': getattr(context, 'sheets', None),
|
|
209
|
+
'default': getattr(context, 'default', None),
|
|
210
|
+
'interval': getattr(context, 'interval', None)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
def _normalize_for_compatibility(self, ast_dict):
|
|
214
|
+
"""Apply version compatibility normalization."""
|
|
215
|
+
if self.compatibility_mode == "auto":
|
|
216
|
+
# Auto-detect and normalize
|
|
217
|
+
return self._auto_normalize(ast_dict)
|
|
218
|
+
elif self.compatibility_mode in self._version_normalizers:
|
|
219
|
+
normalizer = self._version_normalizers[self.compatibility_mode]
|
|
220
|
+
return normalizer(ast_dict)
|
|
221
|
+
else:
|
|
222
|
+
return ast_dict
|
|
223
|
+
|
|
224
|
+
def _setup_version_normalizers(self):
|
|
225
|
+
"""Setup version-specific normalizers."""
|
|
226
|
+
return {
|
|
227
|
+
"3.1.0": self._normalize_v3_1_0,
|
|
228
|
+
"4.0.0": self._normalize_v4_0_0,
|
|
229
|
+
"current": lambda x: x
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
def _normalize_v3_1_0(self, ast_dict):
|
|
233
|
+
"""Normalize AST for version 3.1.0 compatibility."""
|
|
234
|
+
if not isinstance(ast_dict, dict):
|
|
235
|
+
return ast_dict
|
|
236
|
+
|
|
237
|
+
normalized = {}
|
|
238
|
+
for key, value in ast_dict.items():
|
|
239
|
+
# Handle Scalar item naming for v3.1.0
|
|
240
|
+
if key == 'item' and isinstance(value, str) and ':' in value:
|
|
241
|
+
namespace, code = value.split(':', 1)
|
|
242
|
+
if namespace.endswith('_qEC'):
|
|
243
|
+
namespace = namespace.replace('_qEC', '_EC')
|
|
244
|
+
if code.startswith('qx'):
|
|
245
|
+
code = code[1:]
|
|
246
|
+
normalized[key] = f"{namespace}:{code}"
|
|
247
|
+
|
|
248
|
+
# Handle TimeShiftOp field mapping
|
|
249
|
+
elif ast_dict.get('class_name') == 'TimeShiftOp':
|
|
250
|
+
if key == 'component':
|
|
251
|
+
normalized['reference_period'] = value
|
|
252
|
+
continue
|
|
253
|
+
elif key == 'shift_number' and not isinstance(value, dict):
|
|
254
|
+
# Convert to Constant format for v3.1.0
|
|
255
|
+
normalized[key] = {
|
|
256
|
+
'class_name': 'Constant',
|
|
257
|
+
'type_': 'Integer',
|
|
258
|
+
'value': int(value)
|
|
259
|
+
}
|
|
260
|
+
continue
|
|
261
|
+
elif key == 'period_indicator' and not isinstance(value, dict):
|
|
262
|
+
# Convert to Constant format for v3.1.0
|
|
263
|
+
period_map = {'A': 'Q'} # Map known differences
|
|
264
|
+
actual_value = period_map.get(value, value)
|
|
265
|
+
normalized[key] = {
|
|
266
|
+
'class_name': 'Constant',
|
|
267
|
+
'type_': 'String',
|
|
268
|
+
'value': actual_value
|
|
269
|
+
}
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
# Recursively normalize nested structures
|
|
273
|
+
if isinstance(value, dict):
|
|
274
|
+
normalized[key] = self._normalize_v3_1_0(value)
|
|
275
|
+
elif isinstance(value, list):
|
|
276
|
+
normalized[key] = [self._normalize_v3_1_0(item) if isinstance(item, dict) else item for item in value]
|
|
277
|
+
else:
|
|
278
|
+
normalized[key] = value
|
|
279
|
+
|
|
280
|
+
return normalized
|
|
281
|
+
|
|
282
|
+
def _normalize_v4_0_0(self, ast_dict):
|
|
283
|
+
"""Normalize AST for version 4.0.0 compatibility."""
|
|
284
|
+
if not isinstance(ast_dict, dict):
|
|
285
|
+
return ast_dict
|
|
286
|
+
|
|
287
|
+
normalized = {}
|
|
288
|
+
for key, value in ast_dict.items():
|
|
289
|
+
# Handle Scalar item naming for v4.0.0
|
|
290
|
+
if key == 'item' and isinstance(value, str) and ':' in value:
|
|
291
|
+
namespace, code = value.split(':', 1)
|
|
292
|
+
if namespace.endswith('_EC') and not namespace.endswith('_qEC'):
|
|
293
|
+
namespace = namespace.replace('_EC', '_qEC')
|
|
294
|
+
if code.startswith('x') and not code.startswith('qx'):
|
|
295
|
+
code = 'q' + code
|
|
296
|
+
normalized[key] = f"{namespace}:{code}"
|
|
297
|
+
|
|
298
|
+
# Handle TimeShiftOp field mapping
|
|
299
|
+
elif ast_dict.get('class_name') == 'TimeShiftOp':
|
|
300
|
+
if key == 'reference_period':
|
|
301
|
+
normalized['component'] = value
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
# Recursively normalize nested structures
|
|
305
|
+
if isinstance(value, dict):
|
|
306
|
+
normalized[key] = self._normalize_v4_0_0(value)
|
|
307
|
+
elif isinstance(value, list):
|
|
308
|
+
normalized[key] = [self._normalize_v4_0_0(item) if isinstance(item, dict) else item for item in value]
|
|
309
|
+
else:
|
|
310
|
+
normalized[key] = value
|
|
311
|
+
|
|
312
|
+
return normalized
|
|
313
|
+
|
|
314
|
+
def _auto_normalize(self, ast_dict):
|
|
315
|
+
"""Auto-detect version and normalize accordingly."""
|
|
316
|
+
# Simple heuristic: check for version-specific patterns
|
|
317
|
+
ast_str = json.dumps(ast_dict) if ast_dict else ""
|
|
318
|
+
|
|
319
|
+
if 'eba_qEC' in ast_str or 'qx' in ast_str:
|
|
320
|
+
# Looks like v4.0.0 format, normalize to current
|
|
321
|
+
return self._normalize_v4_0_0(ast_dict)
|
|
322
|
+
elif 'eba_EC' in ast_str and 'reference_period' in ast_str:
|
|
323
|
+
# Looks like v3.1.0 format
|
|
324
|
+
return ast_dict
|
|
325
|
+
else:
|
|
326
|
+
# Default to current format
|
|
327
|
+
return ast_dict
|
|
328
|
+
|
|
329
|
+
def _validate_semantics(self, expression):
|
|
330
|
+
"""Perform semantic validation if enabled."""
|
|
331
|
+
try:
|
|
332
|
+
# This would integrate with semantic API when available
|
|
333
|
+
return {'semantic_valid': True, 'operands_checked': False}
|
|
334
|
+
except Exception as e:
|
|
335
|
+
return {'semantic_valid': False, 'error': str(e)}
|
|
336
|
+
|
|
337
|
+
def _extract_variables(self, ast_dict):
|
|
338
|
+
"""Extract variable references from AST."""
|
|
339
|
+
variables = []
|
|
340
|
+
self._traverse_for_type(ast_dict, 'VarID', variables)
|
|
341
|
+
return variables
|
|
342
|
+
|
|
343
|
+
def _extract_constants(self, ast_dict):
|
|
344
|
+
"""Extract constants from AST."""
|
|
345
|
+
constants = []
|
|
346
|
+
self._traverse_for_type(ast_dict, 'Constant', constants)
|
|
347
|
+
return constants
|
|
348
|
+
|
|
349
|
+
def _extract_operations(self, ast_dict):
|
|
350
|
+
"""Extract operations from AST."""
|
|
351
|
+
operations = []
|
|
352
|
+
for op_type in ['BinOp', 'UnaryOp', 'AggregationOp', 'CondExpr']:
|
|
353
|
+
self._traverse_for_type(ast_dict, op_type, operations)
|
|
354
|
+
return operations
|
|
355
|
+
|
|
356
|
+
def _traverse_for_type(self, ast_dict, target_type, collector):
|
|
357
|
+
"""Traverse AST collecting nodes of specific type."""
|
|
358
|
+
if isinstance(ast_dict, dict):
|
|
359
|
+
if ast_dict.get('class_name') == target_type:
|
|
360
|
+
collector.append(ast_dict)
|
|
361
|
+
for value in ast_dict.values():
|
|
362
|
+
if isinstance(value, (dict, list)):
|
|
363
|
+
self._traverse_for_type(value, target_type, collector)
|
|
364
|
+
elif isinstance(ast_dict, list):
|
|
365
|
+
for item in ast_dict:
|
|
366
|
+
self._traverse_for_type(item, target_type, collector)
|
|
367
|
+
|
|
368
|
+
def _has_aggregations(self, ast_dict):
|
|
369
|
+
"""Check if AST contains aggregation operations."""
|
|
370
|
+
aggregations = []
|
|
371
|
+
self._traverse_for_type(ast_dict, 'AggregationOp', aggregations)
|
|
372
|
+
return len(aggregations) > 0
|
|
373
|
+
|
|
374
|
+
def _has_conditionals(self, ast_dict):
|
|
375
|
+
"""Check if AST contains conditional expressions."""
|
|
376
|
+
conditionals = []
|
|
377
|
+
self._traverse_for_type(ast_dict, 'CondExpr', conditionals)
|
|
378
|
+
return len(conditionals) > 0
|
|
379
|
+
|
|
380
|
+
def _calculate_complexity(self, ast_dict):
|
|
381
|
+
"""Calculate complexity score for AST."""
|
|
382
|
+
score = 0
|
|
383
|
+
if isinstance(ast_dict, dict):
|
|
384
|
+
score += 1
|
|
385
|
+
for value in ast_dict.values():
|
|
386
|
+
if isinstance(value, (dict, list)):
|
|
387
|
+
score += self._calculate_complexity(value)
|
|
388
|
+
elif isinstance(ast_dict, list):
|
|
389
|
+
for item in ast_dict:
|
|
390
|
+
score += self._calculate_complexity(item)
|
|
391
|
+
return score
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# Convenience functions for simple usage
|
|
395
|
+
|
|
396
|
+
def parse_expression(expression: str, compatibility_mode: str = "auto") -> Dict[str, Any]:
|
|
397
|
+
"""
|
|
398
|
+
Simple function to parse a single expression.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
expression: DPM-XL expression string
|
|
402
|
+
compatibility_mode: Version compatibility mode
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Parse result dictionary
|
|
406
|
+
"""
|
|
407
|
+
generator = ASTGenerator(compatibility_mode=compatibility_mode)
|
|
408
|
+
return generator.parse_expression(expression)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def validate_expression(expression: str) -> bool:
|
|
412
|
+
"""
|
|
413
|
+
Simple function to validate expression syntax.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
expression: DPM-XL expression string
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
True if valid, False otherwise
|
|
420
|
+
"""
|
|
421
|
+
generator = ASTGenerator()
|
|
422
|
+
result = generator.validate_expression(expression)
|
|
423
|
+
return result['valid']
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def parse_batch(expressions: List[str], compatibility_mode: str = "auto") -> List[Dict[str, Any]]:
|
|
427
|
+
"""
|
|
428
|
+
Simple function to parse multiple expressions.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
expressions: List of DPM-XL expression strings
|
|
432
|
+
compatibility_mode: Version compatibility mode
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
List of parse results
|
|
436
|
+
"""
|
|
437
|
+
generator = ASTGenerator(compatibility_mode=compatibility_mode)
|
|
438
|
+
return generator.parse_batch(expressions)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Complete AST API - Generate ASTs exactly like the JSON examples
|
|
4
|
+
|
|
5
|
+
This API generates ASTs with complete data fields including datapoint IDs and operand references,
|
|
6
|
+
exactly matching the structure found in json_scripts/*.json files.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from py_dpm.utils.ast_serialization import ASTToJSONVisitor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_complete_ast(expression: str, database_path: str = None):
|
|
13
|
+
"""
|
|
14
|
+
Generate complete AST with all data fields, exactly like json_scripts examples.
|
|
15
|
+
|
|
16
|
+
This function replicates the exact same process used to generate the reference
|
|
17
|
+
JSON files in json_scripts/, ensuring complete data field population.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
expression: DPM-XL expression string
|
|
21
|
+
database_path: Path to SQLite database file (e.g., "./database.db")
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
dict: {
|
|
25
|
+
'success': bool,
|
|
26
|
+
'ast': dict, # Complete AST with data fields
|
|
27
|
+
'context': dict, # Context from WITH clause
|
|
28
|
+
'error': str, # Error if failed
|
|
29
|
+
'data_populated': bool # Whether data fields were populated
|
|
30
|
+
}
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
# Import here to avoid circular imports
|
|
34
|
+
from py_dpm.api import API
|
|
35
|
+
from py_dpm.db_utils import get_engine
|
|
36
|
+
|
|
37
|
+
# Initialize database connection if provided
|
|
38
|
+
if database_path:
|
|
39
|
+
try:
|
|
40
|
+
engine = get_engine(database_path)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
return {
|
|
43
|
+
'success': False,
|
|
44
|
+
'ast': None,
|
|
45
|
+
'context': None,
|
|
46
|
+
'error': f'Database connection failed: {e}',
|
|
47
|
+
'data_populated': False
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Use the legacy API which does complete semantic validation
|
|
51
|
+
# This is the same API used to generate the original JSON files
|
|
52
|
+
api = API()
|
|
53
|
+
|
|
54
|
+
# Perform complete semantic validation with operand checking
|
|
55
|
+
# This should populate all data fields on VarID nodes
|
|
56
|
+
semantic_result = api.semantic_validation(expression)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Force data population if semantic validation completed successfully
|
|
60
|
+
if hasattr(api, 'AST') and api.AST and semantic_result:
|
|
61
|
+
try:
|
|
62
|
+
from py_dpm.AST.check_operands import OperandsChecking
|
|
63
|
+
from py_dpm.db_utils import get_session
|
|
64
|
+
|
|
65
|
+
session = get_session()
|
|
66
|
+
|
|
67
|
+
# Extract the expression AST
|
|
68
|
+
def get_inner_ast(ast_obj):
|
|
69
|
+
if hasattr(ast_obj, 'children') and len(ast_obj.children) > 0:
|
|
70
|
+
child = ast_obj.children[0]
|
|
71
|
+
if hasattr(child, 'expression'):
|
|
72
|
+
return child.expression
|
|
73
|
+
else:
|
|
74
|
+
return child
|
|
75
|
+
return ast_obj
|
|
76
|
+
|
|
77
|
+
inner_ast = get_inner_ast(api.AST)
|
|
78
|
+
|
|
79
|
+
# Run operand checking to populate data fields
|
|
80
|
+
oc = OperandsChecking(
|
|
81
|
+
session=session,
|
|
82
|
+
expression=expression,
|
|
83
|
+
ast=inner_ast,
|
|
84
|
+
release_id=None
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# Apply the data from operand checker to VarID nodes
|
|
89
|
+
if hasattr(oc, 'data') and oc.data is not None:
|
|
90
|
+
|
|
91
|
+
# Apply data to VarID nodes in the AST
|
|
92
|
+
def apply_data_to_varids(node):
|
|
93
|
+
if hasattr(node, '__class__') and node.__class__.__name__ == 'VarID':
|
|
94
|
+
table = getattr(node, 'table', None)
|
|
95
|
+
rows = getattr(node, 'rows', None)
|
|
96
|
+
cols = getattr(node, 'cols', None)
|
|
97
|
+
|
|
98
|
+
if table and table in oc.operands:
|
|
99
|
+
# Filter data for this specific VarID
|
|
100
|
+
filtered_data = oc.data[
|
|
101
|
+
(oc.data['table_code'] == table) &
|
|
102
|
+
(oc.data['row_code'].isin(rows or [])) &
|
|
103
|
+
(oc.data['column_code'].isin(cols or []))
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
if not filtered_data.empty:
|
|
107
|
+
# Set the data attribute on the VarID node
|
|
108
|
+
node.data = filtered_data
|
|
109
|
+
|
|
110
|
+
# Recursively apply to child nodes
|
|
111
|
+
for attr_name in ['children', 'left', 'right', 'operand', 'expression', 'condition', 'then_expr', 'else_expr']:
|
|
112
|
+
if hasattr(node, attr_name):
|
|
113
|
+
attr_value = getattr(node, attr_name)
|
|
114
|
+
if attr_value and hasattr(attr_value, '__class__'):
|
|
115
|
+
apply_data_to_varids(attr_value)
|
|
116
|
+
elif isinstance(attr_value, list):
|
|
117
|
+
for item in attr_value:
|
|
118
|
+
if hasattr(item, '__class__'):
|
|
119
|
+
apply_data_to_varids(item)
|
|
120
|
+
|
|
121
|
+
# Apply data to all VarID nodes in the AST
|
|
122
|
+
apply_data_to_varids(inner_ast)
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
# Silently continue if data population fails
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
if hasattr(api, 'AST') and api.AST is not None:
|
|
129
|
+
# Extract components exactly like batch_validator does
|
|
130
|
+
def extract_components(ast_obj):
|
|
131
|
+
if hasattr(ast_obj, 'children') and len(ast_obj.children) > 0:
|
|
132
|
+
child = ast_obj.children[0]
|
|
133
|
+
if hasattr(child, 'expression'):
|
|
134
|
+
return child.expression, child.partial_selection
|
|
135
|
+
else:
|
|
136
|
+
return child, None
|
|
137
|
+
return ast_obj, None
|
|
138
|
+
|
|
139
|
+
actual_ast, context = extract_components(api.AST)
|
|
140
|
+
|
|
141
|
+
# Convert to JSON exactly like batch_validator does
|
|
142
|
+
visitor = ASTToJSONVisitor(context)
|
|
143
|
+
ast_dict = visitor.visit(actual_ast)
|
|
144
|
+
|
|
145
|
+
# Check if data fields were populated
|
|
146
|
+
data_populated = _check_data_fields_populated(ast_dict)
|
|
147
|
+
|
|
148
|
+
# Serialize context
|
|
149
|
+
context_dict = None
|
|
150
|
+
if context:
|
|
151
|
+
context_dict = {
|
|
152
|
+
'table': getattr(context, 'table', None),
|
|
153
|
+
'rows': getattr(context, 'rows', None),
|
|
154
|
+
'columns': getattr(context, 'cols', None),
|
|
155
|
+
'sheets': getattr(context, 'sheets', None),
|
|
156
|
+
'default': getattr(context, 'default', None),
|
|
157
|
+
'interval': getattr(context, 'interval', None)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
'success': True,
|
|
162
|
+
'ast': ast_dict,
|
|
163
|
+
'context': context_dict,
|
|
164
|
+
'error': None,
|
|
165
|
+
'data_populated': data_populated,
|
|
166
|
+
'semantic_result': semantic_result
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
else:
|
|
170
|
+
return {
|
|
171
|
+
'success': False,
|
|
172
|
+
'ast': None,
|
|
173
|
+
'context': None,
|
|
174
|
+
'error': 'Semantic validation did not generate AST',
|
|
175
|
+
'data_populated': False
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
return {
|
|
180
|
+
'success': False,
|
|
181
|
+
'ast': None,
|
|
182
|
+
'context': None,
|
|
183
|
+
'error': f'API error: {str(e)}',
|
|
184
|
+
'data_populated': False
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _check_data_fields_populated(ast_dict):
|
|
189
|
+
"""Check if any VarID nodes have data fields populated"""
|
|
190
|
+
if not isinstance(ast_dict, dict):
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
if ast_dict.get('class_name') == 'VarID' and 'data' in ast_dict:
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
# Recursively check nested structures
|
|
197
|
+
for value in ast_dict.values():
|
|
198
|
+
if isinstance(value, dict):
|
|
199
|
+
if _check_data_fields_populated(value):
|
|
200
|
+
return True
|
|
201
|
+
elif isinstance(value, list):
|
|
202
|
+
for item in value:
|
|
203
|
+
if isinstance(item, dict) and _check_data_fields_populated(item):
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def generate_complete_batch(expressions: list, database_path: str = None):
|
|
210
|
+
"""
|
|
211
|
+
Generate complete ASTs for multiple expressions.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
expressions: List of DPM-XL expression strings
|
|
215
|
+
database_path: Path to SQLite database file
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
list: List of result dictionaries
|
|
219
|
+
"""
|
|
220
|
+
results = []
|
|
221
|
+
for i, expr in enumerate(expressions):
|
|
222
|
+
result = generate_complete_ast(expr, database_path)
|
|
223
|
+
result['batch_index'] = i
|
|
224
|
+
results.append(result)
|
|
225
|
+
return results
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# Convenience function with cleaner interface
|
|
229
|
+
def parse_with_data_fields(expression: str, database_path: str = None):
|
|
230
|
+
"""
|
|
231
|
+
Simple function to parse expression and get AST with data fields.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
expression: DPM-XL expression string
|
|
235
|
+
database_path: Path to SQLite database file
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
dict: AST dictionary with data fields, or None if failed
|
|
239
|
+
"""
|
|
240
|
+
result = generate_complete_ast(expression, database_path)
|
|
241
|
+
return result['ast'] if result['success'] else None
|