pydpm_xl 0.2.0__tar.gz → 0.2.2__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 (103) hide show
  1. {pydpm_xl-0.2.0/pydpm_xl.egg-info → pydpm_xl-0.2.2}/PKG-INFO +1 -1
  2. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/__init__.py +1 -1
  3. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/dpm_xl/complete_ast.py +67 -189
  4. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/dpm_xl/semantic.py +4 -0
  5. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/cli/main.py +5 -3
  6. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm/queries/tables.py +24 -4
  7. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/ast/operands.py +3 -3
  8. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/utils/serialization.py +2 -3
  9. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2/pydpm_xl.egg-info}/PKG-INFO +1 -1
  10. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/pydpm_xl.egg-info/SOURCES.txt +1 -0
  11. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/pyproject.toml +2 -2
  12. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/tests/test_cli_semantic.py +58 -1
  13. pydpm_xl-0.2.2/tests/test_release_filters_semantic.py +108 -0
  14. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/LICENSE +0 -0
  15. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/README.md +0 -0
  16. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/__init__.py +0 -0
  17. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/dpm/__init__.py +0 -0
  18. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/dpm/data_dictionary.py +0 -0
  19. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/dpm/explorer.py +0 -0
  20. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/dpm/hierarchical_queries.py +0 -0
  21. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/dpm/migration.py +0 -0
  22. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/dpm/operation_scopes.py +0 -0
  23. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/dpm_xl/__init__.py +0 -0
  24. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/dpm_xl/ast_generator.py +0 -0
  25. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/dpm_xl/syntax.py +0 -0
  26. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/explorer.py +0 -0
  27. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/api/semantic.py +0 -0
  28. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/cli/__init__.py +0 -0
  29. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/cli/commands/__init__.py +0 -0
  30. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm/__init__.py +0 -0
  31. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm/migration.py +0 -0
  32. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm/models.py +0 -0
  33. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm/queries/base.py +0 -0
  34. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm/queries/basic_objects.py +0 -0
  35. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm/queries/explorer_queries.py +0 -0
  36. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm/queries/filters.py +0 -0
  37. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm/queries/glossary.py +0 -0
  38. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm/queries/hierarchical_queries.py +0 -0
  39. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm/utils.py +0 -0
  40. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/__init__.py +0 -0
  41. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/ast/__init__.py +0 -0
  42. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/ast/constructor.py +0 -0
  43. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/ast/ml_generation.py +0 -0
  44. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/ast/module_analyzer.py +0 -0
  45. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/ast/module_dependencies.py +0 -0
  46. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/ast/nodes.py +0 -0
  47. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/ast/template.py +0 -0
  48. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/ast/visitor.py +0 -0
  49. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/ast/where_clause.py +0 -0
  50. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/grammar/__init__.py +0 -0
  51. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/grammar/generated/__init__.py +0 -0
  52. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.interp +0 -0
  53. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.py +0 -0
  54. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.tokens +0 -0
  55. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.interp +0 -0
  56. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.py +0 -0
  57. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.tokens +0 -0
  58. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/grammar/generated/dpm_xlParserListener.py +0 -0
  59. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/grammar/generated/dpm_xlParserVisitor.py +0 -0
  60. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/grammar/generated/listeners.py +0 -0
  61. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/operators/__init__.py +0 -0
  62. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/operators/aggregate.py +0 -0
  63. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/operators/arithmetic.py +0 -0
  64. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/operators/base.py +0 -0
  65. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/operators/boolean.py +0 -0
  66. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/operators/clause.py +0 -0
  67. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/operators/comparison.py +0 -0
  68. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/operators/conditional.py +0 -0
  69. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/operators/string.py +0 -0
  70. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/operators/time.py +0 -0
  71. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/semantic_analyzer.py +0 -0
  72. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/symbols.py +0 -0
  73. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/types/__init__.py +0 -0
  74. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/types/promotion.py +0 -0
  75. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/types/scalar.py +0 -0
  76. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/types/time.py +0 -0
  77. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/utils/__init__.py +0 -0
  78. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/utils/data_handlers.py +0 -0
  79. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/utils/operands_mapping.py +0 -0
  80. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/utils/operator_mapping.py +0 -0
  81. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/utils/scopes_calculator.py +0 -0
  82. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/utils/tokens.py +0 -0
  83. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/validation/__init__.py +0 -0
  84. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/validation/generation_utils.py +0 -0
  85. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/validation/property_constraints.py +0 -0
  86. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/validation/utils.py +0 -0
  87. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/dpm_xl/validation/variants.py +0 -0
  88. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/exceptions/__init__.py +0 -0
  89. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/exceptions/exceptions.py +0 -0
  90. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/py_dpm/exceptions/messages.py +0 -0
  91. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/pydpm_xl.egg-info/dependency_links.txt +0 -0
  92. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/pydpm_xl.egg-info/entry_points.txt +0 -0
  93. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/pydpm_xl.egg-info/requires.txt +0 -0
  94. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/pydpm_xl.egg-info/top_level.txt +0 -0
  95. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/setup.cfg +0 -0
  96. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/tests/test_data_dictionary_releases.py +0 -0
  97. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/tests/test_db_connection_handling.py +0 -0
  98. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/tests/test_get_table_details.py +0 -0
  99. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/tests/test_get_tables_date_filter.py +0 -0
  100. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/tests/test_get_tables_release_code.py +0 -0
  101. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/tests/test_hierarchical_query.py +0 -0
  102. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/tests/test_query_refactor.py +0 -0
  103. {pydpm_xl-0.2.0 → pydpm_xl-0.2.2}/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.2
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.2"
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)
@@ -74,7 +74,12 @@ class TableQuery:
74
74
  subq.c.table_code.isnot(None)
