pydpm_xl 0.2.0__tar.gz → 0.2.1__tar.gz

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 (102) hide show
  1. {pydpm_xl-0.2.0/pydpm_xl.egg-info → pydpm_xl-0.2.1}/PKG-INFO +1 -1
  2. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/__init__.py +1 -1
  3. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/dpm_xl/complete_ast.py +67 -189
  4. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/dpm_xl/semantic.py +4 -0
  5. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/cli/main.py +5 -3
  6. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/utils/serialization.py +2 -3
  7. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1/pydpm_xl.egg-info}/PKG-INFO +1 -1
  8. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/pyproject.toml +2 -2
  9. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/tests/test_cli_semantic.py +58 -1
  10. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/LICENSE +0 -0
  11. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/README.md +0 -0
  12. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/__init__.py +0 -0
  13. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/dpm/__init__.py +0 -0
  14. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/dpm/data_dictionary.py +0 -0
  15. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/dpm/explorer.py +0 -0
  16. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/dpm/hierarchical_queries.py +0 -0
  17. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/dpm/migration.py +0 -0
  18. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/dpm/operation_scopes.py +0 -0
  19. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/dpm_xl/__init__.py +0 -0
  20. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/dpm_xl/ast_generator.py +0 -0
  21. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/dpm_xl/syntax.py +0 -0
  22. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/explorer.py +0 -0
  23. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/api/semantic.py +0 -0
  24. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/cli/__init__.py +0 -0
  25. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/cli/commands/__init__.py +0 -0
  26. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm/__init__.py +0 -0
  27. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm/migration.py +0 -0
  28. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm/models.py +0 -0
  29. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm/queries/base.py +0 -0
  30. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm/queries/basic_objects.py +0 -0
  31. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm/queries/explorer_queries.py +0 -0
  32. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm/queries/filters.py +0 -0
  33. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm/queries/glossary.py +0 -0
  34. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm/queries/hierarchical_queries.py +0 -0
  35. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm/queries/tables.py +0 -0
  36. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm/utils.py +0 -0
  37. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/__init__.py +0 -0
  38. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/ast/__init__.py +0 -0
  39. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/ast/constructor.py +0 -0
  40. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/ast/ml_generation.py +0 -0
  41. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/ast/module_analyzer.py +0 -0
  42. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/ast/module_dependencies.py +0 -0
  43. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/ast/nodes.py +0 -0
  44. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/ast/operands.py +0 -0
  45. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/ast/template.py +0 -0
  46. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/ast/visitor.py +0 -0
  47. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/ast/where_clause.py +0 -0
  48. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/grammar/__init__.py +0 -0
  49. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/grammar/generated/__init__.py +0 -0
  50. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.interp +0 -0
  51. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.py +0 -0
  52. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.tokens +0 -0
  53. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.interp +0 -0
  54. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.py +0 -0
  55. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.tokens +0 -0
  56. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/grammar/generated/dpm_xlParserListener.py +0 -0
  57. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/grammar/generated/dpm_xlParserVisitor.py +0 -0
  58. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/grammar/generated/listeners.py +0 -0
  59. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/operators/__init__.py +0 -0
  60. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/operators/aggregate.py +0 -0
  61. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/operators/arithmetic.py +0 -0
  62. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/operators/base.py +0 -0
  63. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/operators/boolean.py +0 -0
  64. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/operators/clause.py +0 -0
  65. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/operators/comparison.py +0 -0
  66. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/operators/conditional.py +0 -0
  67. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/operators/string.py +0 -0
  68. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/operators/time.py +0 -0
  69. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/semantic_analyzer.py +0 -0
  70. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/symbols.py +0 -0
  71. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/types/__init__.py +0 -0
  72. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/types/promotion.py +0 -0
  73. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/types/scalar.py +0 -0
  74. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/types/time.py +0 -0
  75. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/utils/__init__.py +0 -0
  76. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/utils/data_handlers.py +0 -0
  77. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/utils/operands_mapping.py +0 -0
  78. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/utils/operator_mapping.py +0 -0
  79. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/utils/scopes_calculator.py +0 -0
  80. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/utils/tokens.py +0 -0
  81. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/validation/__init__.py +0 -0
  82. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/validation/generation_utils.py +0 -0
  83. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/validation/property_constraints.py +0 -0
  84. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/validation/utils.py +0 -0
  85. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/dpm_xl/validation/variants.py +0 -0
  86. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/exceptions/__init__.py +0 -0
  87. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/exceptions/exceptions.py +0 -0
  88. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/py_dpm/exceptions/messages.py +0 -0
  89. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/pydpm_xl.egg-info/SOURCES.txt +0 -0
  90. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/pydpm_xl.egg-info/dependency_links.txt +0 -0
  91. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/pydpm_xl.egg-info/entry_points.txt +0 -0
  92. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/pydpm_xl.egg-info/requires.txt +0 -0
  93. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/pydpm_xl.egg-info/top_level.txt +0 -0
  94. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/setup.cfg +0 -0
  95. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/tests/test_data_dictionary_releases.py +0 -0
  96. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/tests/test_db_connection_handling.py +0 -0
  97. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/tests/test_get_table_details.py +0 -0
  98. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/tests/test_get_tables_date_filter.py +0 -0
  99. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/tests/test_get_tables_release_code.py +0 -0
  100. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/tests/test_hierarchical_query.py +0 -0
  101. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/tests/test_query_refactor.py +0 -0
  102. {pydpm_xl-0.2.0 → pydpm_xl-0.2.1}/tests/test_semantic_release.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydpm_xl
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Python library for DPM-XL data processing and analysis
5
5
  Author-email: "MeaningfulData S.L." <info@meaningfuldata.eu>
