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.
Files changed (123) hide show
  1. py_dpm/__init__.py +1 -1
  2. py_dpm/api/__init__.py +58 -189
  3. py_dpm/api/dpm/__init__.py +20 -0
  4. py_dpm/api/{data_dictionary.py → dpm/data_dictionary.py} +903 -984
  5. py_dpm/api/dpm/explorer.py +236 -0
  6. py_dpm/api/dpm/hierarchical_queries.py +142 -0
  7. py_dpm/api/{migration.py → dpm/migration.py} +16 -19
  8. py_dpm/api/{operation_scopes.py → dpm/operation_scopes.py} +319 -267
  9. py_dpm/api/dpm_xl/__init__.py +25 -0
  10. py_dpm/api/{ast_generator.py → dpm_xl/ast_generator.py} +3 -3
  11. py_dpm/api/{complete_ast.py → dpm_xl/complete_ast.py} +186 -284
  12. py_dpm/api/dpm_xl/semantic.py +358 -0
  13. py_dpm/api/{syntax.py → dpm_xl/syntax.py} +6 -5
  14. py_dpm/api/explorer.py +4 -0
  15. py_dpm/api/semantic.py +30 -306
  16. py_dpm/cli/__init__.py +9 -0
  17. py_dpm/{client.py → cli/main.py} +12 -10
  18. py_dpm/dpm/__init__.py +11 -0
  19. py_dpm/{models.py → dpm/models.py} +112 -88
  20. py_dpm/dpm/queries/base.py +100 -0
  21. py_dpm/dpm/queries/basic_objects.py +33 -0
  22. py_dpm/dpm/queries/explorer_queries.py +352 -0
  23. py_dpm/dpm/queries/filters.py +139 -0
  24. py_dpm/dpm/queries/glossary.py +45 -0
  25. py_dpm/dpm/queries/hierarchical_queries.py +838 -0
  26. py_dpm/dpm/queries/tables.py +133 -0
  27. py_dpm/dpm/utils.py +356 -0
  28. py_dpm/dpm_xl/__init__.py +8 -0
  29. py_dpm/dpm_xl/ast/__init__.py +14 -0
  30. py_dpm/{AST/ASTConstructor.py → dpm_xl/ast/constructor.py} +6 -6
  31. py_dpm/{AST/MLGeneration.py → dpm_xl/ast/ml_generation.py} +137 -87
  32. py_dpm/{AST/ModuleAnalyzer.py → dpm_xl/ast/module_analyzer.py} +7 -7
  33. py_dpm/{AST/ModuleDependencies.py → dpm_xl/ast/module_dependencies.py} +56 -41
  34. py_dpm/{AST/ASTObjects.py → dpm_xl/ast/nodes.py} +1 -1
  35. py_dpm/{AST/check_operands.py → dpm_xl/ast/operands.py} +16 -13
  36. py_dpm/{AST/ASTTemplate.py → dpm_xl/ast/template.py} +2 -2
  37. py_dpm/{AST/WhereClauseChecker.py → dpm_xl/ast/where_clause.py} +2 -2
  38. py_dpm/dpm_xl/grammar/__init__.py +18 -0
  39. py_dpm/dpm_xl/operators/__init__.py +19 -0
  40. py_dpm/{Operators/AggregateOperators.py → dpm_xl/operators/aggregate.py} +7 -7
  41. py_dpm/{Operators/NumericOperators.py → dpm_xl/operators/arithmetic.py} +6 -6
  42. py_dpm/{Operators/Operator.py → dpm_xl/operators/base.py} +5 -5
  43. py_dpm/{Operators/BooleanOperators.py → dpm_xl/operators/boolean.py} +5 -5
  44. py_dpm/{Operators/ClauseOperators.py → dpm_xl/operators/clause.py} +8 -8
  45. py_dpm/{Operators/ComparisonOperators.py → dpm_xl/operators/comparison.py} +5 -5
  46. py_dpm/{Operators/ConditionalOperators.py → dpm_xl/operators/conditional.py} +7 -7
  47. py_dpm/{Operators/StringOperators.py → dpm_xl/operators/string.py} +5 -5
  48. py_dpm/{Operators/TimeOperators.py → dpm_xl/operators/time.py} +6 -6
  49. py_dpm/{semantics/SemanticAnalyzer.py → dpm_xl/semantic_analyzer.py} +168 -68
  50. py_dpm/{semantics/Symbols.py → dpm_xl/symbols.py} +3 -3
  51. py_dpm/dpm_xl/types/__init__.py +13 -0
  52. py_dpm/{DataTypes/TypePromotion.py → dpm_xl/types/promotion.py} +2 -2
  53. py_dpm/{DataTypes/ScalarTypes.py → dpm_xl/types/scalar.py} +2 -2
  54. py_dpm/dpm_xl/utils/__init__.py +14 -0
  55. py_dpm/{data_handlers.py → dpm_xl/utils/data_handlers.py} +2 -2
  56. py_dpm/{Utils → dpm_xl/utils}/operands_mapping.py +1 -1
  57. py_dpm/{Utils → dpm_xl/utils}/operator_mapping.py +8 -8
  58. py_dpm/{OperationScopes/OperationScopeService.py → dpm_xl/utils/scopes_calculator.py} +148 -58
  59. py_dpm/{Utils/ast_serialization.py → dpm_xl/utils/serialization.py} +3 -4
  60. py_dpm/dpm_xl/validation/__init__.py +12 -0
  61. py_dpm/{Utils/ValidationsGenerationUtils.py → dpm_xl/validation/generation_utils.py} +2 -3
  62. py_dpm/{ValidationsGeneration/PropertiesConstraintsProcessor.py → dpm_xl/validation/property_constraints.py} +56 -21
  63. py_dpm/{ValidationsGeneration/auxiliary_functions.py → dpm_xl/validation/utils.py} +2 -2
  64. py_dpm/{ValidationsGeneration/VariantsProcessor.py → dpm_xl/validation/variants.py} +149 -55
  65. py_dpm/exceptions/__init__.py +23 -0
  66. py_dpm/{Exceptions → exceptions}/exceptions.py +7 -2
  67. pydpm_xl-0.2.1.dist-info/METADATA +278 -0
  68. pydpm_xl-0.2.1.dist-info/RECORD +88 -0
  69. pydpm_xl-0.2.1.dist-info/entry_points.txt +2 -0
  70. py_dpm/Exceptions/__init__.py +0 -0
  71. py_dpm/OperationScopes/__init__.py +0 -0
  72. py_dpm/Operators/__init__.py +0 -0
  73. py_dpm/Utils/__init__.py +0 -0
  74. py_dpm/Utils/utils.py +0 -2
  75. py_dpm/ValidationsGeneration/Utils.py +0 -364
  76. py_dpm/ValidationsGeneration/__init__.py +0 -0
  77. py_dpm/api/data_dictionary_validation.py +0 -614
  78. py_dpm/db_utils.py +0 -221
  79. py_dpm/grammar/__init__.py +0 -0
  80. py_dpm/grammar/dist/__init__.py +0 -0
  81. py_dpm/grammar/dpm_xlLexer.g4 +0 -437
  82. py_dpm/grammar/dpm_xlParser.g4 +0 -263
  83. py_dpm/semantics/DAG/DAGAnalyzer.py +0 -158
  84. py_dpm/semantics/DAG/__init__.py +0 -0
  85. py_dpm/semantics/__init__.py +0 -0
  86. py_dpm/views/data_types.sql +0 -12
  87. py_dpm/views/datapoints.sql +0 -65
  88. py_dpm/views/hierarchy_operand_reference.sql +0 -11
  89. py_dpm/views/hierarchy_preconditions.sql +0 -13
  90. py_dpm/views/hierarchy_variables.sql +0 -26
  91. py_dpm/views/hierarchy_variables_context.sql +0 -14
  92. py_dpm/views/key_components.sql +0 -18
  93. py_dpm/views/module_from_table.sql +0 -11
  94. py_dpm/views/open_keys.sql +0 -13
  95. py_dpm/views/operation_info.sql +0 -27
  96. py_dpm/views/operation_list.sql +0 -18
  97. py_dpm/views/operations_versions_from_module_version.sql +0 -30
  98. py_dpm/views/precondition_info.sql +0 -17
  99. py_dpm/views/report_type_operand_reference_info.sql +0 -18
  100. py_dpm/views/subcategory_info.sql +0 -17
  101. py_dpm/views/table_info.sql +0 -19
  102. pydpm_xl-0.1.39rc32.dist-info/METADATA +0 -53
  103. pydpm_xl-0.1.39rc32.dist-info/RECORD +0 -96
  104. pydpm_xl-0.1.39rc32.dist-info/entry_points.txt +0 -2
  105. /py_dpm/{AST → cli/commands}/__init__.py +0 -0
  106. /py_dpm/{migration.py → dpm/migration.py} +0 -0
  107. /py_dpm/{AST/ASTVisitor.py → dpm_xl/ast/visitor.py} +0 -0
  108. /py_dpm/{DataTypes → dpm_xl/grammar/generated}/__init__.py +0 -0
  109. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.interp +0 -0
  110. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.py +0 -0
  111. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlLexer.tokens +0 -0
  112. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.interp +0 -0
  113. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.py +0 -0
  114. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParser.tokens +0 -0
  115. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParserListener.py +0 -0
  116. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/dpm_xlParserVisitor.py +0 -0
  117. /py_dpm/{grammar/dist → dpm_xl/grammar/generated}/listeners.py +0 -0
  118. /py_dpm/{DataTypes/TimeClasses.py → dpm_xl/types/time.py} +0 -0
  119. /py_dpm/{Utils → dpm_xl/utils}/tokens.py +0 -0
  120. /py_dpm/{Exceptions → exceptions}/messages.py +0 -0
  121. {pydpm_xl-0.1.39rc32.dist-info → pydpm_xl-0.2.1.dist-info}/WHEEL +0 -0
  122. {pydpm_xl-0.1.39rc32.dist-info → pydpm_xl-0.2.1.dist-info}/licenses/LICENSE +0 -0
  123. {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.Utils.ast_serialization import ASTToJSONVisitor
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 exact same process used to generate the reference
27
- JSON files in json_scripts/, ensuring complete data field population.
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
- 'success': bool,
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 API
48
- from py_dpm.db_utils import get_engine
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
- engine = get_engine(database_path=database_path, connection_url=connection_url)
48
+ get_engine(database_path=database_path, connection_url=connection_url)
54
49
  except Exception as e:
55
50
  return {
56
- 'success': False,
57
- 'ast': None,
58
- 'context': None,
59
- 'error': f'Database connection failed: {e}',
60
- 'data_populated': False
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 legacy API which does complete semantic validation
64
- # This is the same API used to generate the original JSON files
65
- api = API(database_path=database_path, connection_url=connection_url)
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
- # Force data population if semantic validation completed successfully
73
- if hasattr(api, 'AST') and api.AST and semantic_result:
74
- try:
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
- # Apply the data from operand checker to VarID nodes
101
- if hasattr(oc, 'data') and oc.data is not None:
102
-
103
- # Apply data to VarID nodes in the AST
104
- def apply_data_to_varids(node):
105
- if hasattr(node, '__class__') and node.__class__.__name__ == 'VarID':
106
- table = getattr(node, 'table', None)
107
- rows = getattr(node, 'rows', None)
108
- cols = getattr(node, 'cols', None)
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
- except Exception as e:
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
- 'success': True,
209
- 'ast': ast_dict,
210
- 'context': context_dict,
211
- 'error': None,
212
- 'data_populated': data_populated,
213
- 'semantic_result': semantic_result
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
- else:
217
- return {
218
- 'success': False,
219
- 'ast': None,
220
- 'context': None,
221
- 'error': 'Semantic validation did not generate AST',
222
- 'data_populated': False
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
- 'success': False,
228
- 'ast': None,
229
- 'context': None,
230
- 'error': f'API error: {str(e)}',
231
- 'data_populated': False
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('class_name') == 'VarID' and 'data' in ast_dict:
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['batch_index'] = i
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['ast'] if result['success'] else None
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['success']:
262
+ if not complete_result["success"]:
357
263
  return {
358
- 'success': False,
359
- 'enriched_ast': None,
360
- 'error': f"Failed to generate complete AST: {complete_result['error']}"
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['ast']
364
- context = complete_result.get('context') or table_context
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
- 'success': False,
387
- 'enriched_ast': None,
388
- 'error': f'Enrichment error: {str(e)}'
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.db_utils import get_engine, get_session
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['release'],
444
- "publication_date": release_info['publication_date']
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 'table' in context):
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 = session.query(Release).filter(
541
- Release.code == str(version_float)
542
- ).first()
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
- 'release': str(release.code) if release.code else dpm_version,
547
- 'publication_date': release.date.strftime("%Y-%m-%d") if release.date else "2001-01-01"
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 = session.query(Release).filter(
552
- Release.status == 'released'
553
- ).order_by(Release.code.desc()).first()
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
- 'release': str(release.code) if release.code else "4.1",
558
- 'publication_date': release.date.strftime("%Y-%m-%d") if release.date else "2001-01-01"
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 = session.query(TableVersion).filter(
598
- TableVersion.code == table_code
599
- ).first()
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'^[A-Z]_\d+_\d+', table_code):
609
- parts = table_code.split('_', 2)
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 = session.query(TableVersion).filter(
613
- TableVersion.code == table_code_with_dot
614
- ).first()
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 = session.query(TableVersion).filter(
624
- TableVersion.code.like(f"{table_code}%")
625
- ).order_by(TableVersion.code).first()
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'\{v_([^}]+)\}', precondition)
566
+ match = re.match(r"\{v_([^}]+)\}", precondition)
670
567
  if match:
671
568
  table_code = match.group(1)
672
- elif context and 'table' in context:
673
- table_code = context['table']
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['table_vid']
681
- version_id = table_info['table_vid']
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('class_name') == 'VarID' and 'data' in node:
717
- table = node.get('table')
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['data']:
724
- if 'datapoint' in data_item:
725
- var_id = str(int(data_item['datapoint']))
726
- data_type = data_item.get('data_type', 'e')
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(ast_dict: Dict[str, Any], context: Optional[Dict[str, Any]]) -> Dict[str, Any]:
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('class_name') == 'VarID' and 'data' in node:
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 'columns' in context and context['columns']:
762
- cols = context['columns']
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['data']:
767
- row_code = data_entry.get('row', '')
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('column', '')
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 = [entry.get('column', '') for entry in entries_by_row[row_code]]
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['y'] = y_index
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['x'] = x_index
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