pydpm_xl 0.2.8__tar.gz → 0.2.10__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 (101) hide show
  1. {pydpm_xl-0.2.8/pydpm_xl.egg-info → pydpm_xl-0.2.10}/PKG-INFO +12 -13
  2. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/README.md +11 -12
  3. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/__init__.py +1 -1
  4. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/__init__.py +3 -9
  5. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/dpm_xl/__init__.py +3 -12
  6. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/dpm_xl/ast_generator.py +92 -47
  7. pydpm_xl-0.2.10/py_dpm/api/dpm_xl/complete_ast.py +95 -0
  8. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm/migration.py +40 -45
  9. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm/models.py +22 -2
  10. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/ast/operands.py +41 -32
  11. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/utils/serialization.py +15 -0
  12. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10/pydpm_xl.egg-info}/PKG-INFO +12 -13
  13. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/pydpm_xl.egg-info/SOURCES.txt +1 -0
  14. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/pyproject.toml +2 -2
  15. pydpm_xl-0.2.10/tests/test_migration_type_inference.py +210 -0
  16. pydpm_xl-0.2.8/py_dpm/api/dpm_xl/complete_ast.py +0 -208
  17. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/LICENSE +0 -0
  18. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/dpm/__init__.py +0 -0
  19. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/dpm/data_dictionary.py +0 -0
  20. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/dpm/explorer.py +0 -0
  21. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/dpm/hierarchical_queries.py +0 -0
  22. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/dpm/instance.py +0 -0
  23. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/dpm/migration.py +0 -0
  24. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/dpm_xl/operation_scopes.py +0 -0
  25. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/dpm_xl/semantic.py +0 -0
  26. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/api/dpm_xl/syntax.py +0 -0
  27. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/cli/__init__.py +0 -0
  28. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/cli/commands/__init__.py +0 -0
  29. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/cli/main.py +0 -0
  30. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm/__init__.py +0 -0
  31. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm/queries/base.py +0 -0
  32. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm/queries/basic_objects.py +0 -0
  33. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm/queries/explorer_queries.py +0 -0
  34. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm/queries/filters.py +0 -0
  35. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm/queries/glossary.py +0 -0
  36. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm/queries/hierarchical_queries.py +0 -0
  37. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm/queries/tables.py +0 -0
  38. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm/utils.py +0 -0
  39. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/__init__.py +0 -0
  40. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/ast/__init__.py +0 -0
  41. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/ast/constructor.py +0 -0
  42. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/ast/ml_generation.py +0 -0
  43. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/ast/module_analyzer.py +0 -0
  44. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/ast/module_dependencies.py +0 -0
  45. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/ast/nodes.py +0 -0
  46. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/ast/template.py +0 -0
  47. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/ast/visitor.py +0 -0
  48. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/ast/where_clause.py +0 -0
  49. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/grammar/__init__.py +0 -0
  50. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/grammar/generated/__init__.py +0 -0
  51. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.interp +0 -0
  52. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.py +0 -0
  53. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.tokens +0 -0
  54. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.interp +0 -0
  55. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.py +0 -0
  56. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.tokens +0 -0
  57. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/grammar/generated/dpm_xlParserListener.py +0 -0
  58. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/grammar/generated/dpm_xlParserVisitor.py +0 -0
  59. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/grammar/generated/listeners.py +0 -0
  60. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/operators/__init__.py +0 -0
  61. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/operators/aggregate.py +0 -0
  62. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/operators/arithmetic.py +0 -0
  63. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/operators/base.py +0 -0
  64. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/operators/boolean.py +0 -0
  65. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/operators/clause.py +0 -0
  66. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/operators/comparison.py +0 -0
  67. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/operators/conditional.py +0 -0
  68. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/operators/string.py +0 -0
  69. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/operators/time.py +0 -0
  70. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/semantic_analyzer.py +0 -0
  71. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/symbols.py +0 -0
  72. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/types/__init__.py +0 -0
  73. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/types/promotion.py +0 -0
  74. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/types/scalar.py +0 -0
  75. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/types/time.py +0 -0
  76. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/utils/__init__.py +0 -0
  77. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/utils/data_handlers.py +0 -0
  78. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/utils/operands_mapping.py +0 -0
  79. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/utils/operator_mapping.py +0 -0
  80. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/utils/scopes_calculator.py +0 -0
  81. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/dpm_xl/utils/tokens.py +0 -0
  82. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/exceptions/__init__.py +0 -0
  83. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/exceptions/exceptions.py +0 -0
  84. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/exceptions/messages.py +0 -0
  85. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/instance/__init__.py +0 -0
  86. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/py_dpm/instance/instance.py +0 -0
  87. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/pydpm_xl.egg-info/dependency_links.txt +0 -0
  88. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/pydpm_xl.egg-info/entry_points.txt +0 -0
  89. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/pydpm_xl.egg-info/requires.txt +0 -0
  90. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/pydpm_xl.egg-info/top_level.txt +0 -0
  91. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/setup.cfg +0 -0
  92. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/tests/test_cli_semantic.py +0 -0
  93. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/tests/test_data_dictionary_releases.py +0 -0
  94. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/tests/test_db_connection_handling.py +0 -0
  95. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/tests/test_get_table_details.py +0 -0
  96. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/tests/test_get_tables_date_filter.py +0 -0
  97. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/tests/test_get_tables_release_code.py +0 -0
  98. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/tests/test_hierarchical_query.py +0 -0
  99. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/tests/test_query_refactor.py +0 -0
  100. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/tests/test_release_filters_semantic.py +0 -0
  101. {pydpm_xl-0.2.8 → pydpm_xl-0.2.10}/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.8