6
6
  License: GPL-3.0-or-later
@@ -41,7 +41,7 @@ Available packages:
41
41
  - pydpm.api: Main APIs for migration, syntax, and semantic analysis
42
42
  """
43
43
 
44
- __version__ = "0.2.0"
44
+ __version__ = "0.2.1"
45
45
  __author__ = "MeaningfulData S.L."
46
46
  __email__ = "info@meaningfuldata.eu"
47
47
  __license__ = "GPL-3.0-or-later"
@@ -10,7 +10,7 @@ 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
13
+ from typing import Dict, Any, Any, Optional
14
14
  from py_dpm.dpm_xl.utils.serialization import ASTToJSONVisitor
15
15
 
16
16
 
@@ -23,8 +23,8 @@ def generate_complete_ast(
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,25 +34,18 @@ 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
42
+ from py_dpm.api.dpm_xl.semantic import SemanticAPI
48
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(
54
- database_path=database_path, connection_url=connection_url
55
- )
48
+ get_engine(database_path=database_path, connection_url=connection_url)
56
49
  except Exception as e:
57
50
  return {
58
51
  "success": False,
@@ -62,194 +55,79 @@ def generate_complete_ast(
62
55
  "data_populated": False,
63
56
  }
64
57
 
65
- # Use the legacy API which does complete semantic validation
66
- # This is the same API used to generate the original JSON files
67
- api = API(database_path=database_path, connection_url=connection_url)
68
-
69
- # Perform complete semantic validation with operand checking
70
- # This should populate all data fields on VarID nodes
71
- semantic_result = api.semantic_validation(expression, release_id=release_id)
72
-
73
- # Force data population if semantic validation completed successfully
74
- if hasattr(api, "AST") and api.AST and semantic_result:
75
- try:
76
- from py_dpm.dpm_xl.ast.operands import OperandsChecking
77
- from py_dpm.dpm.utils import get_session
78
-
79
- session = get_session()
80
-
81
- # Extract the expression AST
82
- def get_inner_ast(ast_obj):
83
- if hasattr(ast_obj, "children") and len(ast_obj.children) > 0:
84
- child = ast_obj.children[0]
85
- if hasattr(child, "expression"):
86
- return child.expression
87
- else:
88
- return child
89
- return ast_obj
90
-
91
- inner_ast = get_inner_ast(api.AST)
92
-
93
- # Run operand checking to populate data fields
94
- oc = OperandsChecking(
95
- session=session,
96
- expression=expression,
97
- ast=inner_ast,
98
- release_id=release_id,
99
- )
100
-
101
- # Apply the data from operand checker to VarID nodes
102
- if hasattr(oc, "data") and oc.data is not None:
103
-
104
- # Apply data to VarID nodes in the AST
105
- def apply_data_to_varids(node):
106
- if (
107
- hasattr(node, "__class__")
108
- and node.__class__.__name__ == "VarID"
109
- ):
110
- table = getattr(node, "table", None)
111
- rows = getattr(node, "rows", None)
112
- cols = getattr(node, "cols", None)
113
-
114
- if table and table in oc.operands:
115
- # Filter data for this specific VarID
116
- # Start with table filter
117
- filter_mask = oc.data["table_code"] == table
118
-
119
- # Add row filter only if rows is not None and doesn't contain wildcards
120
- # IMPORTANT: If rows contains '*', include all rows (don't filter)
121
- if rows is not None and "*" not in rows:
122
- filter_mask = filter_mask & (
123
- oc.data["row_code"].isin(rows)
124
- )
125
-
126
- # Add column filter only if cols is not None and doesn't contain wildcards
127
- # IMPORTANT: If cols contains '*', include all columns (don't filter)
128
- if cols is not None and "*" not in cols:
129
- filter_mask = filter_mask & (
130
- oc.data["column_code"].isin(cols)
131
- )
132
-
133
- filtered_data = oc.data[filter_mask]
134
-
135
- if not filtered_data.empty:
136
- # IMPORTANT: Remove wildcard entries (NULL column/row/sheet codes)
137
- # when specific entries exist for the same dimension
138
- # The database contains both wildcard entries (column_code=NULL for c*)
139
- # and specific entries (column_code='0010'). When we query with wildcards,
140
- # we want only the specific entries.
141
-
142
- # Remove rows where column_code is NULL if there are non-NULL column_code entries
143
- if filtered_data["column_code"].notna().any():
144
- filtered_data = filtered_data[
145
- filtered_data["column_code"].notna()
146
- ]
147
-
148
- # Remove rows where row_code is NULL if there are non-NULL row_code entries
149
- if filtered_data["row_code"].notna().any():
150
- filtered_data = filtered_data[
151
- filtered_data["row_code"].notna()
152
- ]
153
-
154
- # Remove rows where sheet_code is NULL if there are non-NULL sheet_code entries
155
- if filtered_data["sheet_code"].notna().any():
156
- filtered_data = filtered_data[
157
- filtered_data["sheet_code"].notna()
158
- ]
159
-
160
- # IMPORTANT: After filtering, remove any remaining duplicates
161
- # based on (row_code, column_code, sheet_code) combination
162
- filtered_data = filtered_data.drop_duplicates(
163
- subset=[
164
- "row_code",
165
- "column_code",
166
- "sheet_code",
167
- ],
168
- keep="first",
169
- )
170
-
171
- # Set the data attribute on the VarID node
172
- if not filtered_data.empty:
173
- node.data = filtered_data
174
-
175
- # Recursively apply to child nodes
176
- for attr_name in [
177
- "children",
178
- "left",
179
- "right",
180
- "operand",
181
- "operands",
182
- "expression",
183
- "condition",
184
- "then_expr",
185
- "else_expr",
186
- ]:
187
- if hasattr(node, attr_name):
188
- attr_value = getattr(node, attr_name)
189
- if isinstance(attr_value, list):
190
- for item in attr_value:
191
- if hasattr(item, "__class__"):
192
- apply_data_to_varids(item)
193
- elif attr_value and hasattr(attr_value, "__class__"):
194
- apply_data_to_varids(attr_value)
195
-
196
- # Apply data to all VarID nodes in the AST
197
- apply_data_to_varids(inner_ast)
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
+ )
198
62
 
199
- except Exception as e:
200
- # Silently continue if data population fails
201
- pass
202
-
203
- if hasattr(api, "AST") and api.AST is not None:
204
- # Extract components exactly like batch_validator does
205
- def extract_components(ast_obj):
206
- if hasattr(ast_obj, "children") and len(ast_obj.children) > 0:
207
- child = ast_obj.children[0]
208
- if hasattr(child, "expression"):
209
- return child.expression, child.partial_selection
210
- else:
211
- return child, None
212
- return ast_obj, None
213
-
214
- actual_ast, context = extract_components(api.AST)
215
-
216
- # Convert to JSON exactly like batch_validator does
217
- visitor = ASTToJSONVisitor(context)
218
- ast_dict = visitor.visit(actual_ast)
219
-
220
- # Check if data fields were populated
221
- data_populated = _check_data_fields_populated(ast_dict)
222
-
223
- # Serialize context
224
- context_dict = None
225
- if context:
226
- context_dict = {
227
- "table": getattr(context, "table", None),
228
- "rows": getattr(context, "rows", None),
229
- "columns": getattr(context, "cols", None),
230
- "sheets": getattr(context, "sheets", None),
231
- "default": getattr(context, "default", None),
232
- "interval": getattr(context, "interval", None),
233
- }
63
+ semantic_result = semantic_api.validate_expression(
64
+ expression, release_id=release_id
65
+ )
234
66
 
67
+ # If semantic validation failed, return structured error
68
+ if not semantic_result.is_valid:
235
69
  return {
236
- "success": True,
237
- "ast": ast_dict,
238
- "context": context_dict,
239
- "error": None,
240
- "data_populated": data_populated,
70
+ "success": False,
71
+ "ast": None,
72
+ "context": None,
73
+ "error": semantic_result.error_message,
74
+ "data_populated": False,
241
75
  "semantic_result": semantic_result,
242
76
  }
243
77
 
244
- else:
78
+ ast_root = getattr(semantic_api, "ast", None)
79
+
80
+ if ast_root is None:
245
81
  return {
246
82
  "success": False,
247
83
  "ast": None,
248
84
  "context": None,
249
85
  "error": "Semantic validation did not generate AST",
250
86
  "data_populated": False,
87
+ "semantic_result": semantic_result,
88
+ }
89
+
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),
251
120
  }
252
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
+
253
131
  except Exception as e:
254
132
  return {
255
133
  "success": False,
@@ -56,6 +56,8 @@ class SemanticAPI:
56
56
  """