75
75
  )
76
76
 
77
- q = filter_by_release(q, release_id, subq.c.start_release, subq.c.end_release)
77
+ q = filter_by_release(
78
+ q,
79
+ start_col=subq.c.start_release,
80
+ end_col=subq.c.end_release,
81
+ release_id=release_id,
82
+ )
78
83
  q = q.order_by(subq.c.table_code)
79
84
 
80
85
  return BaseQuery(session, q)
@@ -91,7 +96,12 @@ class TableQuery:
91
96
  subq.c.table_code == table_code, subq.c.row_code.isnot(None)
92
97
  )
93
98
 
94
- q = filter_by_release(q, release_id, subq.c.start_release, subq.c.end_release)
99
+ q = filter_by_release(
100
+ q,
101
+ start_col=subq.c.start_release,
102
+ end_col=subq.c.end_release,
103
+ release_id=release_id,
104
+ )
95
105
  q = q.order_by(subq.c.row_code)
96
106
 
97
107
  return BaseQuery(session, q)
@@ -108,7 +118,12 @@ class TableQuery:
108
118
  subq.c.table_code == table_code, subq.c.column_code.isnot(None)
109
119
  )
110
120
 
111
- q = filter_by_release(q, release_id, subq.c.start_release, subq.c.end_release)
121
+ q = filter_by_release(
122
+ q,
123
+ start_col=subq.c.start_release,
124
+ end_col=subq.c.end_release,
125
+ release_id=release_id,
126
+ )
112
127
  q = q.order_by(subq.c.column_code)
113
128
 
114
129
  return BaseQuery(session, q)
@@ -127,7 +142,12 @@ class TableQuery:
127
142
  subq.c.sheet_code != "",
128
143
  )
129
144
 
130
- q = filter_by_release(q, release_id, subq.c.start_release, subq.c.end_release)
145
+ q = filter_by_release(
146
+ q,
147
+ start_col=subq.c.start_release,
148
+ end_col=subq.c.end_release,
149
+ release_id=release_id,
150
+ )
131
151
  q = q.order_by(subq.c.sheet_code)
132
152
 
133
153
  return BaseQuery(session, q)
@@ -208,9 +208,9 @@ class OperandsChecking(ASTTemplate, ABC):
208
208
  # Apply release filter
209
209
  query = filter_by_release(
210
210
  query,
211
- self.release_id,
212
- TableVersion.startreleaseid,
213
- TableVersion.endreleaseid,
211
+ start_col=TableVersion.startreleaseid,
212
+ end_col=TableVersion.endreleaseid,
213
+ release_id=self.release_id,
214
214
  )
215
215
 
216
216
  # Execute query and convert to DataFrame
@@ -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.2
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
@@ -97,4 +97,5 @@ tests/test_get_tables_date_filter.py
97
97
  tests/test_get_tables_release_code.py
98
98
  tests/test_hierarchical_query.py
99
99
  tests/test_query_refactor.py
100
+ tests/test_release_filters_semantic.py
100
101
  tests/test_semantic_release.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pydpm_xl"