3
+ Version: 0.2.10
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
@@ -210,22 +210,21 @@ else:
210
210
  print(f"Error: {result.error}")
211
211
  ```
212
212
 
213
- #### Complete AST Generation
213
+ #### Validations Script Generation
214
214
 
215
215
  ```python
216
- from py_dpm.api import generate_complete_ast, generate_enriched_ast
216
+ from py_dpm.api import generate_validations_script
217
217
 
218
- # Generate complete AST with data fields
219
- result = generate_complete_ast("{tT_01.00, r0010, c0010}", release_id=123)
220
- if result.success:
221
- print(f"AST: {result.ast}")
222
- print(f"Data fields: {result.data_fields}")
218
+ # Generate engine-ready validations script
219
+ result = generate_validations_script(
220
+ "{tT_01.00, r0010, c0010}",
221
+ database_path="data.db",
222
+ release_code="4.2"
223
+ )
224
+ if result["success"]:
225
+ print(f"Enriched AST: {result['enriched_ast']}")
223
226
  else:
224
- print(f"Errors: {result.errors}")
225
-
226
- # Generate enriched AST (ready for execution engine)
227
- enriched = generate_enriched_ast("{tT_01.00, r0010, c0010}", release_id=123)
228
- print(f"Enriched AST: {enriched}")
227
+ print(f"Error: {result['error']}")
229
228
  ```
230
229
 
231
230
  #### Migration
@@ -177,22 +177,21 @@ else:
177
177
  print(f"Error: {result.error}")
178
178
  ```
179
179
 
180
- #### Complete AST Generation
180
+ #### Validations Script Generation
181
181
 
182
182
  ```python
183
- from py_dpm.api import generate_complete_ast, generate_enriched_ast
183
+ from py_dpm.api import generate_validations_script
184
184
 
185
- # Generate complete AST with data fields
186
- result = generate_complete_ast("{tT_01.00, r0010, c0010}", release_id=123)
187
- if result.success:
188
- print(f"AST: {result.ast}")
189
- print(f"Data fields: {result.data_fields}")
185
+ # Generate engine-ready validations script
186
+ result = generate_validations_script(
187
+ "{tT_01.00, r0010, c0010}",
188
+ database_path="data.db",
189
+ release_code="4.2"
190
+ )
191
+ if result["success"]:
192
+ print(f"Enriched AST: {result['enriched_ast']}")
190
193
  else:
191
- print(f"Errors: {result.errors}")
192
-
193
- # Generate enriched AST (ready for execution engine)
194
- enriched = generate_enriched_ast("{tT_01.00, r0010, c0010}", release_id=123)
195
- print(f"Enriched AST: {enriched}")
194
+ print(f"Error: {result['error']}")
196
195
  ```
197
196
 
198
197
  #### Migration