57
57
  self.database_path = database_path
58
58
  self.connection_url = connection_url
59
+ # Store last parsed AST for consumers that need it (e.g. complete AST generation)
60
+ self.ast = None
59
61
 
60
62
  if connection_url:
61
63
  # Create isolated engine and session for the provided connection URL
@@ -141,6 +143,8 @@ class SemanticAPI:
141
143
 
142
144
  # Generate AST
143
145
  ast = self.visitor.visit(parse_tree)
146
+ # Expose AST on the instance for downstream consumers
147
+ self.ast = ast
144
148
 
145
149
  # Perform semantic analysis
146
150
  oc = OperandsChecking(
@@ -6,7 +6,7 @@ import os
6
6
  import sys
7
7
  import pandas as pd
8
8
 
9
- from py_dpm.api import SemanticAPI
9
+ from py_dpm.api import SemanticAPI, SyntaxAPI
10
10
  from py_dpm.api.dpm_xl.semantic import SemanticValidationResult
11
11
  from py_dpm.api.dpm.operation_scopes import OperationScopesAPI
12
12
  from py_dpm.dpm.migration import run_migration
@@ -147,9 +147,11 @@ def syntax(expression: str):
147
147
  """Perform syntactic analysis on a DPM expression."""
148
148
 
149
149
  status = 0
150
- api = API()
150
+ api = SyntaxAPI()
151
151
  try:
152
- api.syntax_validation(expression)
152
+ result = api.validate_expression(expression)
153
+ if not result.is_valid:
154
+ raise SyntaxError(result.error_message or "Syntax errors detected")
153
155
  message_formatted = Text("Syntax OK", style="bold green")
154
156
  except SyntaxError as e:
155
157
  message = str(e)
@@ -542,10 +542,9 @@ class ASTToJSONVisitor(NodeVisitor):
542
542
 
543
543
  return result
544
544
 
545
-
546
545
  # Original serialization functions (kept for backward compatibility)
547
546
  import json
548
- from py_dpm.dpm_xl.ast import nodes
547
+ from py_dpm.dpm_xl.ast import nodes as ASTObjects
549
548
 
550
549
 
551
550
  def expand_with_expression(node):
@@ -759,4 +758,4 @@ def ast_from_json_string(json_str):
759
758
  AST object instance
760
759
  """
761
760
  data = json.loads(json_str)
762
- return deserialize_ast(data)
761
+ return deserialize_ast(data)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydpm_xl
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Python library for DPM-XL data processing and analysis
5
5
  Author-email: "MeaningfulData S.L." <info@meaningfuldata.eu>
6
6
  License: GPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pydpm_xl"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Python library for DPM-XL data processing and analysis"
5
5
  authors = [
6
6
  {name = "MeaningfulData S.L.", email = "info@meaningfuldata.eu"}
@@ -52,7 +52,7 @@ exclude = []
52
52
 
53
53
  [tool.poetry]
54
54
  name = "pydpm_xl"
55
- version = "0.2.0"
55
+ version = "0.2.1"
56
56
  description = "Python library for DPM-XL data processing and analysis"
57
57
  authors = ["MeaningfulData S.L. <info@meaningfuldata.eu>"]
58
58
  readme = "README.md"
@@ -1,8 +1,9 @@
1
1
  import pytest
2
2
  from click.testing import CliRunner
3
- from py_dpm.cli.main import main, semantic
3
+ from py_dpm.cli.main import main, semantic, syntax
4
4
  from unittest.mock import patch, MagicMock, MagicMock
5
5
  from py_dpm.api.dpm_xl.semantic import SemanticValidationResult
6
+ from py_dpm.api.dpm_xl.syntax import SyntaxValidationResult
6
7
 
7
8
 
8
9
  @pytest.fixture
@@ -191,3 +192,59 @@ def test_semantic_conflict_flags(runner):
191
192
 
192
193
  assert result.exit_code != 0
193
194
  assert "Cannot provide both --release-id and --dpm-version" in result.output
195
+
196
+
197
+ def test_syntax_valid_expression(runner):
198
+ """Test syntax command with a valid expression."""
199
+ expression = "{tC_01.00, r0100, c0010}"
200
+
201
+ with patch("py_dpm.cli.main.SyntaxAPI") as MockAPI:
202
+ mock_api_instance = MockAPI.return_value
203
+ mock_api_instance.validate_expression.return_value = SyntaxValidationResult(
204
+ is_valid=True,
205
+ error_message=None,
206
+ expression=expression,
207
+ )
208
+
209
+ result = runner.invoke(main, ["syntax", expression])
210
+
211
+ MockAPI.assert_called_once()
212
+ mock_api_instance.validate_expression.assert_called_once_with(expression)
213
+
214
+ assert result.exit_code == 0
215
+ assert "Syntax OK" in result.output
216
+
217
+
218
+ def test_syntax_invalid_expression(runner):
219
+ """Test syntax command with an invalid expression."""
220
+ expression = "invalid_expression"
221
+
222
+ with patch("py_dpm.cli.main.SyntaxAPI") as MockAPI:
223
+ mock_api_instance = MockAPI.return_value
224
+ mock_api_instance.validate_expression.return_value = SyntaxValidationResult(
225
+ is_valid=False,
226
+ error_message="Syntax errors detected",
227
+ expression=expression,
228
+ )
229
+
230
+ result = runner.invoke(main, ["syntax", expression])
231
+
232
+ MockAPI.assert_called_once()
233
+ mock_api_instance.validate_expression.assert_called_once_with(expression)
234
+
235
+ assert result.exit_code == 0
236
+ assert "Syntax Error: Syntax errors detected" in result.output
237
+
238
+
239
+ def test_syntax_unexpected_exception(runner):
240
+ """Test syntax command when an unexpected exception occurs."""
241
+ expression = "{tC_01.00}"
242
+
243
+ with patch("py_dpm.cli.main.SyntaxAPI") as MockAPI:
244
+ mock_api_instance = MockAPI.return_value
245
+ mock_api_instance.validate_expression.side_effect = Exception("Unexpected error")
246
+
247
+ result = runner.invoke(main, ["syntax", expression])
248
+
249
+ assert result.exit_code == 0
250
+ assert "Unexpected Error: Unexpected error" in result.output
File without changes
File without changes
File without changes
File without changes
File without changes