pydpm_xl 0.2.8__py3-none-any.whl → 0.2.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
py_dpm/__init__.py CHANGED
@@ -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"
py_dpm/api/__init__.py CHANGED
@@ -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 {}, []
@@ -1,85 +1,21 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Complete AST API - Generate ASTs exactly like the JSON examples
3
+ Validations Script API - Generate engine-ready ASTs for validation frameworks.
4
4
 
5
- This module provides backwards-compatible standalone functions that delegate to ASTGeneratorAPI.
6
- All AST-related functionality is now consolidated in the ASTGeneratorAPI class.
5
+ This module provides a standalone function that delegates to ASTGeneratorAPI.
7
6
 
8
- For new code, prefer using ASTGeneratorAPI directly:
7
+ For direct class usage:
9
8
  from py_dpm.api.dpm_xl import ASTGeneratorAPI
10
9
 
11
10
  generator = ASTGeneratorAPI(database_path="data.db")
12
- result = generator.generate_complete_ast(expression)
11
+ result = generator.generate_validations_script(expressions)
13
12
  """
14
13
 
15
14
  from typing import Dict, Any, Optional, List, Union, Tuple
16
15
  from py_dpm.api.dpm_xl.ast_generator import ASTGeneratorAPI
17
16
 
18
17
 
19
- def generate_complete_ast(
20
- expression: str,
21
- database_path: str = None,
22
- connection_url: str = None,
23
- release_id: Optional[int] = None,
24
- ):
25
- """
26
- Generate complete AST with all data fields, exactly like json_scripts examples.
27
-
28
- This function delegates to ASTGeneratorAPI for backwards compatibility.
29
-
30
- Args:
31
- expression: DPM-XL expression string
32
- database_path: Path to SQLite database file (e.g., "./database.db")
33
- connection_url: SQLAlchemy connection URL for PostgreSQL (optional)
34
- release_id: Optional release ID to filter database lookups by specific release.
35
- If None, uses all available data (release-agnostic).
36
-
37
- Returns:
38
- dict with keys:
39
- success, ast, context, error, data_populated, semantic_result
40
- """
41
- generator = ASTGeneratorAPI(
42
- database_path=database_path,
43
- connection_url=connection_url,
44
- enable_semantic_validation=True
45
- )
46
- return generator.generate_complete_ast(expression, release_id=release_id)
47
-
48
-
49
- # Convenience function with cleaner interface
50
- def parse_with_data_fields(
51
- expression: str,
52
- database_path: str = None,
53
- connection_url: str = None,
54
- release_id: Optional[int] = None,
55
- ):
56
- """
57
- Simple function to parse expression and get AST with data fields.
58
-
59
- This function delegates to ASTGeneratorAPI for backwards compatibility.
60
-
61
- Args:
62
- expression: DPM-XL expression string
63
- database_path: Path to SQLite database file
64
- connection_url: SQLAlchemy connection URL for PostgreSQL (optional)
65
- release_id: Optional release ID to filter database lookups by specific release.
66
- If None, uses all available data (release-agnostic).
67
-
68
- Returns:
69
- dict: AST dictionary with data fields, or None if failed
70
- """
71
- result = generate_complete_ast(
72
- expression, database_path, connection_url, release_id=release_id
73
- )
74
- return result["ast"] if result["success"] else None
75
-
76
-
77
- # ============================================================================
78
- # AST Enrichment Functions - Create engine-ready ASTs
79
- # ============================================================================
80
-
81
-
82
- def generate_enriched_ast(
18
+ def generate_validations_script(
83
19
  expressions: Union[str, List[Tuple[str, str, Optional[str]]]],
84
20
  database_path: Optional[str] = None,
85
21
  connection_url: Optional[str] = None,
@@ -91,15 +27,15 @@ def generate_enriched_ast(
91
27
  preferred_module_dependencies: Optional[List[str]] = None,
92
28
  ) -> Dict[str, Any]:
93
29
  """