@@ -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.8"
44
+ __version__ = "0.2.10"
45
45
  __author__ = "MeaningfulData S.L."
46
46
  __email__ = "info@meaningfuldata.eu"
47
47
  __license__ = "GPL-3.0-or-later"
@@ -11,10 +11,7 @@ from py_dpm.api.dpm_xl import (
11
11
  SemanticAPI,
12
12
  ASTGeneratorAPI,
13
13
  OperationScopesAPI,
14
- generate_complete_ast,
15
- generate_enriched_ast,
16
- enrich_ast_with_metadata,
17
- parse_with_data_fields,
14
+ generate_validations_script,
18
15
  )
19
16
 
20
17
  # Import from general DPM API
@@ -40,9 +37,6 @@ __all__ = [
40
37
  "SemanticAPI",
41
38
  "ASTGeneratorAPI",
42
39
  "OperationScopesAPI",
43
- # Complete AST functions (backwards compatibility)
44
- "generate_complete_ast",
45
- "generate_enriched_ast",
46
- "enrich_ast_with_metadata",
47
- "parse_with_data_fields",
40
+ # Standalone function
41
+ "generate_validations_script",
48
42
  ]
@@ -9,13 +9,7 @@ from py_dpm.api.dpm_xl.semantic import SemanticAPI
9
9
  from py_dpm.api.dpm_xl.ast_generator import ASTGeneratorAPI
10
10
  from py_dpm.api.dpm_xl.operation_scopes import OperationScopesAPI
11
11
 
12
- # Backwards-compatible standalone functions (delegate to ASTGeneratorAPI)
13
- from py_dpm.api.dpm_xl.complete_ast import (
14
- generate_complete_ast,
15
- generate_enriched_ast,
16
- enrich_ast_with_metadata,
17
- parse_with_data_fields,
18
- )
12
+ from py_dpm.api.dpm_xl.complete_ast import generate_validations_script
19
13
 