3
- version = "0.2.0"
3
+ version = "0.2.2"
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.2"
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
@@ -0,0 +1,108 @@
1
+ import pandas as pd
2
+ from sqlalchemy import create_engine
3
+ from sqlalchemy.orm import sessionmaker
4
+
5
+ from py_dpm.dpm.models import Base, TableVersion
6
+ from py_dpm.dpm.queries.filters import filter_by_release
7
+ from py_dpm.dpm_xl.ast import operands as operands_module
8
+
9
+
10
+ def _make_session():
11
+ """Create a lightweight in-memory SQLAlchemy session for query compilation."""
12
+ engine = create_engine("sqlite:///:memory:")
13
+ Base.metadata.bind = engine
14
+ Session = sessionmaker(bind=engine)
15
+ return Session()
16
+
17
+
18
+ def test_filter_by_release_uses_is_null_for_end_release():
19
+ """
20
+ Ensure filter_by_release uses SQLAlchemy .is_(None) semantics for end_col,
21
+ resulting in an 'IS NULL' predicate rather than '= NULL' in the SQL.
22
+
23
+ This is important for PostgreSQL, which is strict about boolean expressions
24
+ and NULL comparison semantics.
25
+ """
26
+ session = _make_session()
27
+
28
+ query = session.query(TableVersion)
29
+ filtered = filter_by_release(
30
+ query,
31
+ start_col=TableVersion.startreleaseid,
32
+ end_col=TableVersion.endreleaseid,
33
+ release_id=5,
34
+ )
35
+
36
+ sql = str(
37
+ filtered.statement.compile(
38
+ dialect=session.get_bind().dialect, compile_kwargs={"literal_binds": True}
39
+ )
40
+ ).upper()
41
+
42
+ # Ensure NULL handling uses IS NULL rather than = NULL
43
+ assert "IS NULL" in sql
44
+ assert "= NULL" not in sql
45
+
46
+
47
+ def test_operands_check_headers_calls_filter_by_release_with_correct_args(monkeypatch):
48
+ """
49
+ Verify that OperandsChecking.check_headers wires filter_by_release correctly:
50
+ - start_col is TableVersion.startreleaseid
51
+ - end_col is TableVersion.endreleaseid
52
+ - release_id matches the instance's release_id
53
+ """
54
+ called = {}
55
+
56
+ def fake_filter_by_release(query, start_col, end_col, release_id=None, release_code=None):
57
+ called["query"] = query
58
+ called["start_col"] = start_col
59
+ called["end_col"] = end_col
60
+ called["release_id"] = release_id
61
+ called["release_code"] = release_code
62
+ return query
63
+
64
+ monkeypatch.setattr(operands_module, "filter_by_release", fake_filter_by_release)
65
+
66
+ # Stub out the pandas helpers used inside check_headers so that no real DB
67
+ # access is attempted.
68
+ import py_dpm.dpm.models as models
69
+
70
+ monkeypatch.setattr(
71
+ models,
72
+ "_compile_query_for_pandas",
73
+ lambda stmt, session: stmt,
74
+ )
75
+
76
+ def fake_read_sql(sql, session):
77
+ # Return an empty DataFrame with the expected columns so that
78
+ # check_headers completes without touching self.operands.
79
+ return pd.DataFrame(
80
+ columns=[
81
+ "Code",
82
+ "StartReleaseID",
83
+ "EndReleaseID",
84
+ "Direction",
85
+ "HasOpenRows",
86
+ "HasOpenColumns",
87
+ "HasOpenSheets",
88
+ ]
89
+ )
90
+
91
+ monkeypatch.setattr(models, "_read_sql_with_connection", fake_read_sql)
92
+
93
+ session = _make_session()
94
+
95
+ # Create a minimal OperandsChecking instance without running its __init__,
96
+ # since that would require a fully-populated AST and database.
97
+ oc = object.__new__(operands_module.OperandsChecking)
98
+ oc.session = session
99
+ oc.release_id = 7
100
+ oc.tables = {"DummyTable": {}} # Only keys are needed by check_headers
101
+
102
+ operands_module.OperandsChecking.check_headers(oc)
103
+
104
+ assert called["start_col"] is TableVersion.startreleaseid
105
+ assert called["end_col"] is TableVersion.endreleaseid
106
+ assert called["release_id"] == 7
107
+ assert called["release_code"] is None
108
+
File without changes
File without changes
File without changes
File without changes
File without changes