94
- Generate enriched, engine-ready AST from DPM-XL expression(s).
30
+ Generate validations script with engine-ready AST from DPM-XL expression(s).
95
31
 
96
- This function delegates to ASTGeneratorAPI for backwards compatibility.
32
+ This function delegates to ASTGeneratorAPI.generate_validations_script().
97
33
 
98
- Supports both single expressions (for backward compatibility) and multiple
99
- expression/operation/precondition tuples for generating scripts with multiple operations.
34
+ Supports both single expressions and multiple expression/operation/precondition
35
+ tuples for generating scripts with multiple operations.
100
36
 
101
37
  Args:
102
- expressions: Either a single DPM-XL expression string (backward compatible),
38
+ expressions: Either a single DPM-XL expression string,
103
39
  or a list of tuples: [(expression, operation_code, precondition), ...].
104
40
  Each tuple contains:
105
41
  - expression (str): The DPM-XL expression (required)
@@ -125,15 +61,15 @@ def generate_enriched_ast(
125
61
  }
126
62
 
127
63
  Example:
128
- >>> # Single expression (backward compatible)
129
- >>> result = generate_enriched_ast(
64
+ >>> # Single expression
65
+ >>> result = generate_validations_script(
130
66
  ... "{tF_01.00, r0010, c0010}",
131
67
  ... database_path="data.db",
132
68
  ... release_code="4.2",
133
69
  ... )
134
70
  >>>
135
71
  >>> # Multiple expressions
136
- >>> result = generate_enriched_ast(
72
+ >>> result = generate_validations_script(
137
73
  ... [
138
74
  ... ("{tF_01.00, r0010, c0010} = 0", "v1234_m", None),
139
75
  ... ("{tF_01.00, r0020, c0010} > 0", "v1235_m", "{v_F_44_04}"),
@@ -148,7 +84,7 @@ def generate_enriched_ast(
148
84
  connection_url=connection_url,
149
85
  enable_semantic_validation=True
150
86
  )
151
- return generator.generate_enriched_ast(
87
+ return generator.generate_validations_script(
152
88
  expressions=expressions,
153
89
  release_code=release_code,
154
90
  table_context=table_context,
@@ -157,52 +93,3 @@ def generate_enriched_ast(
157
93
  module_code=module_code,
158
94
  preferred_module_dependencies=preferred_module_dependencies,
159
95
  )
160
-
161
-
162
- def enrich_ast_with_metadata(
163
- ast_dict: Dict[str, Any],
164
- expression: str,
165
- context: Optional[Dict[str, Any]],
166
- database_path: Optional[str] = None,
167
- connection_url: Optional[str] = None,
168
- release_code: Optional[str] = None,
169
- operation_code: Optional[str] = None,
170
- precondition: Optional[str] = None,
171
- release_id: Optional[int] = None,
172
- primary_module_vid: Optional[int] = None,
173
- ) -> Dict[str, Any]:
174
- """
175
- Add framework structure (operations, variables, tables, preconditions) to complete AST.
176
-
177
- This function delegates to ASTGeneratorAPI for backwards compatibility.
178
-
179
- Args:
180
- ast_dict: Complete AST dictionary (from generate_complete_ast)
181
- expression: Original DPM-XL expression
182
- context: Context dict with table, rows, columns, sheets, default, interval
183
- database_path: Path to SQLite database
184
- connection_url: PostgreSQL connection URL (takes precedence)
185
- release_code: DPM release code (e.g., "4.2")
186
- operation_code: Operation code (defaults to "default_code")
187
- precondition: Precondition variable reference (e.g., {v_F_44_04})
188
- release_id: Optional release ID to filter database lookups
189
- primary_module_vid: Optional module VID of the module being exported
190
-
191
- Returns:
192
- dict: Engine-ready AST with framework structure
193
- """
194
- generator = ASTGeneratorAPI(
195
- database_path=database_path,
196
- connection_url=connection_url,
197
- enable_semantic_validation=True
198
- )
199
- return generator._enrich_ast_with_metadata(
200
- ast_dict=ast_dict,
201
- expression=expression,
202
- context=context,
203
- release_code=release_code,
204
- operation_code=operation_code,
205
- precondition=precondition,
206
- release_id=release_id,
207
- primary_module_vid=primary_module_vid,
208
- )
py_dpm/dpm/migration.py CHANGED
@@ -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
 
py_dpm/dpm/models.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from datetime import datetime
2
- from typing import List
2
+ from typing import Dict, Hashable, List, Tuple
3
3
  from sqlalchemy import (
4
4
  Boolean,
5
5
  Column,
@@ -36,6 +36,11 @@ class SerializationMixin:
36
36
  Base = declarative_base(cls=SerializationMixin)
37
37
 
38
38
 
39
+ def _get_engine_cache_key(session) -> Hashable:
40
+ bind = session.get_bind()
41
+ return getattr(bind, "url", repr(bind))
42
+
43
+
39
44
  def _read_sql_with_connection(sql, session):
40
45
  """
41
46
  Execute pd.read_sql with proper connection handling to avoid pandas warnings.
@@ -2319,6 +2324,11 @@ class ViewDatapoints(Base):
2319
2324
  context_id = Column(Integer)
2320
2325
  variable_vid = Column(String)
2321
2326
 
2327
+ _TABLE_DATA_CACHE: Dict[
2328
+ Tuple[Hashable, str, Tuple[str, ...] | None, Tuple[str, ...] | None, Tuple[str, ...] | None, int | None],
2329
+ pd.DataFrame,
2330
+ ] = {}
2331
+
2322
2332
  @classmethod
2323
2333
  def _create_base_query_with_aliases(cls, session):
2324
2334
  """
@@ -2552,7 +2562,16 @@ class ViewDatapoints(Base):
2552
2562
  def get_table_data(
2553
2563
  cls, session, table, rows=None, columns=None, sheets=None, release_id=None
2554
2564
  ):
2555
- # Build query using ORM for database-agnostic compatibility
2565
+ engine_key = _get_engine_cache_key(session)
2566
+ rows_key = tuple(rows) if rows is not None else None
2567
+ columns_key = tuple(columns) if columns is not None else None
2568
+ sheets_key = tuple(sheets) if sheets is not None else None
2569
+ cache_key = (engine_key, table, rows_key, columns_key, sheets_key, release_id)
2570
+
2571
+ cached = cls._TABLE_DATA_CACHE.get(cache_key)
2572
+ if cached is not None:
2573
+ return cached
2574
+
2556
2575
  query, aliases = cls._create_base_query_with_aliases(session)
2557
2576
 
2558
2577
  # Add column selections
@@ -2669,6 +2688,7 @@ class ViewDatapoints(Base):
2669
2688
  data = _check_ranges_values_are_present(data, "column_code", columns)
2670
2689
  data = _check_ranges_values_are_present(data, "sheet_code", sheets)
2671
2690
 
2691
+ cls._TABLE_DATA_CACHE[cache_key] = data
2672
2692
  return data
2673
2693
 
2674
2694
  @classmethod
@@ -2,6 +2,7 @@ from abc import ABC
2
2
 
3
3
  import pandas as pd
4
4
  import warnings
5
+ from typing import Dict, Hashable, Tuple
5
6
 
6
7
  # Suppress pandas UserWarning about SQLAlchemy connection types
7
8
  warnings.filterwarnings("ignore", message=".*pandas only supports SQLAlchemy.*")
@@ -43,6 +44,9 @@ from py_dpm.dpm_xl.utils.data_handlers import filter_all_data
43
44
  operand_elements = ["table", "rows", "cols", "sheets", "default", "interval"]
44
45
 
45
46
 
47
+ _HEADERS_CACHE: Dict[Tuple[Hashable, int, Tuple[str, ...]], pd.DataFrame] = {}
48
+
49
+
46
50
  def _create_operand_label(node):
47
51
  label = generate_new_label()
48
52
  node.label = label
@@ -185,42 +189,47 @@ class OperandsChecking(ASTTemplate, ABC):
185
189
  if len(table_codes) == 0:
186
190
  return
187
191
 
188
- # Build ORM query
189
- query = (
190
- self.session.query(
191
- TableVersion.code.label("Code"),
192
- TableVersion.startreleaseid.label("StartReleaseID"),
193
- TableVersion.endreleaseid.label("EndReleaseID"),
194
- Header.direction.label("Direction"),
195
- Table.hasopenrows.label("HasOpenRows"),
196
- Table.hasopencolumns.label("HasOpenColumns"),
197
- Table.hasopensheets.label("HasOpenSheets"),
198
- )
199
- .join(Table, Table.tableid == TableVersion.tableid)
200
- .join(
201
- TableVersionHeader, TableVersion.tablevid == TableVersionHeader.tablevid
192
+ engine = self.session.get_bind()
193
+ engine_key: Hashable = getattr(engine, "url", repr(engine))
194
+ cache_key = (engine_key, self.release_id, tuple(sorted(table_codes)))
195
+
196
+ df_headers = _HEADERS_CACHE.get(cache_key)
197
+ if df_headers is None:
198
+ query = (
199
+ self.session.query(
200
+ TableVersion.code.label("Code"),
201
+ TableVersion.startreleaseid.label("StartReleaseID"),
202
+ TableVersion.endreleaseid.label("EndReleaseID"),
203
+ Header.direction.label("Direction"),
204
+ Table.hasopenrows.label("HasOpenRows"),
205
+ Table.hasopencolumns.label("HasOpenColumns"),
206
+ Table.hasopensheets.label("HasOpenSheets"),
207
+ )
208
+ .join(Table, Table.tableid == TableVersion.tableid)
209
+ .join(
210
+ TableVersionHeader,
211
+ TableVersion.tablevid == TableVersionHeader.tablevid,
212
+ )
213
+ .join(Header, Header.headerid == TableVersionHeader.headerid)
214
+ .filter(TableVersion.code.in_(table_codes))
215
+ .distinct()
202
216
  )
203
- .join(Header, Header.headerid == TableVersionHeader.headerid)
204
- .filter(TableVersion.code.in_(table_codes))
205
- .distinct()
206
- )
207
217
 
208
- # Apply release filter
209
- query = filter_by_release(
210
- query,
211
- start_col=TableVersion.startreleaseid,
212
- end_col=TableVersion.endreleaseid,
213
- release_id=self.release_id,
214
- )
218
+ query = filter_by_release(
219
+ query,
220
+ start_col=TableVersion.startreleaseid,
221
+ end_col=TableVersion.endreleaseid,
222
+ release_id=self.release_id,
223
+ )
215
224
 
216
- # Execute query and convert to DataFrame
217
- from py_dpm.dpm.models import (
218
- _compile_query_for_pandas,
219
- _read_sql_with_connection,
220
- )
225
+ from py_dpm.dpm.models import (
226
+ _compile_query_for_pandas,
227
+ _read_sql_with_connection,
228
+ )
221
229
 
222
- compiled_query = _compile_query_for_pandas(query.statement, self.session)
223
- df_headers = _read_sql_with_connection(compiled_query, self.session)
230
+ compiled_query = _compile_query_for_pandas(query.statement, self.session)
231
+ df_headers = _read_sql_with_connection(compiled_query, self.session)
232
+ _HEADERS_CACHE[cache_key] = df_headers
224
233
 
225
234
  for table in table_codes:
226
235
  table_headers = df_headers[df_headers["Code"] == table]
@@ -452,6 +452,21 @@ class ASTToJSONVisitor(NodeVisitor):
452
452
  'shift_number': node.shift_number
453
453
  }
454
454
 
455
+ def visit_RenameOp(self, node):
456
+ """Visit RenameOp nodes and serialize as RenameClauseOp."""
457
+ return {
458
+ 'class_name': 'RenameClauseOp',
459
+ 'operand': self.visit(node.operand),
460
+ 'clauses': [self._serialize_rename_node(rn) for rn in node.rename_nodes]
461
+ }
462
+
463
+ def _serialize_rename_node(self, node):
464
+ """Serialize a RenameNode as a clause dictionary."""
465
+ return {
466
+ 'from_component': node.old_name,
467
+ 'to_component': node.new_name
468
+ }
469
+
455
470
  def visit_PreconditionItem(self, node):
456
471
  """Visit PreconditionItem nodes."""
457
472
  result = {
@@ -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
@@ -1,14 +1,14 @@
1
- py_dpm/__init__.py,sha256=4k-u3wCmXF8WInrw7MzDeJ0E3F7WlfF1uzzPIe-g7vg,1858
2
- py_dpm/api/__init__.py,sha256=6ElO0NKEjuqiHNxK7pxkzLNlaUOrYKnY3N0fUDDobik,1021
1
+ py_dpm/__init__.py,sha256=8RZiE-F0XPTFPGEyFhEp9cS-0ahXQN5PMGSlLRrarL8,1859
2
+ py_dpm/api/__init__.py,sha256=bPWo4KWW99GmGkQbq0yQ3_tbTxvidEVVrWmcDXWz-xA,828
3
3
  py_dpm/api/dpm/__init__.py,sha256=HQflgiRbs1eDi3KTadNhxS1NoaG6PGQDVMvFnuIEfXo,506
4
4
  py_dpm/api/dpm/data_dictionary.py,sha256=q6w_5bFdc6WPd5Z601PpDaCcnIw39CnI4wdby3GJmFU,29893
5
5
  py_dpm/api/dpm/explorer.py,sha256=xgrSdh2D83RivypF26WWo20rbQifYBEH7PXvINoi07Y,10861
6
6
  py_dpm/api/dpm/hierarchical_queries.py,sha256=X4AbpsWy3iItOTVIdVbtaTmRgOHPf0Y64Ig-_377uns,4054
7
7
  py_dpm/api/dpm/instance.py,sha256=v3DWzdaM5gPCecLjwjZ49FGfqZzUR3dPC0U8zGwdttk,3795
8
8
  py_dpm/api/dpm/migration.py,sha256=9FT7zzz4QdUIRR6MD01gMODBtfq9HH_RF4hRgZqMcZc,2404
9
- py_dpm/api/dpm_xl/__init__.py,sha256=cwqeYgmowGH6MK8kQMQkjQaTL9qKEnC8-M5rueqhrPI,850
10
- py_dpm/api/dpm_xl/ast_generator.py,sha256=7EEUEmXMwck7obL3rnBjHeDnsM0F-LHnpUZNd3rVjHI,94929
11
- py_dpm/api/dpm_xl/complete_ast.py,sha256=_FSxA0FmNXuW0OLS3c8yzp14yjkmdR0rTebBAs1pg-E,8141
9
+ py_dpm/api/dpm_xl/__init__.py,sha256=SOsCtwxuCkFQIdYoTcU6tVJdK1j96dPF4ZQpr8okLPc,576
10
+ py_dpm/api/dpm_xl/ast_generator.py,sha256=hmfy2qV5t_RFGgmid7YuCXbrkjknv1KuQaJGxbXuHLs,97583
11
+ py_dpm/api/dpm_xl/complete_ast.py,sha256=jjn9tsns4X7EN4RV5mFh5DRxxZfBOuX2b1wic4oB_ls,3967
12
12
  py_dpm/api/dpm_xl/operation_scopes.py,sha256=v2t3f2-AiF39hspNtpf_PA94T69JqbymFK9a5hpckRs,48831
13
13
  py_dpm/api/dpm_xl/semantic.py,sha256=Ddmh2Wj_iXIpQZ4jCjqOI-6ddFCquaO9RTu6W9i1Rts,13968
14
14
  py_dpm/api/dpm_xl/syntax.py,sha256=Ke_kKd9ModoJ6siL3GPT9j9QClmopryCRcdDAT3M5-E,5954
@@ -16,8 +16,8 @@ py_dpm/cli/__init__.py,sha256=UrfGHoQ0sZLjWfA0hoOoI4iTrn-bjr2f9Q8wDWd5nMo,133
16
16
  py_dpm/cli/main.py,sha256=LJ7JBk7lyWXe7ZYxnbxmohM1Dbha4sIdQzSTYKd9ZNo,22457
17
17
  py_dpm/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  py_dpm/dpm/__init__.py,sha256=moagUo5Gxf24-Tl9FL_3n2wmVoD_oXtpC-YIGktH_rc,212
19
- py_dpm/dpm/migration.py,sha256=ivO_ObvKzVomTns6qfo-o5FuciWxkXbMd_gJ4_tu7Xc,14110
20
- py_dpm/dpm/models.py,sha256=Rt1b8zOSayXd9KhwyfkGJVhCai3YxUakmLYgwaiICWA,135660
19
+ py_dpm/dpm/migration.py,sha256=I_OX1xS2eE3L2V1RAPyHOqFn3Z54NWE0ozLAJdQrLcc,13810
20
+ py_dpm/dpm/models.py,sha256=YyOYXzN4tIT2rO6-PP-53C6f7skatgnz7VY_5SnOsLQ,136414
21
21
  py_dpm/dpm/utils.py,sha256=3w06_kKiFRPqHvRUfN_0nNQIpppfidFybdDge97qzI0,14918
22
22
  py_dpm/dpm/queries/base.py,sha256=EddMeJMwtp63DyyIFO7_XxGvdlCtJQWWpeOVImlKp4I,3648
23
23
  py_dpm/dpm/queries/basic_objects.py,sha256=JOXC235lMDfVENrFAhZAl7_nqePJ4RrwJhFF0WDyk0M,955
@@ -35,7 +35,7 @@ py_dpm/dpm_xl/ast/ml_generation.py,sha256=Lw_1Btln2x1ewD9xH-2Ea4NJJP3PIqFoivWASX
35
35
  py_dpm/dpm_xl/ast/module_analyzer.py,sha256=ZnldoYn-s41UMiJpcAV6hjIwH6fssZeOpc564epngg8,2872
36
36
  py_dpm/dpm_xl/ast/module_dependencies.py,sha256=tbCqoDcE1n1lJOjtbpD3rNPkXrLk-k2rM5zyVwmsNpc,8355
37
37
  py_dpm/dpm_xl/ast/nodes.py,sha256=5ob8MsCW0fPZgz9yP_6IgVTH2SGeoTk5VncJuQ2SgrE,25035
38
- py_dpm/dpm_xl/ast/operands.py,sha256=UN69SQB0S8VG4sjQPz6CNfwjg1uDPEXqKsJtVIgiGGA,21128
38
+ py_dpm/dpm_xl/ast/operands.py,sha256=0Yye5z6l0BBh9nrMlkVVNy3fYgbeUzZYLmP5Jvzr95M,21606
39
39
  py_dpm/dpm_xl/ast/template.py,sha256=QhYm7Jh_a-ws3kSmf0hqXFLzB_quO9GgKcmcFe22_fg,3045
40
40
  py_dpm/dpm_xl/ast/visitor.py,sha256=yL9UpPMQlq8ToHR8COyFYpuSChnDRjnkQHbCyYX0tsY,509
41
41
  py_dpm/dpm_xl/ast/where_clause.py,sha256=g3cslQ8vmlm0doqQ_ghjXzhzItc_xlC_bQ9odn87FGk,328
@@ -69,16 +69,16 @@ py_dpm/dpm_xl/utils/data_handlers.py,sha256=a0E-IaP_-CDKLcj-Gt2ggAziKIOUiwnT2D9I
69
69
  py_dpm/dpm_xl/utils/operands_mapping.py,sha256=LG0hPlUuTM2X2uWOtiD6HkmNeDEJkWJ8gV-Fxej_8QM,2241
70
70
  py_dpm/dpm_xl/utils/operator_mapping.py,sha256=BFgbVbSCSuutFNHJ4gtgm5VuG38pcl8Kmfi-sefg6JU,1913
71
71
  py_dpm/dpm_xl/utils/scopes_calculator.py,sha256=do_emsUqD1TbrjguKlOOqFleaVhxzqm-NnlgdrdIb6I,20906
72
- py_dpm/dpm_xl/utils/serialization.py,sha256=ouQpkmZWc6_T0hVzjwmxpHRu9yheNyyMBBaOZERe0qM,32558
72
+ py_dpm/dpm_xl/utils/serialization.py,sha256=4ZBHIP-67_19QSxqzkv8GPh3RNJ861if26GKbV9Bxj8,33088
73
73
  py_dpm/dpm_xl/utils/tokens.py,sha256=VRIrPDi5ttwgH-on5Qt4-l4ho4bLA755-nfTalponcA,3496
74
74
  py_dpm/exceptions/__init__.py,sha256=yDERfUxYW7NUUEiTQChGpuJx6abr7IDe2XUpwVFPtvM,416
75
75
  py_dpm/exceptions/exceptions.py,sha256=6S3p-_i5O1oStvSMixt_JQG0xwTeSfBcdzrwL8yBy6Q,2413
76
76
  py_dpm/exceptions/messages.py,sha256=UwY6QIK8c-POcDCc9HYbZFGArCIYAanUGNh2LNKPx3U,7534
77
77
  py_dpm/instance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
78
  py_dpm/instance/instance.py,sha256=gRSg2dh1nEa0Srx9yKcN3bxiYidvZyRU_jsTNaKkP5I,10882
79
- pydpm_xl-0.2.8.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
80
- pydpm_xl-0.2.8.dist-info/METADATA,sha256=lxBecMV5iI2uJ7kYokD9ewnzfV5k6Zprgvb0OdtehF8,9302
81
- pydpm_xl-0.2.8.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
82
- pydpm_xl-0.2.8.dist-info/entry_points.txt,sha256=6DDmBfw-AjtgvMHgq_I730i_LAAs_7-N3C95HD_bRr4,47
83
- pydpm_xl-0.2.8.dist-info/top_level.txt,sha256=495PvWZRoKl2NvbQU25W7dqWIBHqY-mFMPt83uxPpcM,7
84
- pydpm_xl-0.2.8.dist-info/RECORD,,
79
+ pydpm_xl-0.2.10.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
80
+ pydpm_xl-0.2.10.dist-info/METADATA,sha256=SPIgOJT90xDaqE4CYrE2VIsWXOAzGmXbLlI8Yl7yv9I,9154
81
+ pydpm_xl-0.2.10.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
82
+ pydpm_xl-0.2.10.dist-info/entry_points.txt,sha256=6DDmBfw-AjtgvMHgq_I730i_LAAs_7-N3C95HD_bRr4,47
83
+ pydpm_xl-0.2.10.dist-info/top_level.txt,sha256=495PvWZRoKl2NvbQU25W7dqWIBHqY-mFMPt83uxPpcM,7
84
+ pydpm_xl-0.2.10.dist-info/RECORD,,