20
14
  __all__ = [
21
15
  # Class-based APIs
@@ -23,9 +17,6 @@ __all__ = [
23
17
  "SemanticAPI",
24
18
  "ASTGeneratorAPI",
25
19
  "OperationScopesAPI",
26
- # Standalone functions (backwards compatibility)
27
- "generate_complete_ast",
28
- "generate_enriched_ast",
29
- "enrich_ast_with_metadata",
30
- "parse_with_data_fields",
20
+ # Standalone function
21
+ "generate_validations_script",
31
22
  ]
@@ -32,7 +32,7 @@ class ASTGeneratorAPI:
32
32
  - Returns: AST with data fields populated (datapoint IDs, operand references)
33
33
  - Use for: AST analysis with complete metadata, matching json_scripts/*.json format
34
34
 
35
- 3. **Enriched AST** (generate_enriched_ast):
35
+ 3. **Validations Script** (generate_validations_script):
36
36
  - Requires database connection
37
37
  - Extends complete AST with framework structure for execution engines
38
38
  - Returns: Engine-ready AST with operations, variables, tables, preconditions sections
@@ -90,7 +90,7 @@ class ASTGeneratorAPI:
90
90
 
91
91
  **What you DON'T get:**
92
92
  - Data fields (datapoint IDs, operand references) - use generate_complete_ast()
93
- - Framework structure - use generate_enriched_ast()
93
+ - Framework structure - use generate_validations_script()
94
94
 
95
95
  Args:
96
96
  expression: DPM-XL expression string
@@ -347,7 +347,7 @@ class ASTGeneratorAPI:
347
347
  return [(expressions, "default_code", None)]
348
348
  return expressions
349
349
 
350
- def generate_enriched_ast(
350
+ def generate_validations_script(
351
351
  self,
352
352
  expressions: Union[str, List[Tuple[str, str, Optional[str]]]],
353
353
  release_code: Optional[str] = None,
@@ -360,9 +360,9 @@ class ASTGeneratorAPI:
360
360
  module_version_number: Optional[str] = None,
361
361
  ) -> Dict[str, Any]:
362
362
  """
363
- Generate enriched, engine-ready AST with framework structure (Level 3).
363
+ Generate validations script with engine-ready AST and framework structure.
364
364
 
365
- This extends generate_complete_ast() by wrapping the complete AST in an engine-ready
365
+ This method generates the complete validations script by wrapping ASTs in an engine-ready
366
366
  framework structure with operations, variables, tables, and preconditions sections.
367
367
  This is the format required by business rule execution engines.
368
368
 
@@ -370,7 +370,7 @@ class ASTGeneratorAPI:
370
370
  expression/operation/precondition tuples for generating scripts with multiple operations.
371
371
 
372
372
  **What you get:**
373
- - Everything from generate_complete_ast() PLUS:
373
+ - Complete AST with data fields PLUS:
374
374
  - Framework structure: operations, variables, tables, preconditions
375
375
  - Module metadata: version, release info, dates
376
376
  - Dependency information (including cross-module dependencies)
@@ -428,14 +428,14 @@ class ASTGeneratorAPI:
428
428
 
429
429
  Example:
430
430
  >>> generator = ASTGeneratorAPI(database_path="data.db")
431
- >>> # Single expression (backward compatible)
432
- >>> result = generator.generate_enriched_ast(
431
+ >>> # Single expression
432
+ >>> result = generator.generate_validations_script(
433
433
  ... "{tF_01.00, r0010, c0010}",
434
434
  ... release_code="4.2",
435
435
  ... )
436
436
  >>>
437
437
  >>> # Multiple expressions with operations and preconditions
438
- >>> result = generator.generate_enriched_ast(
438
+ >>> result = generator.generate_validations_script(
439
439
  ... [
440
440
  ... ("{tF_01.00, r0010, c0010} = 0", "v1234_m", None),
441
441
  ... ("{tF_01.00, r0020, c0010} > 0", "v1235_m", "{v_F_44_04}"),
@@ -984,6 +984,7 @@ class ASTGeneratorAPI:
984
984
  """
985
985
  from py_dpm.dpm.utils import get_engine
986
986
  from py_dpm.api.dpm import DataDictionaryAPI
987
+ from py_dpm.api.dpm_xl.operation_scopes import OperationScopesAPI
987
988
 
988
989
  # Initialize database connection
989
990
  engine = get_engine(database_path=self.database_path, connection_url=self.connection_url)
@@ -1017,6 +1018,12 @@ class ASTGeneratorAPI:
1017
1018
  connection_url=self.connection_url
1018
1019
  )
1019
1020
 
1021
+ # Initialize OperationScopesAPI once for all expressions (performance optimization)
1022
+ scopes_api = OperationScopesAPI(
1023
+ database_path=self.database_path,
1024
+ connection_url=self.connection_url
1025
+ )
1026
+
1020
1027
  # Primary module info will be determined from the first expression or module_code
1021
1028
  primary_module_info = None
1022
1029
  namespace = None
@@ -1035,6 +1042,21 @@ class ASTGeneratorAPI:
1035
1042
  complete_ast = complete_result["ast"]
1036
1043
  context = complete_result.get("context") or table_context
1037
1044
 
1045
+ # Get tables with modules for this expression FIRST (reuse scopes_api from outer scope)
1046
+ # This is done before _get_primary_module_info to pass precomputed values
1047
+ tables_with_modules = scopes_api.get_tables_with_metadata_from_expression(
1048
+ expression=expression,
1049
+ release_id=release_id
1050
+ )
1051
+
1052
+ # Calculate scope_result once (avoid duplicate calls in other methods)
1053
+ scope_result = scopes_api.calculate_scopes_from_expression(
1054
+ expression=expression,
1055
+ release_id=release_id,
1056
+ read_only=True
1057
+ )
1058
+ all_tables_with_modules.extend(tables_with_modules)
1059
+
1038
1060
  # Get primary module info from first expression (or use module_code)
1039
1061
  if primary_module_info is None:
1040
1062
  primary_module_info = self._get_primary_module_info(
@@ -1042,6 +1064,9 @@ class ASTGeneratorAPI:
1042
1064
  primary_module_vid=primary_module_vid,
1043
1065
  release_id=release_id,
1044
1066
  module_code=module_code,
1067
+ # Performance optimization: pass precomputed values
1068
+ tables_with_modules=tables_with_modules,
1069
+ scopes_api=scopes_api,
1045
1070
  )
1046
1071
  namespace = primary_module_info.get("module_uri", "default_module")
1047
1072
 
@@ -1066,18 +1091,6 @@ class ASTGeneratorAPI:
1066
1091
  # Clean extra fields from data entries
1067
1092
  self._clean_ast_data_entries(ast_with_coords)
1068
1093
 
1069
- # Get tables with modules for this expression
1070
- from py_dpm.api.dpm_xl.operation_scopes import OperationScopesAPI
1071
- scopes_api = OperationScopesAPI(
1072
- database_path=self.database_path,
1073
- connection_url=self.connection_url
1074
- )
1075
- tables_with_modules = scopes_api.get_tables_with_metadata_from_expression(
1076
- expression=expression,
1077
- release_id=release_id
1078
- )
1079
- all_tables_with_modules.extend(tables_with_modules)
1080
-
1081
1094
  # Build mapping of table_code -> module_vid
1082
1095
  # Prefer the module VID that matches the detected primary module
1083
1096
  table_to_module = {}
@@ -1179,6 +1192,10 @@ class ASTGeneratorAPI:
1179
1192
  operation_code=operation_code,
1180
1193
  release_id=release_id,
1181
1194
  preferred_module_dependencies=preferred_module_dependencies,
1195
+ # Performance optimization: pass precomputed values to avoid redundant work
1196
+ tables_with_modules=tables_with_modules,
1197
+ scopes_api=scopes_api,
1198
+ scope_result=scope_result,
1182
1199
  )
1183
1200
 
1184
1201
  # Merge dependency modules (avoid table duplicates)
@@ -1313,6 +1330,8 @@ class ASTGeneratorAPI:
1313
1330
  primary_module_vid: Optional[int],
1314
1331
  release_id: Optional[int],
1315
1332
  module_code: Optional[str] = None,
1333
+ tables_with_modules: Optional[List[Dict[str, Any]]] = None,
1334
+ scopes_api: Optional[Any] = None,
1316
1335
  ) -> Dict[str, Any]:
1317
1336
  """
1318
1337
  Detect and return metadata for the primary module from the expression.
@@ -1323,6 +1342,10 @@ class ASTGeneratorAPI:
1323
1342
  release_id: Optional release ID for filtering
1324
1343
  module_code: Optional module code (e.g., "FINREP9") - takes precedence over
1325
1344
  primary_module_vid if provided
1345
+ tables_with_modules: Optional precomputed tables with module metadata
1346
+ (performance optimization to avoid redundant database queries)
1347
+ scopes_api: Optional precomputed OperationScopesAPI instance
1348
+ (performance optimization to reuse database connections)
1326
1349
 
1327
1350
  Returns:
1328
1351
  Dict with module_uri, module_code, module_version, framework_code,
@@ -1341,20 +1364,28 @@ class ASTGeneratorAPI:
1341
1364
  "module_vid": None,
1342
1365
  }
1343
1366
 
1367
+ # Track if we created the scopes_api locally (need to close it)
1368
+ local_scopes_api = False
1369
+
1344
1370
  try:
1345
- scopes_api = OperationScopesAPI(
1346
- database_path=self.database_path,
1347
- connection_url=self.connection_url
1348
- )
1371
+ # Reuse provided scopes_api or create a new one
1372
+ if scopes_api is None:
1373
+ scopes_api = OperationScopesAPI(
1374
+ database_path=self.database_path,
1375
+ connection_url=self.connection_url
1376
+ )
1377
+ local_scopes_api = True
1349
1378
 
1350
- # Get tables with module metadata from expression
1351
- tables_with_modules = scopes_api.get_tables_with_metadata_from_expression(
1352
- expression=expression,
1353
- release_id=release_id
1354
- )
1379
+ # Reuse provided tables_with_modules or fetch if not available
1380
+ if tables_with_modules is None:
1381
+ tables_with_modules = scopes_api.get_tables_with_metadata_from_expression(
1382
+ expression=expression,
1383
+ release_id=release_id
1384
+ )
1355
1385
 
1356
1386
  if not tables_with_modules:
1357
- scopes_api.close()
1387
+ if local_scopes_api:
1388
+ scopes_api.close()
1358
1389
  return default_info
1359
1390
 
1360
1391
  # Determine primary module
@@ -1408,7 +1439,8 @@ class ASTGeneratorAPI:
1408
1439
  to_date = module.get("to_reference_date", to_date)
1409
1440
  break
1410
1441
 
1411
- scopes_api.close()
1442
+ if local_scopes_api:
1443
+ scopes_api.close()
1412
1444
 
1413
1445
  return {
1414
1446
  "module_uri": module_uri or "default_module",
@@ -1864,6 +1896,9 @@ class ASTGeneratorAPI:
1864
1896
  operation_code: str,
1865
1897
  release_id: Optional[int] = None,
1866
1898
  preferred_module_dependencies: Optional[List[str]] = None,
1899
+ tables_with_modules: Optional[List[Dict[str, Any]]] = None,
1900
+ scopes_api: Optional[Any] = None,
1901
+ scope_result: Optional[Any] = None,
1867
1902
  ) -> tuple:
1868
1903
  """
1869
1904
  Detect cross-module dependencies for a single expression.
@@ -1879,6 +1914,12 @@ class ASTGeneratorAPI:
1879
1914
  release_id: Optional release ID for filtering
1880
1915
  preferred_module_dependencies: Optional list of module codes to prefer when
1881
1916
  a table belongs to multiple modules
1917
+ tables_with_modules: Optional precomputed tables with module metadata
1918
+ (performance optimization to avoid redundant database queries)
1919
+ scopes_api: Optional precomputed OperationScopesAPI instance
1920
+ (performance optimization to reuse database connections)
1921
+ scope_result: Optional precomputed scope result from calculate_scopes_from_expression
1922
+ (performance optimization to avoid redundant computation)
1882
1923
 
1883
1924
  Returns:
1884
1925
  Tuple of (dependency_modules, cross_instance_dependencies)
@@ -1889,24 +1930,28 @@ class ASTGeneratorAPI:
1889
1930
  from py_dpm.dpm.queries.explorer_queries import ExplorerQuery
1890
1931
  import logging
1891
1932
 
1892
- scopes_api = OperationScopesAPI(
1893
- database_path=self.database_path,
1894
- connection_url=self.connection_url
1895
- )
1933
+ # Reuse provided scopes_api or create a new one
1934
+ if scopes_api is None:
1935
+ scopes_api = OperationScopesAPI(
1936
+ database_path=self.database_path,
1937
+ connection_url=self.connection_url
1938
+ )
1896
1939
 
1897
1940
  try:
1898
- # Get tables with module info (includes module_version)
1899
- tables_with_modules = scopes_api.get_tables_with_metadata_from_expression(
1900
- expression=expression,
1901
- release_id=release_id
1902
- )
1941
+ # Reuse provided tables_with_modules or fetch if not available
1942
+ if tables_with_modules is None:
1943
+ tables_with_modules = scopes_api.get_tables_with_metadata_from_expression(
1944
+ expression=expression,
1945
+ release_id=release_id
1946
+ )
1903
1947
 
1904
- # Check if cross-module
1905
- scope_result = scopes_api.calculate_scopes_from_expression(
1906
- expression=expression,
1907
- release_id=release_id,
1908
- read_only=True
1909
- )
1948
+ # Reuse provided scope_result or compute if not available
1949
+ if scope_result is None:
1950
+ scope_result = scopes_api.calculate_scopes_from_expression(
1951
+ expression=expression,
1952
+ release_id=release_id,
1953
+ read_only=True
1954
+ )
1910
1955
 
1911
1956
  if scope_result.has_error or not scope_result.is_cross_module:
1912
1957
  return {}, []
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validations Script API - Generate engine-ready ASTs for validation frameworks.
4
+
5
+ This module provides a standalone function that delegates to ASTGeneratorAPI.
6
+
7
+ For direct class usage:
8
+ from py_dpm.api.dpm_xl import ASTGeneratorAPI
9
+
10
+ generator = ASTGeneratorAPI(database_path="data.db")
11
+ result = generator.generate_validations_script(expressions)
12
+ """
13
+
14
+ from typing import Dict, Any, Optional, List, Union, Tuple
15
+ from py_dpm.api.dpm_xl.ast_generator import ASTGeneratorAPI
16
+
17
+
18
+ def generate_validations_script(
19
+ expressions: Union[str, List[Tuple[str, str, Optional[str]]]],
20
+ database_path: Optional[str] = None,
21
+ connection_url: Optional[str] = None,
22
+ release_code: Optional[str] = None,
23
+ table_context: Optional[Dict[str, Any]] = None,
24
+ release_id: Optional[int] = None,
25
+ primary_module_vid: Optional[int] = None,
26
+ module_code: Optional[str] = None,
27
+ preferred_module_dependencies: Optional[List[str]] = None,
28
+ ) -> Dict[str, Any]:
29
+ """
30
+ Generate validations script with engine-ready AST from DPM-XL expression(s).
31
+
32
+ This function delegates to ASTGeneratorAPI.generate_validations_script().
33
+
34
+ Supports both single expressions and multiple expression/operation/precondition
35
+ tuples for generating scripts with multiple operations.
36
+
37
+ Args:
38
+ expressions: Either a single DPM-XL expression string,
39
+ or a list of tuples: [(expression, operation_code, precondition), ...].
40
+ Each tuple contains:
41
+ - expression (str): The DPM-XL expression (required)
42
+ - operation_code (str): The operation code (required)
43
+ - precondition (Optional[str]): Optional precondition reference (e.g., {v_F_44_04})
44
+ database_path: Path to SQLite database (or None for PostgreSQL)
45
+ connection_url: PostgreSQL connection URL (takes precedence over database_path)
46
+ release_code: DPM release code (e.g., "4.0", "4.1", "4.2")
47
+ table_context: Optional table context dict with keys: 'table', 'columns', 'rows', 'sheets', 'default', 'interval'
48
+ release_id: Optional release ID to filter database lookups by specific release.
49
+ If None, uses all available data (release-agnostic).
50
+ primary_module_vid: Optional module version ID of the module being exported.
51
+ When provided, enables detection of cross-module dependencies.
52
+ module_code: Optional module code (e.g., "FINREP9") to specify the main module.
53
+ preferred_module_dependencies: Optional list of module codes to prefer when
54
+ multiple dependency scopes are possible.
55
+
56
+ Returns:
57
+ dict: {
58
+ 'success': bool,
59
+ 'enriched_ast': dict, # Engine-ready AST with framework structure
60
+ 'error': str # Error message if failed
61
+ }
62
+
63
+ Example:
64
+ >>> # Single expression
65
+ >>> result = generate_validations_script(
66
+ ... "{tF_01.00, r0010, c0010}",
67
+ ... database_path="data.db",
68
+ ... release_code="4.2",
69
+ ... )
70
+ >>>
71
+ >>> # Multiple expressions
72
+ >>> result = generate_validations_script(
73
+ ... [
74
+ ... ("{tF_01.00, r0010, c0010} = 0", "v1234_m", None),
75
+ ... ("{tF_01.00, r0020, c0010} > 0", "v1235_m", "{v_F_44_04}"),
76
+ ... ],
77
+ ... database_path="data.db",
78
+ ... release_code="4.2",
79
+ ... module_code="FINREP9",
80
+ ... )
81
+ """
82
+ generator = ASTGeneratorAPI(
83
+ database_path=database_path,
84
+ connection_url=connection_url,
85
+ enable_semantic_validation=True
86
+ )
87
+ return generator.generate_validations_script(
88
+ expressions=expressions,
89
+ release_code=release_code,
90
+ table_context=table_context,
91
+ release_id=release_id,
92
+ primary_module_vid=primary_module_vid,
93
+ module_code=module_code,
94
+ preferred_module_dependencies=preferred_module_dependencies,
95
+ )
@@ -103,14 +103,16 @@ def _extract_with_pyodbc(access_file):
103
103
  import pyodbc
104
104
  except ImportError:
105
105
  raise Exception("pyodbc not available")
106
-
106
+
107
+ import decimal
108
+
107
109
  # Try different Access drivers
108
110
  drivers_to_try = [
109
111
  r'DRIVER={Microsoft Access Driver (*.mdb, *.accdb)};',
110
112
  r'DRIVER={Microsoft Access Driver (*.mdb)};',
111
113
  r'DRIVER={MDBTools};'
112
114
  ]
113
-
115
+
114
116
  conn = None
115
117
  for driver in drivers_to_try:
116
118
  try:
@@ -120,10 +122,10 @@ def _extract_with_pyodbc(access_file):
120
122
  break
121
123
  except pyodbc.Error:
122
124
  continue
123
-
125
+
124
126
  if not conn:
125
127
  raise Exception("No suitable ODBC driver found for Access database")
126
-
128
+
127
129
  try:
128
130
  # Get all table names
129
131
  cursor = conn.cursor()
@@ -132,63 +134,56 @@ def _extract_with_pyodbc(access_file):
132
134
  table_name = table_info.table_name
133
135
  if not table_name.startswith('MSys'): # Skip system tables
134
136
  tables.append(table_name)
135
-
137
+
136
138
  data = {}
137
- STRING_COLUMNS = ["row", "column", "sheet"]
138
-
139
+
139
140
  # Extract each table
140
141
  for table_name in tables:
141
142
  print(table_name)
142
143
  try:
143
144
  cursor.execute(f"SELECT * FROM [{table_name}]")
144
- columns = [column[0] for column in cursor.description]
145
+
146
+ # Get column metadata from cursor.description
147
+ # Each entry is: (name, type_code, display_size, internal_size, precision, scale, null_ok)
148
+ # type_code is a Python type (str, int, float, decimal.Decimal, etc.)
149
+ column_info = []
150
+ for col_desc in cursor.description:
151
+ col_name = col_desc[0]
152
+ col_type = col_desc[1] # Python type from ODBC metadata
153
+ column_info.append((col_name, col_type))
154
+
155
+ columns = [info[0] for info in column_info]
145
156
  rows = cursor.fetchall()
146
-
157
+
147
158
  if rows:
148
159
  # Convert to DataFrame
149
160
  df = pd.DataFrame([list(row) for row in rows], columns=columns)
150
161
 
151
- # Apply same dtype conversion logic as mdb-tools method
152
- # Start with all strings, but preserve None as actual None (not string 'None')
153
- for col in df.columns:
154
- df[col] = df[col].astype(object)
155
- mask = df[col].notna()
156
- df.loc[mask, col] = df.loc[mask, col].astype(str)
157
-
158
- numeric_columns = []
159
- for column in df.columns:
160
- if column in STRING_COLUMNS:
161
- continue
162
- try:
163
- # Convert to numeric and check if any values start with '0' (except '0' itself)
164
- # Only check string values for leading zeros
165
- string_mask = df[column].astype(str).str.match(r'^0\d+', na=False)
166
- has_leading_zeros = string_mask.any()
167
-
168
- # Test numeric conversion
169
- numeric_series = pd.to_numeric(df[column], errors='coerce')
170
-
171
- if not has_leading_zeros and not numeric_series.isna().all():
172
- numeric_columns.append(column)
173
- except Exception:
174
- continue
175
-
176
- # Convert only the identified numeric columns
177
- for col in numeric_columns:
178
- try:
179
- df[col] = pd.to_numeric(df[col], errors='coerce')
180
- except (ValueError, TypeError):
181
- # Keep as string if conversion fails
182
- pass
183
-
162
+ # Use the actual column types from Access schema metadata
163
+ # instead of inferring from data values (fixes Windows vs Linux inconsistency)
164
+ numeric_types = (int, float, decimal.Decimal)
165
+
166
+ for col_name, col_type in column_info:
167
+ if col_type in numeric_types:
168
+ # Column is defined as numeric in Access schema - convert to numeric
169
+ try:
170
+ df[col_name] = pd.to_numeric(df[col_name], errors='coerce')
171
+ except (ValueError, TypeError):
172
+ pass
173
+ else:
174
+ # Column is defined as text/other in Access schema - keep as string
175
+ df[col_name] = df[col_name].astype(object)
176
+ mask = df[col_name].notna()
177
+ df.loc[mask, col_name] = df.loc[mask, col_name].astype(str)
178
+
184
179
  data[table_name] = df
185
-
180
+
186
181
  except Exception as e:
187
182
  print(f"Error processing table {table_name}: {e}", file=sys.stderr)
188
183
  continue
189
-
184
+
190
185
  return data
191
-
186
+
192
187
  finally:
193
188
  conn.close()
194
189