pydpm_xl 0.1.39rc32__py3-none-any.whl → 0.2.1__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/__init__.py +1 -1
- py_dpm/api/__init__.py +58 -189
- py_dpm/api/dpm/__init__.py +20 -0
- py_dpm/api/{data_dictionary.py → dpm/data_dictionary.py} +903 -984
- py_dpm/api/dpm/explorer.py +236 -0
- py_dpm/api/dpm/hierarchical_queries.py +142 -0
- py_dpm/api/{migration.py → dpm/migration.py} +16 -19
- py_dpm/api/{operation_scopes.py → dpm/operation_scopes.py} +319 -267
- py_dpm/api/dpm_xl/__init__.py +25 -0
- py_dpm/api/{ast_generator.py → dpm_xl/ast_generator.py} +3 -3
- py_dpm/api/{complete_ast.py → dpm_xl/complete_ast.py} +186 -284
- py_dpm/api/dpm_xl/semantic.py +358 -0
- py_dpm/api/{syntax.py → dpm_xl/syntax.py} +6 -5
- py_dpm/api/explorer.py +4 -0
- py_dpm/api/semantic.py +30 -306
- py_dpm/cli/__init__.py +9 -0
- py_dpm/{client.py → cli/main.py} +12 -10
- py_dpm/dpm/__init__.py +11 -0
- py_dpm/{models.py → dpm/models.py} +112 -88
- py_dpm/dpm/queries/base.py +100 -0
- py_dpm/dpm/queries/basic_objects.py +33 -0
- py_dpm/dpm/queries/explorer_queries.py +352 -0
- py_dpm/dpm/queries/filters.py +139 -0
- py_dpm/dpm/queries/glossary.py +45 -0
- py_dpm/dpm/queries/hierarchical_queries.py +838 -0
- py_dpm/dpm/queries/tables.py +133 -0
- py_dpm/dpm/utils.py +356 -0
- py_dpm/dpm_xl/__init__.py +8 -0
- py_dpm/dpm_xl/ast/__init__.py +14 -0
- py_dpm/{AST/ASTConstructor.py → dpm_xl/ast/constructor.py} +6 -6
- py_dpm/{AST/MLGeneration.py → dpm_xl/ast/ml_generation.py} +137 -87
- py_dpm/{AST/ModuleAnalyzer.py → dpm_xl/ast/module_analyzer.py} +7 -7
- py_dpm/{AST/ModuleDependencies.py → dpm_xl/ast/module_dependencies.py} +56 -41
- py_dpm/{AST/ASTObjects.py → dpm_xl/ast/nodes.py} +1 -1
- py_dpm/{AST/check_operands.py → dpm_xl/ast/operands.py} +16 -13
- py_dpm/{AST/ASTTemplate.py → dpm_xl/ast/template.py} +2 -2
- py_dpm/{AST/WhereClauseChecker.py → dpm_xl/ast/where_clause.py} +2 -2
- py_dpm/dpm_xl/grammar/__init__.py +18 -0
- py_dpm/dpm_xl/operators/__init__.py +19 -0
- py_dpm/{Operators/AggregateOperators.py → dpm_xl/operators/aggregate.py} +7 -7
- py_dpm/{Operators/NumericOperators.py → dpm_xl/operators/arithmetic.py} +6 -6
- py_dpm/{Operators/Operator.py → dpm_xl/operators/base.py} +5 -5
- py_dpm/{Operators/BooleanOperators.py → dpm_xl/operators/boolean.py} +5 -5
- py_dpm/{Operators/ClauseOperators.py → dpm_xl/operators/clause.py} +8 -8
- py_dpm/{Operators/ComparisonOperators.py → dpm_xl/operators/comparison.py} +5 -5
- py_dpm/{Operators/ConditionalOperators.py → dpm_xl/operators/conditional.py} +7 -7
- py_dpm/{Operators/StringOperators.py → dpm_xl/operators/string.py} +5 -5
- py_dpm/{Operators/TimeOperators.py → dpm_xl/operators/time.py} +6 -6
- py_dpm/{semantics/SemanticAnalyzer.py → dpm_xl/semantic_analyzer.py} +168 -68
- py_dpm/{semantics/Symbols.py → dpm_xl/symbols.py} +3 -3
- py_dpm/dpm_xl/types/__init__.py +13 -0
- py_dpm/{DataTypes/TypePromotion.py → dpm_xl/types/promotion.py} +2 -2
- py_dpm/{DataTypes/ScalarTypes.py → dpm_xl/types/scalar.py} +2 -2
- py_dpm/dpm_xl/utils/__init__.py +14 -0
- py_dpm/{data_handlers.py → dpm_xl/utils/data_handlers.py} +2 -2
- py_dpm/{Utils → dpm_xl/utils}/operands_mapping.py +1 -1
- py_dpm/{Utils → dpm_xl/utils}/operator_mapping.py +8 -8
- py_dpm/{OperationScopes/OperationScopeService.py → dpm_xl/utils/scopes_calculator.py} +148 -58
- py_dpm/{Utils/ast_serialization.py → dpm_xl/utils/serialization.py} +3 -4
- py_dpm/dpm_xl/validation/__init__.py +12 -0
- py_dpm/{Utils/ValidationsGenerationUtils.py → dpm_xl/validation/generation_utils.py} +2 -3
- py_dpm/{ValidationsGeneration/PropertiesConstraintsProcessor.py → dpm_xl/validation/property_constraints.py} +56 -21
- py_dpm/{ValidationsGeneration/auxiliary_functions.py → dpm_xl/validation/utils.py} +2 -2
- py_dpm/{ValidationsGeneration/VariantsProcessor.py → dpm_xl/validation/variants.py} +149 -55
- py_dpm/exceptions/__init__.py +23 -0
- py_dpm/{Exceptions → exceptions}/exceptions.py +7 -2
- pydpm_xl-0.2.1.dist-info/METADATA +278 -0
- pydpm_xl-0.2.1.dist-info/RECORD +88 -0
- pydpm_xl-0.2.1.dist-info/entry_points.txt +2 -0
- py_dpm/Exceptions/__init__.py +0 -0
- py_dpm/OperationScopes/__init__.py +0 -0
- py_dpm/Operators/__init__.py +0 -0
- py_dpm/Utils/__init__.py +0 -0
- py_dpm/Utils/utils.py +0 -2
- py_dpm/ValidationsGeneration/Utils.py +0 -364
- py_dpm/ValidationsGeneration/__init__.py +0 -0
- py_dpm/api/data_dictionary_validation.py +0 -614
- py_dpm/db_utils.py +0 -221
- py_dpm/grammar/__init__.py +0 -0
- py_dpm/grammar/dist/__init__.py +0 -0
- py_dpm/grammar/dpm_xlLexer.g4 +0 -437
- py_dpm/grammar/dpm_xlParser.g4 +0 -263
- py_dpm/semantics/DAG/DAGAnalyzer.py +0 -158
- py_dpm/semantics/DAG/__init__.py +0 -0
- py_dpm/semantics/__init__.py +0 -0
- py_dpm/views/data_types.sql +0 -12
- py_dpm/views/datapoints.sql +0 -65
- py_dpm/views/hierarchy_operand_reference.sql +0 -11
- py_dpm/views/hierarchy_preconditions.sql +0 -13
- py_dpm/views/hierarchy_variables.sql +0 -26
- py_dpm/views/hierarchy_variables_context.sql +0 -14
- py_dpm/views/key_components.sql +0 -18
- py_dpm/views/module_from_table.sql +0 -11
- py_dpm/views/open_keys.sql +0 -13
- py_dpm/views/operation_info.sql +0 -27
- py_dpm/views/operation_list.sql +0 -18
- py_dpm/views/operations_versions_from_module_version.sql +0 -30
- py_dpm/views/precondition_info.sql +0 -17
- py_dpm/views/report_type_operand_reference_info.sql +0 -18
- py_dpm/views/subcategory_info.sql +0 -17
- py_dpm/views/table_info.sql +0 -19
- pydpm_xl-0.1.39rc32.dist-info/METADATA +0 -53
- pydpm_xl-0.1.39rc32.dist-info/RECORD +0 -96
- pydpm_xl-0.1.39rc32.dist-info/entry_points.txt +0 -2
- /py_dpm/{AST → cli/commands}/__init__.py +0 -0
- /py_dpm/{migration.py → dpm/migration.py} +0 -0
- /py_dpm/{AST/ASTVisitor.py → dpm_xl/ast/visitor.py} +0 -0
- /py_dpm/{DataTypes → dpm_xl/grammar/generated}/__init__.py +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.interp +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.py +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.tokens +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.interp +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.py +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.tokens +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParserListener.py +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParserVisitor.py +0 -0
- /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/listeners.py +0 -0
- /py_dpm/{DataTypes/TimeClasses.py → dpm_xl/types/time.py} +0 -0
- /py_dpm/{Utils → dpm_xl/utils}/tokens.py +0 -0
- /py_dpm/{Exceptions → exceptions}/messages.py +0 -0
- {pydpm_xl-0.1.39rc32.dist-info → pydpm_xl-0.2.1.dist-info}/WHEEL +0 -0
- {pydpm_xl-0.1.39rc32.dist-info → pydpm_xl-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {pydpm_xl-0.1.39rc32.dist-info → pydpm_xl-0.2.1.dist-info}/top_level.txt +0 -0
|
@@ -10,21 +10,21 @@ Also provides enrichment functionality to create engine-ready ASTs with framewor
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from datetime import datetime
|
|
13
|
-
from typing import Dict, Any, Optional
|
|
14
|
-
from py_dpm.
|
|
13
|
+
from typing import Dict, Any, Any, Optional
|
|
14
|
+
from py_dpm.dpm_xl.utils.serialization import ASTToJSONVisitor
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def generate_complete_ast(
|
|
18
18
|
expression: str,
|
|
19
19
|
database_path: str = None,
|
|
20
20
|
connection_url: str = None,
|
|
21
|
-
release_id: Optional[int] = None
|
|
21
|
+
release_id: Optional[int] = None,
|
|
22
22
|
):
|
|
23
23
|
"""
|
|
24
24
|
Generate complete AST with all data fields, exactly like json_scripts examples.
|
|
25
25
|
|
|
26
|
-
This function replicates the
|
|
27
|
-
|
|
26
|
+
This function replicates the process used to generate the reference JSON files,
|
|
27
|
+
using the new SemanticAPI to perform full semantic validation and operand checking.
|
|
28
28
|
|
|
29
29
|
Args:
|
|
30
30
|
expression: DPM-XL expression string
|
|
@@ -34,201 +34,107 @@ def generate_complete_ast(
|
|
|
34
34
|
If None, uses all available data (release-agnostic).
|
|
35
35
|
|
|
36
36
|
Returns:
|
|
37
|
-
dict:
|
|
38
|
-
|
|
39
|
-
'ast': dict, # Complete AST with data fields
|
|
40
|
-
'context': dict, # Context from WITH clause
|
|
41
|
-
'error': str, # Error if failed
|
|
42
|
-
'data_populated': bool # Whether data fields were populated
|
|
43
|
-
}
|
|
37
|
+
dict with keys:
|
|
38
|
+
success, ast, context, error, data_populated, semantic_result
|
|
44
39
|
"""
|
|
45
40
|
try:
|
|
46
41
|
# Import here to avoid circular imports
|
|
47
|
-
from py_dpm.api import
|
|
48
|
-
from py_dpm.
|
|
42
|
+
from py_dpm.api.dpm_xl.semantic import SemanticAPI
|
|
43
|
+
from py_dpm.dpm.utils import get_engine
|
|
49
44
|
|
|
50
|
-
# Initialize database connection if provided
|
|
45
|
+
# Initialize database connection if explicitly provided, to surface connection errors early
|
|
51
46
|
if connection_url or database_path:
|
|
52
47
|
try:
|
|
53
|
-
|
|
48
|
+
get_engine(database_path=database_path, connection_url=connection_url)
|
|
54
49
|
except Exception as e:
|
|
55
50
|
return {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
"success": False,
|
|
52
|
+
"ast": None,
|
|
53
|
+
"context": None,
|
|
54
|
+
"error": f"Database connection failed: {e}",
|
|
55
|
+
"data_populated": False,
|
|
61
56
|
}
|
|
62
57
|
|
|
63
|
-
# Use the
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Perform complete semantic validation with operand checking
|
|
68
|
-
# This should populate all data fields on VarID nodes
|
|
69
|
-
semantic_result = api.semantic_validation(expression, release_id=release_id)
|
|
70
|
-
|
|
58
|
+
# Use the modern SemanticAPI which performs full semantic validation and operand checking
|
|
59
|
+
semantic_api = SemanticAPI(
|
|
60
|
+
database_path=database_path, connection_url=connection_url
|
|
61
|
+
)
|
|
71
62
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
from py_dpm.AST.check_operands import OperandsChecking
|
|
76
|
-
from py_dpm.db_utils import get_session
|
|
77
|
-
|
|
78
|
-
session = get_session()
|
|
79
|
-
|
|
80
|
-
# Extract the expression AST
|
|
81
|
-
def get_inner_ast(ast_obj):
|
|
82
|
-
if hasattr(ast_obj, 'children') and len(ast_obj.children) > 0:
|
|
83
|
-
child = ast_obj.children[0]
|
|
84
|
-
if hasattr(child, 'expression'):
|
|
85
|
-
return child.expression
|
|
86
|
-
else:
|
|
87
|
-
return child
|
|
88
|
-
return ast_obj
|
|
89
|
-
|
|
90
|
-
inner_ast = get_inner_ast(api.AST)
|
|
91
|
-
|
|
92
|
-
# Run operand checking to populate data fields
|
|
93
|
-
oc = OperandsChecking(
|
|
94
|
-
session=session,
|
|
95
|
-
expression=expression,
|
|
96
|
-
ast=inner_ast,
|
|
97
|
-
release_id=release_id
|
|
98
|
-
)
|
|
63
|
+
semantic_result = semantic_api.validate_expression(
|
|
64
|
+
expression, release_id=release_id
|
|
65
|
+
)
|
|
99
66
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if table and table in oc.operands:
|
|
111
|
-
# Filter data for this specific VarID
|
|
112
|
-
# Start with table filter
|
|
113
|
-
filter_mask = (oc.data['table_code'] == table)
|
|
114
|
-
|
|
115
|
-
# Add row filter only if rows is not None and doesn't contain wildcards
|
|
116
|
-
# IMPORTANT: If rows contains '*', include all rows (don't filter)
|
|
117
|
-
if rows is not None and '*' not in rows:
|
|
118
|
-
filter_mask = filter_mask & (oc.data['row_code'].isin(rows))
|
|
119
|
-
|
|
120
|
-
# Add column filter only if cols is not None and doesn't contain wildcards
|
|
121
|
-
# IMPORTANT: If cols contains '*', include all columns (don't filter)
|
|
122
|
-
if cols is not None and '*' not in cols:
|
|
123
|
-
filter_mask = filter_mask & (oc.data['column_code'].isin(cols))
|
|
124
|
-
|
|
125
|
-
filtered_data = oc.data[filter_mask]
|
|
126
|
-
|
|
127
|
-
if not filtered_data.empty:
|
|
128
|
-
# IMPORTANT: Remove wildcard entries (NULL column/row/sheet codes)
|
|
129
|
-
# when specific entries exist for the same dimension
|
|
130
|
-
# The database contains both wildcard entries (column_code=NULL for c*)
|
|
131
|
-
# and specific entries (column_code='0010'). When we query with wildcards,
|
|
132
|
-
# we want only the specific entries.
|
|
133
|
-
|
|
134
|
-
# Remove rows where column_code is NULL if there are non-NULL column_code entries
|
|
135
|
-
if filtered_data['column_code'].notna().any():
|
|
136
|
-
filtered_data = filtered_data[filtered_data['column_code'].notna()]
|
|
137
|
-
|
|
138
|
-
# Remove rows where row_code is NULL if there are non-NULL row_code entries
|
|
139
|
-
if filtered_data['row_code'].notna().any():
|
|
140
|
-
filtered_data = filtered_data[filtered_data['row_code'].notna()]
|
|
141
|
-
|
|
142
|
-
# Remove rows where sheet_code is NULL if there are non-NULL sheet_code entries
|
|
143
|
-
if filtered_data['sheet_code'].notna().any():
|
|
144
|
-
filtered_data = filtered_data[filtered_data['sheet_code'].notna()]
|
|
145
|
-
|
|
146
|
-
# IMPORTANT: After filtering, remove any remaining duplicates
|
|
147
|
-
# based on (row_code, column_code, sheet_code) combination
|
|
148
|
-
filtered_data = filtered_data.drop_duplicates(
|
|
149
|
-
subset=['row_code', 'column_code', 'sheet_code'],
|
|
150
|
-
keep='first'
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
# Set the data attribute on the VarID node
|
|
154
|
-
if not filtered_data.empty:
|
|
155
|
-
node.data = filtered_data
|
|
156
|
-
|
|
157
|
-
# Recursively apply to child nodes
|
|
158
|
-
for attr_name in ['children', 'left', 'right', 'operand', 'operands', 'expression', 'condition', 'then_expr', 'else_expr']:
|
|
159
|
-
if hasattr(node, attr_name):
|
|
160
|
-
attr_value = getattr(node, attr_name)
|
|
161
|
-
if isinstance(attr_value, list):
|
|
162
|
-
for item in attr_value:
|
|
163
|
-
if hasattr(item, '__class__'):
|
|
164
|
-
apply_data_to_varids(item)
|
|
165
|
-
elif attr_value and hasattr(attr_value, '__class__'):
|
|
166
|
-
apply_data_to_varids(attr_value)
|
|
167
|
-
|
|
168
|
-
# Apply data to all VarID nodes in the AST
|
|
169
|
-
apply_data_to_varids(inner_ast)
|
|
67
|
+
# If semantic validation failed, return structured error
|
|
68
|
+
if not semantic_result.is_valid:
|
|
69
|
+
return {
|
|
70
|
+
"success": False,
|
|
71
|
+
"ast": None,
|
|
72
|
+
"context": None,
|
|
73
|
+
"error": semantic_result.error_message,
|
|
74
|
+
"data_populated": False,
|
|
75
|
+
"semantic_result": semantic_result,
|
|
76
|
+
}
|
|
170
77
|
|
|
171
|
-
|
|
172
|
-
# Silently continue if data population fails
|
|
173
|
-
pass
|
|
174
|
-
|
|
175
|
-
if hasattr(api, 'AST') and api.AST is not None:
|
|
176
|
-
# Extract components exactly like batch_validator does
|
|
177
|
-
def extract_components(ast_obj):
|
|
178
|
-
if hasattr(ast_obj, 'children') and len(ast_obj.children) > 0:
|
|
179
|
-
child = ast_obj.children[0]
|
|
180
|
-
if hasattr(child, 'expression'):
|
|
181
|
-
return child.expression, child.partial_selection
|
|
182
|
-
else:
|
|
183
|
-
return child, None
|
|
184
|
-
return ast_obj, None
|
|
185
|
-
|
|
186
|
-
actual_ast, context = extract_components(api.AST)
|
|
187
|
-
|
|
188
|
-
# Convert to JSON exactly like batch_validator does
|
|
189
|
-
visitor = ASTToJSONVisitor(context)
|
|
190
|
-
ast_dict = visitor.visit(actual_ast)
|
|
191
|
-
|
|
192
|
-
# Check if data fields were populated
|
|
193
|
-
data_populated = _check_data_fields_populated(ast_dict)
|
|
194
|
-
|
|
195
|
-
# Serialize context
|
|
196
|
-
context_dict = None
|
|
197
|
-
if context:
|
|
198
|
-
context_dict = {
|
|
199
|
-
'table': getattr(context, 'table', None),
|
|
200
|
-
'rows': getattr(context, 'rows', None),
|
|
201
|
-
'columns': getattr(context, 'cols', None),
|
|
202
|
-
'sheets': getattr(context, 'sheets', None),
|
|
203
|
-
'default': getattr(context, 'default', None),
|
|
204
|
-
'interval': getattr(context, 'interval', None)
|
|
205
|
-
}
|
|
78
|
+
ast_root = getattr(semantic_api, "ast", None)
|
|
206
79
|
|
|
80
|
+
if ast_root is None:
|
|
207
81
|
return {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
82
|
+
"success": False,
|
|
83
|
+
"ast": None,
|
|
84
|
+
"context": None,
|
|
85
|
+
"error": "Semantic validation did not generate AST",
|
|
86
|
+
"data_populated": False,
|
|
87
|
+
"semantic_result": semantic_result,
|
|
214
88
|
}
|
|
215
89
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
90
|
+
# Extract components exactly like batch_validator does
|
|
91
|
+
def extract_components(ast_obj):
|
|
92
|
+
if hasattr(ast_obj, "children") and len(ast_obj.children) > 0:
|
|
93
|
+
child = ast_obj.children[0]
|
|
94
|
+
if hasattr(child, "expression"):
|
|
95
|
+
return child.expression, child.partial_selection
|
|
96
|
+
else:
|
|
97
|
+
return child, None
|
|
98
|
+
return ast_obj, None
|
|
99
|
+
|
|
100
|
+
actual_ast, context = extract_components(ast_root)
|
|
101
|
+
|
|
102
|
+
# Convert to JSON using the ASTToJSONVisitor, which uses VarID.data populated
|
|
103
|
+
# during semantic validation / operand checking.
|
|
104
|
+
visitor = ASTToJSONVisitor(context)
|
|
105
|
+
ast_dict = visitor.visit(actual_ast)
|
|
106
|
+
|
|
107
|
+
# Check if data fields were populated
|
|
108
|
+
data_populated = _check_data_fields_populated(ast_dict)
|
|
109
|
+
|
|
110
|
+
# Serialize context
|
|
111
|
+
context_dict = None
|
|
112
|
+
if context:
|
|
113
|
+
context_dict = {
|
|
114
|
+
"table": getattr(context, "table", None),
|
|
115
|
+
"rows": getattr(context, "rows", None),
|
|
116
|
+
"columns": getattr(context, "cols", None),
|
|
117
|
+
"sheets": getattr(context, "sheets", None),
|
|
118
|
+
"default": getattr(context, "default", None),
|
|
119
|
+
"interval": getattr(context, "interval", None),
|
|
223
120
|
}
|
|
224
121
|
|
|
122
|
+
return {
|
|
123
|
+
"success": True,
|
|
124
|
+
"ast": ast_dict,
|
|
125
|
+
"context": context_dict,
|
|
126
|
+
"error": None,
|
|
127
|
+
"data_populated": data_populated,
|
|
128
|
+
"semantic_result": semantic_result,
|
|
129
|
+
}
|
|
130
|
+
|
|
225
131
|
except Exception as e:
|
|
226
132
|
return {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
133
|
+
"success": False,
|
|
134
|
+
"ast": None,
|
|
135
|
+
"context": None,
|
|
136
|
+
"error": f"API error: {str(e)}",
|
|
137
|
+
"data_populated": False,
|
|
232
138
|
}
|
|
233
139
|
|
|
234
140
|
|
|
@@ -237,7 +143,7 @@ def _check_data_fields_populated(ast_dict):
|
|
|
237
143
|
if not isinstance(ast_dict, dict):
|
|
238
144
|
return False
|
|
239
145
|
|
|
240
|
-
if ast_dict.get(
|
|
146
|
+
if ast_dict.get("class_name") == "VarID" and "data" in ast_dict:
|
|
241
147
|
return True
|
|
242
148
|
|
|
243
149
|
# Recursively check nested structures
|
|
@@ -257,7 +163,7 @@ def generate_complete_batch(
|
|
|
257
163
|
expressions: list,
|
|
258
164
|
database_path: str = None,
|
|
259
165
|
connection_url: str = None,
|
|
260
|
-
release_id: Optional[int] = None
|
|
166
|
+
release_id: Optional[int] = None,
|
|
261
167
|
):
|
|
262
168
|
"""
|
|
263
169
|
Generate complete ASTs for multiple expressions.
|
|
@@ -277,7 +183,7 @@ def generate_complete_batch(
|
|
|
277
183
|
result = generate_complete_ast(
|
|
278
184
|
expr, database_path, connection_url, release_id=release_id
|
|
279
185
|
)
|
|
280
|
-
result[
|
|
186
|
+
result["batch_index"] = i
|
|
281
187
|
results.append(result)
|
|
282
188
|
return results
|
|
283
189
|
|
|
@@ -287,7 +193,7 @@ def parse_with_data_fields(
|
|
|
287
193
|
expression: str,
|
|
288
194
|
database_path: str = None,
|
|
289
195
|
connection_url: str = None,
|
|
290
|
-
release_id: Optional[int] = None
|
|
196
|
+
release_id: Optional[int] = None,
|
|
291
197
|
):
|
|
292
198
|
"""
|
|
293
199
|
Simple function to parse expression and get AST with data fields.
|
|
@@ -305,7 +211,7 @@ def parse_with_data_fields(
|
|
|
305
211
|
result = generate_complete_ast(
|
|
306
212
|
expression, database_path, connection_url, release_id=release_id
|
|
307
213
|
)
|
|
308
|
-
return result[
|
|
214
|
+
return result["ast"] if result["success"] else None
|
|
309
215
|
|
|
310
216
|
|
|
311
217
|
# ============================================================================
|
|
@@ -353,15 +259,15 @@ def generate_enriched_ast(
|
|
|
353
259
|
expression, database_path, connection_url, release_id=release_id
|
|
354
260
|
)
|
|
355
261
|
|
|
356
|
-
if not complete_result[
|
|
262
|
+
if not complete_result["success"]:
|
|
357
263
|
return {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
264
|
+
"success": False,
|
|
265
|
+
"enriched_ast": None,
|
|
266
|
+
"error": f"Failed to generate complete AST: {complete_result['error']}",
|
|
361
267
|
}
|
|
362
268
|
|
|
363
|
-
complete_ast = complete_result[
|
|
364
|
-
context = complete_result.get(
|
|
269
|
+
complete_ast = complete_result["ast"]
|
|
270
|
+
context = complete_result.get("context") or table_context
|
|
365
271
|
|
|
366
272
|
# Enrich with framework structure
|
|
367
273
|
enriched_ast = enrich_ast_with_metadata(
|
|
@@ -372,20 +278,16 @@ def generate_enriched_ast(
|
|
|
372
278
|
connection_url=connection_url,
|
|
373
279
|
dpm_version=dpm_version,
|
|
374
280
|
operation_code=operation_code,
|
|
375
|
-
precondition=precondition
|
|
281
|
+
precondition=precondition,
|
|
376
282
|
)
|
|
377
283
|
|
|
378
|
-
return {
|
|
379
|
-
'success': True,
|
|
380
|
-
'enriched_ast': enriched_ast,
|
|
381
|
-
'error': None
|
|
382
|
-
}
|
|
284
|
+
return {"success": True, "enriched_ast": enriched_ast, "error": None}
|
|
383
285
|
|
|
384
286
|
except Exception as e:
|
|
385
287
|
return {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
288
|
+
"success": False,
|
|
289
|
+
"enriched_ast": None,
|
|
290
|
+
"error": f"Enrichment error: {str(e)}",
|
|
389
291
|
}
|
|
390
292
|
|
|
391
293
|
|
|
@@ -417,8 +319,8 @@ def enrich_ast_with_metadata(
|
|
|
417
319
|
Returns:
|
|
418
320
|
dict: Engine-ready AST with framework structure
|
|
419
321
|
"""
|
|
420
|
-
from py_dpm.
|
|
421
|
-
from py_dpm.models import TableVersion, Release
|
|
322
|
+
from py_dpm.dpm.utils import get_engine, get_session
|
|
323
|
+
from py_dpm.dpm.models import TableVersion, Release
|
|
422
324
|
import copy
|
|
423
325
|
|
|
424
326
|
# Initialize database connection
|
|
@@ -440,13 +342,10 @@ def enrich_ast_with_metadata(
|
|
|
440
342
|
"module_version": "1.0.0",
|
|
441
343
|
"framework_code": "default",
|
|
442
344
|
"dpm_release": {
|
|
443
|
-
"release": release_info[
|
|
444
|
-
"publication_date": release_info[
|
|
345
|
+
"release": release_info["release"],
|
|
346
|
+
"publication_date": release_info["publication_date"],
|
|
445
347
|
},
|
|
446
|
-
"dates": {
|
|
447
|
-
"from": "2001-01-01",
|
|
448
|
-
"to": None
|
|
449
|
-
}
|
|
348
|
+
"dates": {"from": "2001-01-01", "to": None},
|
|
450
349
|
}
|
|
451
350
|
|
|
452
351
|
# Add coordinates to AST data entries
|
|
@@ -461,7 +360,7 @@ def enrich_ast_with_metadata(
|
|
|
461
360
|
"root_operator_id": 24, # Default for now
|
|
462
361
|
"ast": ast_with_coords,
|
|
463
362
|
"from_submission_date": current_date,
|
|
464
|
-
"severity": "Error"
|
|
363
|
+
"severity": "Error",
|
|
465
364
|
}
|
|
466
365
|
}
|
|
467
366
|
|
|
@@ -473,27 +372,24 @@ def enrich_ast_with_metadata(
|
|
|
473
372
|
|
|
474
373
|
# Build tables with their specific variables
|
|
475
374
|
for table_code, table_variables in variables_by_table.items():
|
|
476
|
-
tables[table_code] = {
|
|
477
|
-
"variables": table_variables,
|
|
478
|
-
"open_keys": {}
|
|
479
|
-
}
|
|
375
|
+
tables[table_code] = {"variables": table_variables, "open_keys": {}}
|
|
480
376
|
|
|
481
377
|
# Build preconditions
|
|
482
378
|
preconditions = {}
|
|
483
379
|
precondition_variables = {}
|
|
484
380
|
|
|
485
|
-
if precondition or (context and
|
|
381
|
+
if precondition or (context and "table" in context):
|
|
486
382
|
preconditions, precondition_variables = _build_preconditions(
|
|
487
383
|
precondition=precondition,
|
|
488
384
|
context=context,
|
|
489
385
|
operation_code=operation_code,
|
|
490
|
-
engine=engine
|
|
386
|
+
engine=engine,
|
|
491
387
|
)
|
|
492
388
|
|
|
493
389
|
# Build dependency information
|
|
494
390
|
dependency_info = {
|
|
495
391
|
"intra_instance_validations": [operation_code],
|
|
496
|
-
"cross_instance_dependencies": []
|
|
392
|
+
"cross_instance_dependencies": [],
|
|
497
393
|
}
|
|
498
394
|
|
|
499
395
|
# Build dependency modules
|
|
@@ -511,7 +407,7 @@ def enrich_ast_with_metadata(
|
|
|
511
407
|
"preconditions": preconditions,
|
|
512
408
|
"precondition_variables": precondition_variables,
|
|
513
409
|
"dependency_information": dependency_info,
|
|
514
|
-
"dependency_modules": dependency_modules
|
|
410
|
+
"dependency_modules": dependency_modules,
|
|
515
411
|
}
|
|
516
412
|
}
|
|
517
413
|
|
|
@@ -527,7 +423,7 @@ def _get_release_info(dpm_version: Optional[str], engine) -> Dict[str, Any]:
|
|
|
527
423
|
Returns:
|
|
528
424
|
dict: {'release': str, 'publication_date': str}
|
|
529
425
|
"""
|
|
530
|
-
from py_dpm.models import Release
|
|
426
|
+
from py_dpm.dpm.models import Release
|
|
531
427
|
from sqlalchemy.orm import sessionmaker
|
|
532
428
|
|
|
533
429
|
Session = sessionmaker(bind=engine)
|
|
@@ -537,39 +433,44 @@ def _get_release_info(dpm_version: Optional[str], engine) -> Dict[str, Any]:
|
|
|
537
433
|
if dpm_version:
|
|
538
434
|
# Query for specific version
|
|
539
435
|
version_float = float(dpm_version)
|
|
540
|
-
release =
|
|
541
|
-
|
|
542
|
-
|
|
436
|
+
release = (
|
|
437
|
+
session.query(Release)
|
|
438
|
+
.filter(Release.code == str(version_float))
|
|
439
|
+
.first()
|
|
440
|
+
)
|
|
543
441
|
|
|
544
442
|
if release:
|
|
545
443
|
return {
|
|
546
|
-
|
|
547
|
-
|
|
444
|
+
"release": str(release.code) if release.code else dpm_version,
|
|
445
|
+
"publication_date": (
|
|
446
|
+
release.date.strftime("%Y-%m-%d")
|
|
447
|
+
if release.date
|
|
448
|
+
else "2001-01-01"
|
|
449
|
+
),
|
|
548
450
|
}
|
|
549
451
|
|
|
550
452
|
# Fallback: get latest released version
|
|
551
|
-
release =
|
|
552
|
-
Release
|
|
553
|
-
|
|
453
|
+
release = (
|
|
454
|
+
session.query(Release)
|
|
455
|
+
.filter(Release.status == "released")
|
|
456
|
+
.order_by(Release.code.desc())
|
|
457
|
+
.first()
|
|
458
|
+
)
|
|
554
459
|
|
|
555
460
|
if release:
|
|
556
461
|
return {
|
|
557
|
-
|
|
558
|
-
|
|
462
|
+
"release": str(release.code) if release.code else "4.1",
|
|
463
|
+
"publication_date": (
|
|
464
|
+
release.date.strftime("%Y-%m-%d") if release.date else "2001-01-01"
|
|
465
|
+
),
|
|
559
466
|
}
|
|
560
467
|
|
|
561
468
|
# Final fallback
|
|
562
|
-
return {
|
|
563
|
-
'release': "4.1",
|
|
564
|
-
'publication_date': "2001-01-01"
|
|
565
|
-
}
|
|
469
|
+
return {"release": "4.1", "publication_date": "2001-01-01"}
|
|
566
470
|
|
|
567
471
|
except Exception:
|
|
568
472
|
# Fallback on any error
|
|
569
|
-
return {
|
|
570
|
-
'release': "4.1",
|
|
571
|
-
'publication_date': "2001-01-01"
|
|
572
|
-
}
|
|
473
|
+
return {"release": "4.1", "publication_date": "2001-01-01"}
|
|
573
474
|
finally:
|
|
574
475
|
session.close()
|
|
575
476
|
|
|
@@ -585,7 +486,7 @@ def _get_table_info(table_code: str, engine) -> Optional[Dict[str, Any]]:
|
|
|
585
486
|
Returns:
|
|
586
487
|
dict: {'table_vid': int, 'code': str} or None if not found
|
|
587
488
|
"""
|
|
588
|
-
from py_dpm.models import TableVersion
|
|
489
|
+
from py_dpm.dpm.models import TableVersion
|
|
589
490
|
from sqlalchemy.orm import sessionmaker
|
|
590
491
|
import re
|
|
591
492
|
|
|
@@ -594,41 +495,37 @@ def _get_table_info(table_code: str, engine) -> Optional[Dict[str, Any]]:
|
|
|
594
495
|
|
|
595
496
|
try:
|
|
596
497
|
# Try exact match first
|
|
597
|
-
table =
|
|
598
|
-
TableVersion.code == table_code
|
|
599
|
-
)
|
|
498
|
+
table = (
|
|
499
|
+
session.query(TableVersion).filter(TableVersion.code == table_code).first()
|
|
500
|
+
)
|
|
600
501
|
|
|
601
502
|
if table:
|
|
602
|
-
return {
|
|
603
|
-
'table_vid': table.tablevid,
|
|
604
|
-
'code': table.code
|
|
605
|
-
}
|
|
503
|
+
return {"table_vid": table.tablevid, "code": table.code}
|
|
606
504
|
|
|
607
505
|
# Handle precondition parser format: F_25_01 -> F_25.01
|
|
608
|
-
if re.match(r
|
|
609
|
-
parts = table_code.split(
|
|
506
|
+
if re.match(r"^[A-Z]_\d+_\d+", table_code):
|
|
507
|
+
parts = table_code.split("_", 2)
|
|
610
508
|
if len(parts) >= 3:
|
|
611
509
|
table_code_with_dot = f"{parts[0]}_{parts[1]}.{parts[2]}"
|
|
612
|
-
table =
|
|
613
|
-
TableVersion
|
|
614
|
-
|
|
510
|
+
table = (
|
|
511
|
+
session.query(TableVersion)
|
|
512
|
+
.filter(TableVersion.code == table_code_with_dot)
|
|
513
|
+
.first()
|
|
514
|
+
)
|
|
615
515
|
|
|
616
516
|
if table:
|
|
617
|
-
return {
|
|
618
|
-
'table_vid': table.tablevid,
|
|
619
|
-
'code': table.code
|
|
620
|
-
}
|
|
517
|
+
return {"table_vid": table.tablevid, "code": table.code}
|
|
621
518
|
|
|
622
519
|
# Try LIKE pattern as last resort (handles sub-tables like F_25.01.a)
|
|
623
|
-
table =
|
|
624
|
-
|
|
625
|
-
|
|
520
|
+
table = (
|
|
521
|
+
session.query(TableVersion)
|
|
522
|
+
.filter(TableVersion.code.like(f"{table_code}%"))
|
|
523
|
+
.order_by(TableVersion.code)
|
|
524
|
+
.first()
|
|
525
|
+
)
|
|
626
526
|
|
|
627
527
|
if table:
|
|
628
|
-
return {
|
|
629
|
-
'table_vid': table.tablevid,
|
|
630
|
-
'code': table.code
|
|
631
|
-
}
|
|
528
|
+
return {"table_vid": table.tablevid, "code": table.code}
|
|
632
529
|
|
|
633
530
|
return None
|
|
634
531
|
|
|
@@ -642,7 +539,7 @@ def _build_preconditions(
|
|
|
642
539
|
precondition: Optional[str],
|
|
643
540
|
context: Optional[Dict[str, Any]],
|
|
644
541
|
operation_code: str,
|
|
645
|
-
engine
|
|
542
|
+
engine,
|
|
646
543
|
) -> tuple:
|
|
647
544
|
"""
|
|
648
545
|
Build preconditions and precondition_variables sections.
|
|
@@ -666,30 +563,30 @@ def _build_preconditions(
|
|
|
666
563
|
|
|
667
564
|
if precondition:
|
|
668
565
|
# Extract variable code from precondition reference like {v_F_44_04}
|
|
669
|
-
match = re.match(r
|
|
566
|
+
match = re.match(r"\{v_([^}]+)\}", precondition)
|
|
670
567
|
if match:
|
|
671
568
|
table_code = match.group(1)
|
|
672
|
-
elif context and
|
|
673
|
-
table_code = context[
|
|
569
|
+
elif context and "table" in context:
|
|
570
|
+
table_code = context["table"]
|
|
674
571
|
|
|
675
572
|
if table_code:
|
|
676
573
|
# Query database for actual variable ID and version
|
|
677
574
|
table_info = _get_table_info(table_code, engine)
|
|
678
575
|
|
|
679
576
|
if table_info:
|
|
680
|
-
precondition_var_id = table_info[
|
|
681
|
-
version_id = table_info[
|
|
577
|
+
precondition_var_id = table_info["table_vid"]
|
|
578
|
+
version_id = table_info["table_vid"]
|
|
682
579
|
precondition_code = f"p_{precondition_var_id}"
|
|
683
580
|
|
|
684
581
|
preconditions[precondition_code] = {
|
|
685
582
|
"ast": {
|
|
686
583
|
"class_name": "PreconditionItem",
|
|
687
584
|
"variable_id": precondition_var_id,
|
|
688
|
-
"variable_code": table_code
|
|
585
|
+
"variable_code": table_code,
|
|
689
586
|
},
|
|
690
587
|
"affected_operations": [operation_code],
|
|
691
588
|
"version_id": version_id,
|
|
692
|
-
"code": precondition_code
|
|
589
|
+
"code": precondition_code,
|
|
693
590
|
}
|
|
694
591
|
|
|
695
592
|
precondition_variables[str(precondition_var_id)] = "b"
|
|
@@ -713,17 +610,17 @@ def _extract_variables_from_ast(ast_dict: Dict[str, Any]) -> tuple:
|
|
|
713
610
|
def extract_from_node(node):
|
|
714
611
|
if isinstance(node, dict):
|
|
715
612
|
# Check if this is a VarID node with data
|
|
716
|
-
if node.get(
|
|
717
|
-
table = node.get(
|
|
613
|
+
if node.get("class_name") == "VarID" and "data" in node:
|
|
614
|
+
table = node.get("table")
|
|
718
615
|
if table:
|
|
719
616
|
if table not in variables_by_table:
|
|
720
617
|
variables_by_table[table] = {}
|
|
721
618
|
|
|
722
619
|
# Extract variable IDs and data types from AST data array
|
|
723
|
-
for data_item in node[
|
|
724
|
-
if
|
|
725
|
-
var_id = str(int(data_item[
|
|
726
|
-
data_type = data_item.get(
|
|
620
|
+
for data_item in node["data"]:
|
|
621
|
+
if "datapoint" in data_item:
|
|
622
|
+
var_id = str(int(data_item["datapoint"]))
|
|
623
|
+
data_type = data_item.get("data_type", "e")
|
|
727
624
|
variables_by_table[table][var_id] = data_type
|
|
728
625
|
all_variables[var_id] = data_type
|
|
729
626
|
|
|
@@ -739,7 +636,9 @@ def _extract_variables_from_ast(ast_dict: Dict[str, Any]) -> tuple:
|
|
|
739
636
|
return all_variables, variables_by_table
|
|
740
637
|
|
|
741
638
|
|
|
742
|
-
def _add_coordinates_to_ast(
|
|
639
|
+
def _add_coordinates_to_ast(
|
|
640
|
+
ast_dict: Dict[str, Any], context: Optional[Dict[str, Any]]
|
|
641
|
+
) -> Dict[str, Any]:
|
|
743
642
|
"""
|
|
744
643
|
Add x/y/z coordinates to data entries in AST.
|
|
745
644
|
|
|
@@ -755,16 +654,16 @@ def _add_coordinates_to_ast(ast_dict: Dict[str, Any], context: Optional[Dict[str
|
|
|
755
654
|
def add_coords_to_node(node):
|
|
756
655
|
if isinstance(node, dict):
|
|
757
656
|
# Handle VarID nodes with data arrays
|
|
758
|
-
if node.get(
|
|
657
|
+
if node.get("class_name") == "VarID" and "data" in node:
|
|
759
658
|
# Get column information from context
|
|
760
659
|
cols = []
|
|
761
|
-
if context and
|
|
762
|
-
cols = context[
|
|
660
|
+
if context and "columns" in context and context["columns"]:
|
|
661
|
+
cols = context["columns"]
|
|
763
662
|
|
|
764
663
|
# Group data entries by row to assign coordinates correctly
|
|
765
664
|
entries_by_row = {}
|
|
766
|
-
for data_entry in node[
|
|
767
|
-
row_code = data_entry.get(
|
|
665
|
+
for data_entry in node["data"]:
|
|
666
|
+
row_code = data_entry.get("row", "")
|
|
768
667
|
if row_code not in entries_by_row:
|
|
769
668
|
entries_by_row[row_code] = []
|
|
770
669
|
entries_by_row[row_code].append(data_entry)
|
|
@@ -773,7 +672,7 @@ def _add_coordinates_to_ast(ast_dict: Dict[str, Any], context: Optional[Dict[str
|
|
|
773
672
|
rows = list(entries_by_row.keys())
|
|
774
673
|
for x_index, row_code in enumerate(rows, 1):
|
|
775
674
|
for data_entry in entries_by_row[row_code]:
|
|
776
|
-
column_code = data_entry.get(
|
|
675
|
+
column_code = data_entry.get("column", "")
|
|
777
676
|
|
|
778
677
|
# Find y coordinate based on column position in context
|
|
779
678
|
y_index = 1 # default
|
|
@@ -781,16 +680,19 @@ def _add_coordinates_to_ast(ast_dict: Dict[str, Any], context: Optional[Dict[str
|
|
|
781
680
|
y_index = cols.index(column_code) + 1
|
|
782
681
|
elif cols:
|
|
783
682
|
# Fallback to order in data
|
|
784
|
-
row_columns = [
|
|
683
|
+
row_columns = [
|
|
684
|
+
entry.get("column", "")
|
|
685
|
+
for entry in entries_by_row[row_code]
|
|
686
|
+
]
|
|
785
687
|
if column_code in row_columns:
|
|
786
688
|
y_index = row_columns.index(column_code) + 1
|
|
787
689
|
|
|
788
690
|
# Always add y coordinate
|
|
789
|
-
data_entry[
|
|
691
|
+
data_entry["y"] = y_index
|
|
790
692
|
|
|
791
693
|
# Add x coordinate only if there are multiple rows
|
|
792
694
|
if len(rows) > 1:
|
|
793
|
-
data_entry[
|
|
695
|
+
data_entry["x"] = x_index
|
|
794
696
|
|
|
795
697
|
# TODO: Add z coordinate for sheets when needed
|
|
796
698
|
|
|
@@ -805,4 +707,4 @@ def _add_coordinates_to_ast(ast_dict: Dict[str, Any], context: Optional[Dict[str
|
|
|
805
707
|
# Create a deep copy to avoid modifying the original
|
|
806
708
|
result = copy.deepcopy(ast_dict)
|
|
807
709
|
add_coords_to_node(result)
|
|
808
|
-
return result
|
|
710
|
+
return result
|