pydpm_xl 0.2.5rc2__py3-none-any.whl → 0.2.6__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 +1 -1
- py_dpm/api/__init__.py +0 -2
- py_dpm/api/dpm/data_dictionary.py +2 -0
- py_dpm/api/dpm/explorer.py +75 -1
- py_dpm/api/dpm_xl/__init__.py +0 -2
- py_dpm/api/dpm_xl/ast_generator.py +1051 -205
- py_dpm/api/dpm_xl/complete_ast.py +45 -46
- py_dpm/cli/main.py +9 -9
- py_dpm/dpm/models.py +263 -7
- py_dpm/dpm/queries/explorer_queries.py +119 -0
- py_dpm/dpm_xl/utils/scopes_calculator.py +86 -30
- py_dpm/dpm_xl/utils/serialization.py +5 -3
- py_dpm/instance/instance.py +1 -0
- {pydpm_xl-0.2.5rc2.dist-info → pydpm_xl-0.2.6.dist-info}/METADATA +1 -1
- {pydpm_xl-0.2.5rc2.dist-info → pydpm_xl-0.2.6.dist-info}/RECORD +19 -19
- {pydpm_xl-0.2.5rc2.dist-info → pydpm_xl-0.2.6.dist-info}/WHEEL +1 -1
- {pydpm_xl-0.2.5rc2.dist-info → pydpm_xl-0.2.6.dist-info}/entry_points.txt +0 -0
- {pydpm_xl-0.2.5rc2.dist-info → pydpm_xl-0.2.6.dist-info}/licenses/LICENSE +0 -0
- {pydpm_xl-0.2.5rc2.dist-info → pydpm_xl-0.2.6.dist-info}/top_level.txt +0 -0
|
@@ -6,7 +6,7 @@ This module provides a clean, abstracted interface for generating ASTs from DPM-
|
|
|
6
6
|
without exposing internal complexity or version compatibility issues.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
from typing import Dict, Any, Optional, List, Union
|
|
9
|
+
from typing import Dict, Any, Optional, List, Union, Tuple
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
import json
|
|
12
12
|
from datetime import datetime
|
|
@@ -139,24 +139,6 @@ class ASTGeneratorAPI:
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
def parse_batch(self, expressions: List[str]) -> List[Dict[str, Any]]:
|
|
143
|
-
"""
|
|
144
|
-
Parse multiple expressions efficiently.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
expressions: List of DPM-XL expression strings
|
|
148
|
-
|
|
149
|
-
Returns:
|
|
150
|
-
List of parse results (same format as parse_expression)
|
|
151
|
-
"""
|
|
152
|
-
results = []
|
|
153
|
-
for i, expr in enumerate(expressions):
|
|
154
|
-
result = self.parse_expression(expr)
|
|
155
|
-
result['metadata']['batch_index'] = i
|
|
156
|
-
results.append(result)
|
|
157
|
-
|
|
158
|
-
return results
|
|
159
|
-
|
|
160
142
|
def validate_expression(self, expression: str) -> Dict[str, Any]:
|
|
161
143
|
"""
|
|
162
144
|
Validate expression syntax without full parsing.
|
|
@@ -333,43 +315,42 @@ class ASTGeneratorAPI:
|
|
|
333
315
|
"data_populated": False,
|
|
334
316
|
}
|
|
335
317
|
|
|
336
|
-
|
|
318
|
+
# ============================================================================
|
|
319
|
+
# Enriched AST Generation (requires database)
|
|
320
|
+
# ============================================================================
|
|
321
|
+
|
|
322
|
+
def _normalize_expressions_input(
|
|
337
323
|
self,
|
|
338
|
-
expressions: List[str]
|
|
339
|
-
|
|
340
|
-
) -> List[Dict[str, Any]]:
|
|
324
|
+
expressions: Union[str, List[Tuple[str, str, Optional[str]]]]
|
|
325
|
+
) -> List[Tuple[str, str, Optional[str]]]:
|
|
341
326
|
"""
|
|
342
|
-
|
|
327
|
+
Normalize input to list of (expression, operation_code, precondition) tuples.
|
|
328
|
+
|
|
329
|
+
Supports:
|
|
330
|
+
- Single expression string: "expr" -> [("expr", "default_code", None)]
|
|
331
|
+
- List of tuples: [("expr1", "op1", "precond1"), ("expr2", "op2", None)]
|
|
343
332
|
|
|
344
333
|
Args:
|
|
345
|
-
expressions:
|
|
346
|
-
release_id: Optional release ID to filter database lookups by specific release.
|
|
347
|
-
If None, uses all available data (release-agnostic).
|
|
334
|
+
expressions: Either a single expression string or a list of tuples
|
|
348
335
|
|
|
349
336
|
Returns:
|
|
350
|
-
|
|
337
|
+
List of (expression, operation_code, precondition) tuples
|
|
351
338
|
"""
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
result["batch_index"] = i
|
|
356
|
-
results.append(result)
|
|
357
|
-
return results
|
|
358
|
-
|
|
359
|
-
# ============================================================================
|
|
360
|
-
# Enriched AST Generation (requires database)
|
|
361
|
-
# ============================================================================
|
|
339
|
+
if isinstance(expressions, str):
|
|
340
|
+
return [(expressions, "default_code", None)]
|
|
341
|
+
return expressions
|
|
362
342
|
|
|
363
343
|
def generate_enriched_ast(
|
|
364
344
|
self,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
operation_code: Optional[str] = None,
|
|
345
|
+
expressions: Union[str, List[Tuple[str, str, Optional[str]]]],
|
|
346
|
+
release_code: Optional[str] = None,
|
|
368
347
|
table_context: Optional[Dict[str, Any]] = None,
|
|
369
|
-
precondition: Optional[str] = None,
|
|
370
348
|
release_id: Optional[int] = None,
|
|
371
349
|
output_path: Optional[Union[str, Path]] = None,
|
|
372
350
|
primary_module_vid: Optional[int] = None,
|
|
351
|
+
module_code: Optional[str] = None,
|
|
352
|
+
preferred_module_dependencies: Optional[List[str]] = None,
|
|
353
|
+
module_version_number: Optional[str] = None,
|
|
373
354
|
) -> Dict[str, Any]:
|
|
374
355
|
"""
|
|
375
356
|
Generate enriched, engine-ready AST with framework structure (Level 3).
|
|
@@ -378,6 +359,9 @@ class ASTGeneratorAPI:
|
|
|
378
359
|
framework structure with operations, variables, tables, and preconditions sections.
|
|
379
360
|
This is the format required by business rule execution engines.
|
|
380
361
|
|
|
362
|
+
Supports both single expressions (for backward compatibility) and multiple
|
|
363
|
+
expression/operation/precondition tuples for generating scripts with multiple operations.
|
|
364
|
+
|
|
381
365
|
**What you get:**
|
|
382
366
|
- Everything from generate_complete_ast() PLUS:
|
|
383
367
|
- Framework structure: operations, variables, tables, preconditions
|
|
@@ -392,13 +376,17 @@ class ASTGeneratorAPI:
|
|
|
392
376
|
- Module exports with cross-module dependency tracking
|
|
393
377
|
|
|
394
378
|
Args:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
379
|
+
expressions: Either a single DPM-XL expression string (backward compatible),
|
|
380
|
+
or a list of tuples: [(expression, operation_code, precondition), ...].
|
|
381
|
+
Each tuple contains:
|
|
382
|
+
- expression (str): The DPM-XL expression (required)
|
|
383
|
+
- operation_code (str): The operation code (required)
|
|
384
|
+
- precondition (Optional[str]): Optional precondition reference (e.g., {v_F_44_04})
|
|
385
|
+
release_code: Optional release code (e.g., "4.0", "4.1", "4.2").
|
|
386
|
+
Mutually exclusive with release_id and module_version_number.
|
|
398
387
|
table_context: Optional table context dict with keys: 'table', 'columns', 'rows', 'sheets', 'default', 'interval'
|
|
399
|
-
precondition: Optional precondition variable reference (e.g., {v_F_44_04})
|
|
400
388
|
release_id: Optional release ID to filter database lookups by specific release.
|
|
401
|
-
|
|
389
|
+
Mutually exclusive with release_code and module_version_number.
|
|
402
390
|
output_path: Optional path (string or Path) to save the enriched_ast as JSON file.
|
|
403
391
|
If provided, the enriched_ast will be automatically saved to this location.
|
|
404
392
|
primary_module_vid: Optional module version ID of the module being exported.
|
|
@@ -406,6 +394,18 @@ class ASTGeneratorAPI:
|
|
|
406
394
|
other modules will be identified and added to dependency_modules and
|
|
407
395
|
cross_instance_dependencies fields. If None, cross-module detection uses
|
|
408
396
|
the first table's module as the primary module.
|
|
397
|
+
module_code: Optional module code (e.g., "FINREP9") to specify the main module.
|
|
398
|
+
The main module's URL will be used as the root key of the output.
|
|
399
|
+
If provided, this takes precedence over primary_module_vid for determining
|
|
400
|
+
the main module.
|
|
401
|
+
preferred_module_dependencies: Optional list of module codes to prefer when
|
|
402
|
+
multiple dependency scopes are possible. If a table belongs to multiple modules,
|
|
403
|
+
the module in this list will be selected as the dependency.
|
|
404
|
+
module_version_number: Optional module version number (e.g., "4.1.0") to specify
|
|
405
|
+
which version of the module to use. Requires module_code to be specified.
|
|
406
|
+
Mutually exclusive with release_code and release_id.
|
|
407
|
+
If none of release_code, release_id, or module_version_number are provided,
|
|
408
|
+
the latest (active) module version is used.
|
|
409
409
|
|
|
410
410
|
Returns:
|
|
411
411
|
dict: {
|
|
@@ -414,51 +414,68 @@ class ASTGeneratorAPI:
|
|
|
414
414
|
'error': str # Error message if failed
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
Raises:
|
|
418
|
+
ValueError: If more than one of release_id, release_code, or module_version_number
|
|
419
|
+
are specified; if module_version_number is specified without module_code; or if
|
|
420
|
+
no operation scope belongs to the specified module.
|
|
421
|
+
|
|
417
422
|
Example:
|
|
418
423
|
>>> generator = ASTGeneratorAPI(database_path="data.db")
|
|
424
|
+
>>> # Single expression (backward compatible)
|
|
419
425
|
>>> result = generator.generate_enriched_ast(
|
|
420
426
|
... "{tF_01.00, r0010, c0010}",
|
|
421
|
-
...
|
|
422
|
-
... operation_code="my_validation"
|
|
427
|
+
... release_code="4.2",
|
|
423
428
|
... )
|
|
424
|
-
>>> # result['enriched_ast'] contains framework structure ready for engines
|
|
425
429
|
>>>
|
|
426
|
-
>>> #
|
|
430
|
+
>>> # Multiple expressions with operations and preconditions
|
|
427
431
|
>>> result = generator.generate_enriched_ast(
|
|
428
|
-
...
|
|
429
|
-
...
|
|
430
|
-
...
|
|
431
|
-
...
|
|
432
|
-
...
|
|
432
|
+
... [
|
|
433
|
+
... ("{tF_01.00, r0010, c0010} = 0", "v1234_m", None),
|
|
434
|
+
... ("{tF_01.00, r0020, c0010} > 0", "v1235_m", "{v_F_44_04}"),
|
|
435
|
+
... ("{tF_01.00, r0030, c0010} >= 0", "v1236_m", "{v_F_44_04}"), # Same precondition, deduplicated
|
|
436
|
+
... ],
|
|
437
|
+
... release_code="4.2",
|
|
438
|
+
... module_code="FINREP9",
|
|
433
439
|
... )
|
|
434
|
-
>>> # result['enriched_ast']['dependency_modules'] contains external module info
|
|
435
|
-
>>> # result['enriched_ast']['dependency_information']['cross_instance_dependencies']
|
|
436
|
-
>>> # contains list of external module dependencies
|
|
437
440
|
"""
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
+
# Validate mutually exclusive parameters
|
|
442
|
+
version_params = [release_id, release_code, module_version_number]
|
|
443
|
+
if sum(p is not None for p in version_params) > 1:
|
|
444
|
+
raise ValueError(
|
|
445
|
+
"Specify a maximum of one of release_id, release_code, or module_version_number."
|
|
446
|
+
)
|
|
441
447
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
+
# Validate module_version_number requires module_code
|
|
449
|
+
if module_version_number is not None and module_code is None:
|
|
450
|
+
raise ValueError(
|
|
451
|
+
"module_version_number requires module_code to be specified."
|
|
452
|
+
)
|
|
448
453
|
|
|
449
|
-
|
|
450
|
-
|
|
454
|
+
# Resolve version parameters to release_id
|
|
455
|
+
effective_release_id = release_id
|
|
456
|
+
effective_release_code = release_code
|
|
451
457
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
458
|
+
if release_code is not None:
|
|
459
|
+
effective_release_id = self._resolve_release_code(release_code)
|
|
460
|
+
elif module_version_number is not None:
|
|
461
|
+
# Resolve module_version_number to release_id
|
|
462
|
+
effective_release_id, effective_release_code = self._resolve_module_version(
|
|
463
|
+
module_code, module_version_number
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# Normalize input to list of tuples
|
|
467
|
+
expression_tuples = self._normalize_expressions_input(expressions)
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
# Enrich with framework structure for multiple expressions
|
|
471
|
+
enriched_ast = self._enrich_ast_with_metadata_multi(
|
|
472
|
+
expression_tuples=expression_tuples,
|
|
473
|
+
table_context=table_context,
|
|
474
|
+
release_code=effective_release_code,
|
|
475
|
+
release_id=effective_release_id,
|
|
461
476
|
primary_module_vid=primary_module_vid,
|
|
477
|
+
module_code=module_code,
|
|
478
|
+
preferred_module_dependencies=preferred_module_dependencies,
|
|
462
479
|
)
|
|
463
480
|
|
|
464
481
|
# Save to file if output_path is provided
|
|
@@ -732,11 +749,13 @@ class ASTGeneratorAPI:
|
|
|
732
749
|
ast_dict: Dict[str, Any],
|
|
733
750
|
expression: str,
|
|
734
751
|
context: Optional[Dict[str, Any]],
|
|
735
|
-
|
|
752
|
+
release_code: Optional[str] = None,
|
|
736
753
|
operation_code: Optional[str] = None,
|
|
737
754
|
precondition: Optional[str] = None,
|
|
738
755
|
release_id: Optional[int] = None,
|
|
739
756
|
primary_module_vid: Optional[int] = None,
|
|
757
|
+
module_code: Optional[str] = None,
|
|
758
|
+
preferred_module_dependencies: Optional[List[str]] = None,
|
|
740
759
|
) -> Dict[str, Any]:
|
|
741
760
|
"""
|
|
742
761
|
Add framework structure (operations, variables, tables, preconditions) to complete AST.
|
|
@@ -747,7 +766,7 @@ class ASTGeneratorAPI:
|
|
|
747
766
|
ast_dict: Complete AST dictionary
|
|
748
767
|
expression: Original DPM-XL expression
|
|
749
768
|
context: Context dict with table, rows, columns, sheets, default, interval
|
|
750
|
-
|
|
769
|
+
release_code: Release code (e.g., "4.2")
|
|
751
770
|
operation_code: Operation code (defaults to "default_code")
|
|
752
771
|
precondition: Precondition variable reference (e.g., {v_F_44_04})
|
|
753
772
|
release_id: Optional release ID to filter database lookups
|
|
@@ -766,25 +785,38 @@ class ASTGeneratorAPI:
|
|
|
766
785
|
# Get current date for framework structure
|
|
767
786
|
current_date = datetime.now().strftime("%Y-%m-%d")
|
|
768
787
|
|
|
788
|
+
# Detect primary module from the expression (or use provided module_code)
|
|
789
|
+
primary_module_info = self._get_primary_module_info(
|
|
790
|
+
expression=expression,
|
|
791
|
+
primary_module_vid=primary_module_vid,
|
|
792
|
+
release_id=release_id,
|
|
793
|
+
module_code=module_code,
|
|
794
|
+
)
|
|
795
|
+
|
|
769
796
|
# Query database for release information
|
|
770
|
-
release_info = self._get_release_info(
|
|
797
|
+
release_info = self._get_release_info(release_code, engine)
|
|
771
798
|
|
|
772
|
-
# Build module info
|
|
799
|
+
# Build module info using detected primary module or defaults
|
|
773
800
|
module_info = {
|
|
774
|
-
"module_code": "default",
|
|
775
|
-
"module_version": "1.0.0",
|
|
776
|
-
"framework_code": "default",
|
|
801
|
+
"module_code": primary_module_info.get("module_code", "default"),
|
|
802
|
+
"module_version": primary_module_info.get("module_version", "1.0.0"),
|
|
803
|
+
"framework_code": primary_module_info.get("framework_code", "default"),
|
|
777
804
|
"dpm_release": {
|
|
778
805
|
"release": release_info["release"],
|
|
779
806
|
"publication_date": release_info["publication_date"],
|
|
780
807
|
},
|
|
781
|
-
"dates": {
|
|
808
|
+
"dates": {
|
|
809
|
+
"from": primary_module_info.get("from_date", "2001-01-01"),
|
|
810
|
+
"to": primary_module_info.get("to_date"),
|
|
811
|
+
},
|
|
782
812
|
}
|
|
783
813
|
|
|
784
814
|
# Add coordinates to AST data entries
|
|
785
815
|
ast_with_coords = self._add_coordinates_to_ast(ast_dict, context)
|
|
786
816
|
|
|
787
817
|
# Build operations section
|
|
818
|
+
# Use module's from_date for from_submission_date (fallback to current date)
|
|
819
|
+
submission_date = primary_module_info.get("from_date", current_date)
|
|
788
820
|
operations = {
|
|
789
821
|
operation_code: {
|
|
790
822
|
"version_id": hash(expression) % 10000,
|
|
@@ -792,56 +824,118 @@ class ASTGeneratorAPI:
|
|
|
792
824
|
"expression": expression,
|
|
793
825
|
"root_operator_id": 24, # Default for now
|
|
794
826
|
"ast": ast_with_coords,
|
|
795
|
-
"from_submission_date":
|
|
827
|
+
"from_submission_date": submission_date,
|
|
796
828
|
"severity": "Error",
|
|
797
829
|
}
|
|
798
830
|
}
|
|
799
831
|
|
|
800
832
|
# Build variables section by extracting from the complete AST
|
|
801
|
-
|
|
833
|
+
# This gives us the tables referenced in the expression
|
|
834
|
+
_, variables_by_table = self._extract_variables_from_ast(ast_with_coords)
|
|
835
|
+
|
|
836
|
+
# Clean extra fields from data entries (after extraction, as it uses data_type)
|
|
837
|
+
self._clean_ast_data_entries(ast_with_coords)
|
|
802
838
|
|
|
803
|
-
|
|
839
|
+
all_variables = {}
|
|
804
840
|
tables = {}
|
|
805
841
|
|
|
806
|
-
#
|
|
807
|
-
|
|
808
|
-
|
|
842
|
+
# Get tables_with_modules to filter tables by primary module
|
|
843
|
+
tables_with_modules = primary_module_info.get("tables_with_modules", [])
|
|
844
|
+
primary_module_vid = primary_module_info.get("module_vid")
|
|
845
|
+
|
|
846
|
+
# Build mapping of table_code -> module_vid for filtering
|
|
847
|
+
table_to_module = {}
|
|
848
|
+
for table_info in tables_with_modules:
|
|
849
|
+
table_code = table_info.get("code", "")
|
|
850
|
+
module_vid = table_info.get("module_vid")
|
|
851
|
+
if table_code and module_vid:
|
|
852
|
+
table_to_module[table_code] = module_vid
|
|
853
|
+
|
|
854
|
+
# Initialize DataDictionaryAPI to query open keys and all variables
|
|
855
|
+
from py_dpm.api.dpm import DataDictionaryAPI
|
|
856
|
+
data_dict_api = DataDictionaryAPI(
|
|
857
|
+
database_path=self.database_path,
|
|
858
|
+
connection_url=self.connection_url
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
# Build tables with ALL variables from database (not just from expression)
|
|
862
|
+
# Only include tables belonging to the primary module
|
|
863
|
+
for table_code in variables_by_table.keys():
|
|
864
|
+
# Check if this table belongs to the primary module
|
|
865
|
+
table_module_vid = table_to_module.get(table_code)
|
|
866
|
+
if table_module_vid and table_module_vid != primary_module_vid:
|
|
867
|
+
# This table belongs to a different module, skip it for the main tables section
|
|
868
|
+
continue
|
|
869
|
+
|
|
870
|
+
# Get table version info to get table_vid
|
|
871
|
+
table_info = data_dict_api.get_table_version(table_code, release_id)
|
|
872
|
+
|
|
873
|
+
if table_info and table_info.get("table_vid"):
|
|
874
|
+
table_vid = table_info["table_vid"]
|
|
875
|
+
# Get ALL variables for this table from database
|
|
876
|
+
table_variables = data_dict_api.get_all_variables_for_table(table_vid)
|
|
877
|
+
else:
|
|
878
|
+
# Fallback to expression variables if table not found
|
|
879
|
+
table_variables = variables_by_table[table_code]
|
|
880
|
+
|
|
881
|
+
# Query open keys for this table
|
|
882
|
+
open_keys_list = data_dict_api.get_open_keys_for_table(table_code, release_id)
|
|
883
|
+
open_keys = {item["property_code"]: item["data_type_code"] for item in open_keys_list}
|
|
884
|
+
|
|
885
|
+
tables[table_code] = {"variables": table_variables, "open_keys": open_keys}
|
|
886
|
+
|
|
887
|
+
# Add table variables to all_variables
|
|
888
|
+
all_variables.update(table_variables)
|
|
889
|
+
|
|
890
|
+
data_dict_api.close()
|
|
809
891
|
|
|
810
892
|
# Build preconditions
|
|
811
893
|
preconditions = {}
|
|
812
894
|
precondition_variables = {}
|
|
813
895
|
|
|
814
|
-
if precondition
|
|
896
|
+
if precondition:
|
|
815
897
|
preconditions, precondition_variables = self._build_preconditions(
|
|
816
898
|
precondition=precondition,
|
|
817
899
|
context=context,
|
|
818
900
|
operation_code=operation_code,
|
|
819
|
-
|
|
901
|
+
release_id=release_id,
|
|
820
902
|
)
|
|
821
903
|
|
|
822
904
|
# Detect cross-module dependencies
|
|
905
|
+
# Use ALL variables from tables (not just expression variables)
|
|
906
|
+
full_variables_by_table = {
|
|
907
|
+
table_code: table_data["variables"]
|
|
908
|
+
for table_code, table_data in tables.items()
|
|
909
|
+
}
|
|
910
|
+
# Use module_vid from primary_module_info (may have been resolved from module_code)
|
|
911
|
+
resolved_primary_module_vid = primary_module_info.get("module_vid") or primary_module_vid
|
|
823
912
|
dependency_modules, cross_instance_dependencies = self._detect_cross_module_dependencies(
|
|
824
913
|
expression=expression,
|
|
825
|
-
variables_by_table=
|
|
826
|
-
primary_module_vid=
|
|
914
|
+
variables_by_table=full_variables_by_table,
|
|
915
|
+
primary_module_vid=resolved_primary_module_vid,
|
|
827
916
|
operation_code=operation_code,
|
|
828
917
|
release_id=release_id,
|
|
918
|
+
preferred_module_dependencies=preferred_module_dependencies,
|
|
829
919
|
)
|
|
830
920
|
|
|
831
921
|
# Build dependency information
|
|
922
|
+
# intra_instance_validations should be empty for cross-module operations
|
|
923
|
+
# (operations that have cross_instance_dependencies)
|
|
924
|
+
is_cross_module = bool(cross_instance_dependencies)
|
|
832
925
|
dependency_info = {
|
|
833
|
-
"intra_instance_validations": [operation_code],
|
|
926
|
+
"intra_instance_validations": [] if is_cross_module else [operation_code],
|
|
834
927
|
"cross_instance_dependencies": cross_instance_dependencies,
|
|
835
928
|
}
|
|
836
929
|
|
|
837
930
|
# Build complete structure
|
|
838
|
-
namespace
|
|
931
|
+
# Use module URI as namespace if available, otherwise use "default_module"
|
|
932
|
+
namespace = primary_module_info.get("module_uri", "default_module")
|
|
839
933
|
|
|
840
934
|
return {
|
|
841
935
|
namespace: {
|
|
842
936
|
**module_info,
|
|
843
937
|
"operations": operations,
|
|
844
|
-
"variables":
|
|
938
|
+
"variables": all_variables,
|
|
845
939
|
"tables": tables,
|
|
846
940
|
"preconditions": preconditions,
|
|
847
941
|
"precondition_variables": precondition_variables,
|
|
@@ -850,32 +944,591 @@ class ASTGeneratorAPI:
|
|
|
850
944
|
}
|
|
851
945
|
}
|
|
852
946
|
|
|
853
|
-
def
|
|
947
|
+
def _enrich_ast_with_metadata_multi(
|
|
948
|
+
self,
|
|
949
|
+
expression_tuples: List[Tuple[str, str, Optional[str]]],
|
|
950
|
+
table_context: Optional[Dict[str, Any]],
|
|
951
|
+
release_code: Optional[str] = None,
|
|
952
|
+
release_id: Optional[int] = None,
|
|
953
|
+
primary_module_vid: Optional[int] = None,
|
|
954
|
+
module_code: Optional[str] = None,
|
|
955
|
+
preferred_module_dependencies: Optional[List[str]] = None,
|
|
956
|
+
) -> Dict[str, Any]:
|
|
957
|
+
"""
|
|
958
|
+
Add framework structure for multiple expressions (operations, variables, tables, preconditions).
|
|
959
|
+
|
|
960
|
+
This creates the engine-ready format with all metadata sections, aggregating
|
|
961
|
+
multiple expressions into a single script structure.
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
expression_tuples: List of (expression, operation_code, precondition) tuples
|
|
965
|
+
table_context: Context dict with table, rows, columns, sheets, default, interval
|
|
966
|
+
release_code: Release code (e.g., "4.2")
|
|
967
|
+
release_id: Optional release ID to filter database lookups
|
|
968
|
+
primary_module_vid: Module VID being exported (to identify external dependencies)
|
|
969
|
+
module_code: Optional module code to specify the main module
|
|
970
|
+
preferred_module_dependencies: Optional list of module codes to prefer for dependencies
|
|
971
|
+
|
|
972
|
+
Returns:
|
|
973
|
+
Dict with the enriched AST structure
|
|
974
|
+
|
|
975
|
+
Raises:
|
|
976
|
+
ValueError: If no operation scope belongs to the specified module
|
|
977
|
+
"""
|
|
978
|
+
from py_dpm.dpm.utils import get_engine
|
|
979
|
+
from py_dpm.api.dpm import DataDictionaryAPI
|
|
980
|
+
|
|
981
|
+
# Initialize database connection
|
|
982
|
+
engine = get_engine(database_path=self.database_path, connection_url=self.connection_url)
|
|
983
|
+
|
|
984
|
+
# Get current date for framework structure
|
|
985
|
+
current_date = datetime.now().strftime("%Y-%m-%d")
|
|
986
|
+
|
|
987
|
+
# Aggregated structures
|
|
988
|
+
all_operations = {}
|
|
989
|
+
all_variables = {}
|
|
990
|
+
all_tables = {}
|
|
991
|
+
all_preconditions = {}
|
|
992
|
+
all_precondition_variables = {}
|
|
993
|
+
all_dependency_modules = {}
|
|
994
|
+
all_cross_instance_deps = []
|
|
995
|
+
all_intra_instance_ops = []
|
|
996
|
+
|
|
997
|
+
# Track processed preconditions to avoid duplicates
|
|
998
|
+
# Maps precondition string -> list of precondition keys generated from it
|
|
999
|
+
processed_preconditions: Dict[str, List[str]] = {}
|
|
1000
|
+
|
|
1001
|
+
# Track all tables with their modules for validation
|
|
1002
|
+
all_tables_with_modules = []
|
|
1003
|
+
|
|
1004
|
+
# Flag to track if at least one operation belongs to the primary module
|
|
1005
|
+
has_primary_module_operation = False
|
|
1006
|
+
|
|
1007
|
+
# Initialize DataDictionaryAPI once for all expressions
|
|
1008
|
+
data_dict_api = DataDictionaryAPI(
|
|
1009
|
+
database_path=self.database_path,
|
|
1010
|
+
connection_url=self.connection_url
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
# Primary module info will be determined from the first expression or module_code
|
|
1014
|
+
primary_module_info = None
|
|
1015
|
+
namespace = None
|
|
1016
|
+
|
|
1017
|
+
try:
|
|
1018
|
+
for idx, (expression, operation_code, precondition) in enumerate(expression_tuples):
|
|
1019
|
+
# Generate complete AST for this expression
|
|
1020
|
+
complete_result = self.generate_complete_ast(expression, release_id=release_id)
|
|
1021
|
+
|
|
1022
|
+
if not complete_result["success"]:
|
|
1023
|
+
raise ValueError(
|
|
1024
|
+
f"Failed to generate complete AST for expression {idx + 1} "
|
|
1025
|
+
f"(operation '{operation_code}'): {complete_result['error']}"
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
complete_ast = complete_result["ast"]
|
|
1029
|
+
context = complete_result.get("context") or table_context
|
|
1030
|
+
|
|
1031
|
+
# Get primary module info from first expression (or use module_code)
|
|
1032
|
+
if primary_module_info is None:
|
|
1033
|
+
primary_module_info = self._get_primary_module_info(
|
|
1034
|
+
expression=expression,
|
|
1035
|
+
primary_module_vid=primary_module_vid,
|
|
1036
|
+
release_id=release_id,
|
|
1037
|
+
module_code=module_code,
|
|
1038
|
+
)
|
|
1039
|
+
namespace = primary_module_info.get("module_uri", "default_module")
|
|
1040
|
+
|
|
1041
|
+
# Add coordinates to AST data entries
|
|
1042
|
+
ast_with_coords = self._add_coordinates_to_ast(complete_ast, context)
|
|
1043
|
+
|
|
1044
|
+
# Build operation entry
|
|
1045
|
+
submission_date = primary_module_info.get("from_date", current_date)
|
|
1046
|
+
all_operations[operation_code] = {
|
|
1047
|
+
"version_id": hash(expression) % 10000,
|
|
1048
|
+
"code": operation_code,
|
|
1049
|
+
"expression": expression,
|
|
1050
|
+
"root_operator_id": 24,
|
|
1051
|
+
"ast": ast_with_coords,
|
|
1052
|
+
"from_submission_date": submission_date,
|
|
1053
|
+
"severity": "Error",
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
# Extract variables from this expression's AST
|
|
1057
|
+
_, variables_by_table = self._extract_variables_from_ast(ast_with_coords)
|
|
1058
|
+
|
|
1059
|
+
# Clean extra fields from data entries
|
|
1060
|
+
self._clean_ast_data_entries(ast_with_coords)
|
|
1061
|
+
|
|
1062
|
+
# Get tables with modules for this expression
|
|
1063
|
+
from py_dpm.api.dpm_xl.operation_scopes import OperationScopesAPI
|
|
1064
|
+
scopes_api = OperationScopesAPI(
|
|
1065
|
+
database_path=self.database_path,
|
|
1066
|
+
connection_url=self.connection_url
|
|
1067
|
+
)
|
|
1068
|
+
tables_with_modules = scopes_api.get_tables_with_metadata_from_expression(
|
|
1069
|
+
expression=expression,
|
|
1070
|
+
release_id=release_id
|
|
1071
|
+
)
|
|
1072
|
+
all_tables_with_modules.extend(tables_with_modules)
|
|
1073
|
+
|
|
1074
|
+
# Build mapping of table_code -> module_vid
|
|
1075
|
+
# Prefer the module VID that matches the detected primary module
|
|
1076
|
+
table_to_module = {}
|
|
1077
|
+
primary_module_code = primary_module_info.get("module_code")
|
|
1078
|
+
|
|
1079
|
+
# First pass: record mappings for tables belonging to the primary module (by code)
|
|
1080
|
+
if primary_module_code:
|
|
1081
|
+
for table_info in tables_with_modules:
|
|
1082
|
+
table_code = table_info.get("code", "")
|
|
1083
|
+
table_module_vid = table_info.get("module_vid")
|
|
1084
|
+
table_module_code = table_info.get("module_code")
|
|
1085
|
+
if (
|
|
1086
|
+
table_code
|
|
1087
|
+
and table_module_vid
|
|
1088
|
+
and table_module_code == primary_module_code
|
|
1089
|
+
):
|
|
1090
|
+
table_to_module[table_code] = table_module_vid
|
|
1091
|
+
|
|
1092
|
+
# Second pass: fill in any remaining tables with the first available module VID
|
|
1093
|
+
for table_info in tables_with_modules:
|
|
1094
|
+
table_code = table_info.get("code", "")
|
|
1095
|
+
table_module_vid = table_info.get("module_vid")
|
|
1096
|
+
if table_code and table_module_vid and table_code not in table_to_module:
|
|
1097
|
+
table_to_module[table_code] = table_module_vid
|
|
1098
|
+
|
|
1099
|
+
resolved_primary_module_vid = primary_module_info.get("module_vid") or primary_module_vid
|
|
1100
|
+
|
|
1101
|
+
# Process tables from this expression
|
|
1102
|
+
for table_code in variables_by_table.keys():
|
|
1103
|
+
# Check if this table belongs to the primary module
|
|
1104
|
+
table_module_vid = table_to_module.get(table_code)
|
|
1105
|
+
|
|
1106
|
+
if table_module_vid and table_module_vid != resolved_primary_module_vid:
|
|
1107
|
+
# This table belongs to a different module, skip for main tables
|
|
1108
|
+
continue
|
|
1109
|
+
|
|
1110
|
+
# Skip if we already have this table
|
|
1111
|
+
if table_code in all_tables:
|
|
1112
|
+
# Table already added, it passed the module filter before
|
|
1113
|
+
has_primary_module_operation = True
|
|
1114
|
+
continue
|
|
1115
|
+
|
|
1116
|
+
# Get table version info
|
|
1117
|
+
table_info = data_dict_api.get_table_version(table_code, release_id)
|
|
1118
|
+
|
|
1119
|
+
if table_info and table_info.get("table_vid"):
|
|
1120
|
+
table_vid = table_info["table_vid"]
|
|
1121
|
+
table_variables = data_dict_api.get_all_variables_for_table(table_vid)
|
|
1122
|
+
else:
|
|
1123
|
+
table_variables = variables_by_table[table_code]
|
|
1124
|
+
|
|
1125
|
+
# Query open keys for this table
|
|
1126
|
+
open_keys_list = data_dict_api.get_open_keys_for_table(table_code, release_id)
|
|
1127
|
+
open_keys = {item["property_code"]: item["data_type_code"] for item in open_keys_list}
|
|
1128
|
+
|
|
1129
|
+
all_tables[table_code] = {"variables": table_variables, "open_keys": open_keys}
|
|
1130
|
+
all_variables.update(table_variables)
|
|
1131
|
+
|
|
1132
|
+
# We successfully added a table that passed the module filter
|
|
1133
|
+
# This means at least one operation references the primary module
|
|
1134
|
+
has_primary_module_operation = True
|
|
1135
|
+
|
|
1136
|
+
# Handle precondition (deduplicate by precondition string)
|
|
1137
|
+
if precondition and precondition not in processed_preconditions:
|
|
1138
|
+
preconds, precond_vars = self._build_preconditions(
|
|
1139
|
+
precondition=precondition,
|
|
1140
|
+
context=context,
|
|
1141
|
+
operation_code=operation_code,
|
|
1142
|
+
release_id=release_id,
|
|
1143
|
+
)
|
|
1144
|
+
# Track which keys were generated for this precondition string
|
|
1145
|
+
processed_preconditions[precondition] = list(preconds.keys())
|
|
1146
|
+
# Merge preconditions
|
|
1147
|
+
for precond_key, precond_data in preconds.items():
|
|
1148
|
+
if precond_key not in all_preconditions:
|
|
1149
|
+
all_preconditions[precond_key] = precond_data
|
|
1150
|
+
else:
|
|
1151
|
+
# Add this operation to affected_operations if not already there
|
|
1152
|
+
if operation_code not in all_preconditions[precond_key]["affected_operations"]:
|
|
1153
|
+
all_preconditions[precond_key]["affected_operations"].append(operation_code)
|
|
1154
|
+
all_precondition_variables.update(precond_vars)
|
|
1155
|
+
elif precondition and precondition in processed_preconditions:
|
|
1156
|
+
# Precondition already processed, add this operation ONLY to the matching precondition(s)
|
|
1157
|
+
matching_keys = processed_preconditions[precondition]
|
|
1158
|
+
for precond_key in matching_keys:
|
|
1159
|
+
if precond_key in all_preconditions:
|
|
1160
|
+
if operation_code not in all_preconditions[precond_key]["affected_operations"]:
|
|
1161
|
+
all_preconditions[precond_key]["affected_operations"].append(operation_code)
|
|
1162
|
+
|
|
1163
|
+
# Detect cross-module dependencies for this expression
|
|
1164
|
+
full_variables_by_table = {
|
|
1165
|
+
table_code: table_data["variables"]
|
|
1166
|
+
for table_code, table_data in all_tables.items()
|
|
1167
|
+
}
|
|
1168
|
+
dep_modules, cross_deps = self._detect_cross_module_dependencies(
|
|
1169
|
+
expression=expression,
|
|
1170
|
+
variables_by_table=full_variables_by_table,
|
|
1171
|
+
primary_module_vid=resolved_primary_module_vid,
|
|
1172
|
+
operation_code=operation_code,
|
|
1173
|
+
release_id=release_id,
|
|
1174
|
+
preferred_module_dependencies=preferred_module_dependencies,
|
|
1175
|
+
)
|
|
1176
|
+
|
|
1177
|
+
# Merge dependency modules (avoid table duplicates)
|
|
1178
|
+
self._merge_dependency_modules(all_dependency_modules, dep_modules)
|
|
1179
|
+
|
|
1180
|
+
# Merge cross-instance dependencies (avoid duplicates)
|
|
1181
|
+
self._merge_cross_instance_dependencies(all_cross_instance_deps, cross_deps)
|
|
1182
|
+
|
|
1183
|
+
# Track intra-instance operations
|
|
1184
|
+
if not cross_deps:
|
|
1185
|
+
all_intra_instance_ops.append(operation_code)
|
|
1186
|
+
|
|
1187
|
+
finally:
|
|
1188
|
+
data_dict_api.close()
|
|
1189
|
+
|
|
1190
|
+
# Validate: at least one operation must belong to the primary module
|
|
1191
|
+
if not has_primary_module_operation and module_code:
|
|
1192
|
+
raise ValueError(
|
|
1193
|
+
f"No operation scope belongs to the specified module '{module_code}'. "
|
|
1194
|
+
"At least one expression must reference tables from the primary module."
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
# Query database for release information
|
|
1198
|
+
release_info = self._get_release_info(release_code, engine)
|
|
1199
|
+
|
|
1200
|
+
# Build module info
|
|
1201
|
+
module_info = {
|
|
1202
|
+
"module_code": primary_module_info.get("module_code", "default"),
|
|
1203
|
+
"module_version": primary_module_info.get("module_version", "1.0.0"),
|
|
1204
|
+
"framework_code": primary_module_info.get("framework_code", "default"),
|
|
1205
|
+
"dpm_release": {
|
|
1206
|
+
"release": release_info["release"],
|
|
1207
|
+
"publication_date": release_info["publication_date"],
|
|
1208
|
+
},
|
|
1209
|
+
"dates": {
|
|
1210
|
+
"from": primary_module_info.get("from_date", "2001-01-01"),
|
|
1211
|
+
"to": primary_module_info.get("to_date"),
|
|
1212
|
+
},
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
# Build dependency information
|
|
1216
|
+
dependency_info = {
|
|
1217
|
+
"intra_instance_validations": all_intra_instance_ops,
|
|
1218
|
+
"cross_instance_dependencies": all_cross_instance_deps,
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return {
|
|
1222
|
+
namespace: {
|
|
1223
|
+
**module_info,
|
|
1224
|
+
"operations": all_operations,
|
|
1225
|
+
"variables": all_variables,
|
|
1226
|
+
"tables": all_tables,
|
|
1227
|
+
"preconditions": all_preconditions,
|
|
1228
|
+
"precondition_variables": all_precondition_variables,
|
|
1229
|
+
"dependency_information": dependency_info,
|
|
1230
|
+
"dependency_modules": all_dependency_modules,
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
def _merge_dependency_modules(
|
|
1235
|
+
self,
|
|
1236
|
+
existing: Dict[str, Any],
|
|
1237
|
+
new: Dict[str, Any]
|
|
1238
|
+
) -> None:
|
|
1239
|
+
"""
|
|
1240
|
+
Merge new dependency_modules into existing, avoiding table duplicates.
|
|
1241
|
+
|
|
1242
|
+
Args:
|
|
1243
|
+
existing: Existing dependency_modules dict (modified in place)
|
|
1244
|
+
new: New dependency_modules dict to merge
|
|
1245
|
+
"""
|
|
1246
|
+
for uri, module_data in new.items():
|
|
1247
|
+
if uri not in existing:
|
|
1248
|
+
existing[uri] = module_data
|
|
1249
|
+
else:
|
|
1250
|
+
# Merge tables (avoid duplicates)
|
|
1251
|
+
for table_code, table_data in module_data.get("tables", {}).items():
|
|
1252
|
+
if table_code not in existing[uri].get("tables", {}):
|
|
1253
|
+
existing[uri].setdefault("tables", {})[table_code] = table_data
|
|
1254
|
+
# Merge variables
|
|
1255
|
+
existing[uri].setdefault("variables", {}).update(
|
|
1256
|
+
module_data.get("variables", {})
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
def _merge_cross_instance_dependencies(
|
|
1260
|
+
self,
|
|
1261
|
+
existing: List[Dict[str, Any]],
|
|
1262
|
+
new: List[Dict[str, Any]]
|
|
1263
|
+
) -> None:
|
|
1264
|
+
"""
|
|
1265
|
+
Merge new cross_instance_dependencies into existing, avoiding duplicates.
|
|
1266
|
+
|
|
1267
|
+
Duplicates are identified by the set of module URIs involved.
|
|
1268
|
+
|
|
1269
|
+
Args:
|
|
1270
|
+
existing: Existing list (modified in place)
|
|
1271
|
+
new: New list to merge
|
|
1272
|
+
"""
|
|
1273
|
+
def get_module_uris(dep: Dict[str, Any]) -> tuple:
|
|
1274
|
+
"""Extract sorted URIs from modules list for deduplication."""
|
|
1275
|
+
modules = dep.get("modules", [])
|
|
1276
|
+
uris = []
|
|
1277
|
+
for m in modules:
|
|
1278
|
+
if isinstance(m, dict):
|
|
1279
|
+
uris.append(m.get("URI", ""))
|
|
1280
|
+
else:
|
|
1281
|
+
uris.append(str(m))
|
|
1282
|
+
return tuple(sorted(uris))
|
|
1283
|
+
|
|
1284
|
+
# Build a set of existing module URI combinations for deduplication
|
|
1285
|
+
existing_module_sets = set()
|
|
1286
|
+
for dep in existing:
|
|
1287
|
+
existing_module_sets.add(get_module_uris(dep))
|
|
1288
|
+
|
|
1289
|
+
for dep in new:
|
|
1290
|
+
dep_uris = get_module_uris(dep)
|
|
1291
|
+
if dep_uris not in existing_module_sets:
|
|
1292
|
+
existing.append(dep)
|
|
1293
|
+
existing_module_sets.add(dep_uris)
|
|
1294
|
+
else:
|
|
1295
|
+
# Merge affected_operations for existing dependency
|
|
1296
|
+
for existing_dep in existing:
|
|
1297
|
+
if get_module_uris(existing_dep) == dep_uris:
|
|
1298
|
+
for op in dep.get("affected_operations", []):
|
|
1299
|
+
if op not in existing_dep.get("affected_operations", []):
|
|
1300
|
+
existing_dep.setdefault("affected_operations", []).append(op)
|
|
1301
|
+
break
|
|
1302
|
+
|
|
1303
|
+
def _get_primary_module_info(
|
|
1304
|
+
self,
|
|
1305
|
+
expression: str,
|
|
1306
|
+
primary_module_vid: Optional[int],
|
|
1307
|
+
release_id: Optional[int],
|
|
1308
|
+
module_code: Optional[str] = None,
|
|
1309
|
+
) -> Dict[str, Any]:
|
|
1310
|
+
"""
|
|
1311
|
+
Detect and return metadata for the primary module from the expression.
|
|
1312
|
+
|
|
1313
|
+
Args:
|
|
1314
|
+
expression: DPM-XL expression
|
|
1315
|
+
primary_module_vid: Optional module VID (if known)
|
|
1316
|
+
release_id: Optional release ID for filtering
|
|
1317
|
+
module_code: Optional module code (e.g., "FINREP9") - takes precedence over
|
|
1318
|
+
primary_module_vid if provided
|
|
1319
|
+
|
|
1320
|
+
Returns:
|
|
1321
|
+
Dict with module_uri, module_code, module_version, framework_code,
|
|
1322
|
+
from_date, to_date, module_vid
|
|
1323
|
+
"""
|
|
1324
|
+
from py_dpm.api.dpm_xl.operation_scopes import OperationScopesAPI
|
|
1325
|
+
from py_dpm.dpm.queries.explorer_queries import ExplorerQuery
|
|
1326
|
+
|
|
1327
|
+
default_info = {
|
|
1328
|
+
"module_uri": "default_module",
|
|
1329
|
+
"module_code": "default",
|
|
1330
|
+
"module_version": "1.0.0",
|
|
1331
|
+
"framework_code": "default",
|
|
1332
|
+
"from_date": "2001-01-01",
|
|
1333
|
+
"to_date": None,
|
|
1334
|
+
"module_vid": None,
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
try:
|
|
1338
|
+
scopes_api = OperationScopesAPI(
|
|
1339
|
+
database_path=self.database_path,
|
|
1340
|
+
connection_url=self.connection_url
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
# Get tables with module metadata from expression
|
|
1344
|
+
tables_with_modules = scopes_api.get_tables_with_metadata_from_expression(
|
|
1345
|
+
expression=expression,
|
|
1346
|
+
release_id=release_id
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
if not tables_with_modules:
|
|
1350
|
+
scopes_api.close()
|
|
1351
|
+
return default_info
|
|
1352
|
+
|
|
1353
|
+
# Determine primary module
|
|
1354
|
+
# Priority: module_code (param) > primary_module_vid > first table
|
|
1355
|
+
primary_table = None
|
|
1356
|
+
|
|
1357
|
+
if module_code:
|
|
1358
|
+
# Find table matching the provided module_code
|
|
1359
|
+
for table_info in tables_with_modules:
|
|
1360
|
+
if table_info.get("module_code") == module_code:
|
|
1361
|
+
primary_table = table_info
|
|
1362
|
+
break
|
|
1363
|
+
elif primary_module_vid:
|
|
1364
|
+
# Find table matching the provided module VID
|
|
1365
|
+
for table_info in tables_with_modules:
|
|
1366
|
+
if table_info.get("module_vid") == primary_module_vid:
|
|
1367
|
+
primary_table = table_info
|
|
1368
|
+
break
|
|
1369
|
+
|
|
1370
|
+
# If no match found, use first table
|
|
1371
|
+
if not primary_table:
|
|
1372
|
+
primary_table = tables_with_modules[0]
|
|
1373
|
+
|
|
1374
|
+
resolved_module_code = primary_table.get("module_code")
|
|
1375
|
+
module_vid = primary_table.get("module_vid")
|
|
1376
|
+
|
|
1377
|
+
# Get module URI
|
|
1378
|
+
try:
|
|
1379
|
+
module_uri = ExplorerQuery.get_module_url(
|
|
1380
|
+
scopes_api.session,
|
|
1381
|
+
module_code=resolved_module_code,
|
|
1382
|
+
release_id=release_id,
|
|
1383
|
+
)
|
|
1384
|
+
# Remove .json extension if present
|
|
1385
|
+
if module_uri and module_uri.endswith(".json"):
|
|
1386
|
+
module_uri = module_uri[:-5]
|
|
1387
|
+
except Exception:
|
|
1388
|
+
module_uri = "default_module"
|
|
1389
|
+
|
|
1390
|
+
# Get module version dates from scopes metadata
|
|
1391
|
+
from_date = "2001-01-01"
|
|
1392
|
+
to_date = None
|
|
1393
|
+
scopes_metadata = scopes_api.get_scopes_with_metadata_from_expression(
|
|
1394
|
+
expression=expression,
|
|
1395
|
+
release_id=release_id
|
|
1396
|
+
)
|
|
1397
|
+
for scope_info in scopes_metadata:
|
|
1398
|
+
for module in scope_info.module_versions:
|
|
1399
|
+
if module.get("module_vid") == module_vid:
|
|
1400
|
+
from_date = module.get("from_reference_date", from_date)
|
|
1401
|
+
to_date = module.get("to_reference_date", to_date)
|
|
1402
|
+
break
|
|
1403
|
+
|
|
1404
|
+
scopes_api.close()
|
|
1405
|
+
|
|
1406
|
+
return {
|
|
1407
|
+
"module_uri": module_uri or "default_module",
|
|
1408
|
+
"module_code": resolved_module_code or "default",
|
|
1409
|
+
"module_version": primary_table.get("module_version", "1.0.0"),
|
|
1410
|
+
"framework_code": resolved_module_code or "default", # Framework code typically matches module code
|
|
1411
|
+
"from_date": str(from_date) if from_date else "2001-01-01",
|
|
1412
|
+
"to_date": str(to_date) if to_date else None,
|
|
1413
|
+
"module_vid": module_vid,
|
|
1414
|
+
"tables_with_modules": tables_with_modules, # Include table-to-module mapping
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
except Exception as e:
|
|
1418
|
+
import logging
|
|
1419
|
+
logging.warning(f"Failed to detect primary module info: {e}")
|
|
1420
|
+
return {**default_info, "tables_with_modules": []}
|
|
1421
|
+
|
|
1422
|
+
def _resolve_release_code(self, release_code: str) -> Optional[int]:
|
|
1423
|
+
"""
|
|
1424
|
+
Resolve a release code (e.g., "4.2") to its release ID.
|
|
1425
|
+
|
|
1426
|
+
Args:
|
|
1427
|
+
release_code: The release code string (e.g., "4.2")
|
|
1428
|
+
|
|
1429
|
+
Returns:
|
|
1430
|
+
The release ID if found, None otherwise.
|
|
1431
|
+
"""
|
|
1432
|
+
from py_dpm.dpm.utils import get_engine
|
|
1433
|
+
from py_dpm.dpm.models import Release
|
|
1434
|
+
from sqlalchemy.orm import sessionmaker
|
|
1435
|
+
|
|
1436
|
+
engine = get_engine(database_path=self.database_path, connection_url=self.connection_url)
|
|
1437
|
+
Session = sessionmaker(bind=engine)
|
|
1438
|
+
session = Session()
|
|
1439
|
+
|
|
1440
|
+
try:
|
|
1441
|
+
release = (
|
|
1442
|
+
session.query(Release)
|
|
1443
|
+
.filter(Release.code == release_code)
|
|
1444
|
+
.first()
|
|
1445
|
+
)
|
|
1446
|
+
if release:
|
|
1447
|
+
return release.releaseid
|
|
1448
|
+
return None
|
|
1449
|
+
except Exception:
|
|
1450
|
+
return None
|
|
1451
|
+
finally:
|
|
1452
|
+
session.close()
|
|
1453
|
+
|
|
1454
|
+
def _resolve_module_version(
|
|
1455
|
+
self, module_code: str, module_version_number: str
|
|
1456
|
+
) -> Tuple[Optional[int], Optional[str]]:
|
|
1457
|
+
"""
|
|
1458
|
+
Resolve a module version number to its release ID and release code.
|
|
1459
|
+
|
|
1460
|
+
Args:
|
|
1461
|
+
module_code: The module code (e.g., "COREP_LR")
|
|
1462
|
+
module_version_number: The module version number (e.g., "4.1.0")
|
|
1463
|
+
|
|
1464
|
+
Returns:
|
|
1465
|
+
Tuple of (release_id, release_code) if found, (None, None) otherwise.
|
|
1466
|
+
"""
|
|
1467
|
+
from py_dpm.dpm.utils import get_engine
|
|
1468
|
+
from py_dpm.dpm.models import ModuleVersion, Release
|
|
1469
|
+
from sqlalchemy.orm import sessionmaker
|
|
1470
|
+
|
|
1471
|
+
engine = get_engine(database_path=self.database_path, connection_url=self.connection_url)
|
|
1472
|
+
Session = sessionmaker(bind=engine)
|
|
1473
|
+
session = Session()
|
|
1474
|
+
|
|
1475
|
+
try:
|
|
1476
|
+
# Find the module version by code and version number
|
|
1477
|
+
module_version = (
|
|
1478
|
+
session.query(ModuleVersion)
|
|
1479
|
+
.filter(
|
|
1480
|
+
ModuleVersion.code == module_code,
|
|
1481
|
+
ModuleVersion.versionnumber == module_version_number
|
|
1482
|
+
)
|
|
1483
|
+
.first()
|
|
1484
|
+
)
|
|
1485
|
+
if not module_version:
|
|
1486
|
+
raise ValueError(
|
|
1487
|
+
f"Module version '{module_version_number}' not found for module '{module_code}'."
|
|
1488
|
+
)
|
|
1489
|
+
|
|
1490
|
+
# Get the release code from the start release
|
|
1491
|
+
release = (
|
|
1492
|
+
session.query(Release)
|
|
1493
|
+
.filter(Release.releaseid == module_version.startreleaseid)
|
|
1494
|
+
.first()
|
|
1495
|
+
)
|
|
1496
|
+
release_code = release.code if release else None
|
|
1497
|
+
|
|
1498
|
+
return module_version.startreleaseid, release_code
|
|
1499
|
+
finally:
|
|
1500
|
+
session.close()
|
|
1501
|
+
|
|
1502
|
+
def _get_release_info(self, release_code: Optional[str], engine) -> Dict[str, Any]:
|
|
854
1503
|
"""Get release information from database using SQLAlchemy."""
|
|
855
1504
|
from py_dpm.dpm.models import Release
|
|
856
1505
|
from sqlalchemy.orm import sessionmaker
|
|
857
1506
|
|
|
1507
|
+
def format_date(date_value) -> str:
|
|
1508
|
+
"""Format date whether it's a string or datetime object."""
|
|
1509
|
+
if date_value is None:
|
|
1510
|
+
return "2001-01-01"
|
|
1511
|
+
if isinstance(date_value, str):
|
|
1512
|
+
return date_value
|
|
1513
|
+
# Assume it's a datetime-like object
|
|
1514
|
+
return date_value.strftime("%Y-%m-%d")
|
|
1515
|
+
|
|
858
1516
|
Session = sessionmaker(bind=engine)
|
|
859
1517
|
session = Session()
|
|
860
1518
|
|
|
861
1519
|
try:
|
|
862
|
-
if
|
|
1520
|
+
if release_code:
|
|
863
1521
|
# Query for specific version
|
|
864
|
-
version_float = float(dpm_version)
|
|
865
1522
|
release = (
|
|
866
1523
|
session.query(Release)
|
|
867
|
-
.filter(Release.code ==
|
|
1524
|
+
.filter(Release.code == release_code)
|
|
868
1525
|
.first()
|
|
869
1526
|
)
|
|
870
1527
|
|
|
871
1528
|
if release:
|
|
872
1529
|
return {
|
|
873
|
-
"release": str(release.code) if release.code else
|
|
874
|
-
"publication_date": (
|
|
875
|
-
release.date.strftime("%Y-%m-%d")
|
|
876
|
-
if release.date
|
|
877
|
-
else "2001-01-01"
|
|
878
|
-
),
|
|
1530
|
+
"release": str(release.code) if release.code else release_code,
|
|
1531
|
+
"publication_date": format_date(release.date),
|
|
879
1532
|
}
|
|
880
1533
|
|
|
881
1534
|
# Fallback: get latest released version
|
|
@@ -889,9 +1542,7 @@ class ASTGeneratorAPI:
|
|
|
889
1542
|
if release:
|
|
890
1543
|
return {
|
|
891
1544
|
"release": str(release.code) if release.code else "4.1",
|
|
892
|
-
"publication_date": (
|
|
893
|
-
release.date.strftime("%Y-%m-%d") if release.date else "2001-01-01"
|
|
894
|
-
),
|
|
1545
|
+
"publication_date": format_date(release.date),
|
|
895
1546
|
}
|
|
896
1547
|
|
|
897
1548
|
# Final fallback
|
|
@@ -958,49 +1609,133 @@ class ASTGeneratorAPI:
|
|
|
958
1609
|
precondition: Optional[str],
|
|
959
1610
|
context: Optional[Dict[str, Any]],
|
|
960
1611
|
operation_code: str,
|
|
961
|
-
|
|
1612
|
+
release_id: Optional[int] = None,
|
|
962
1613
|
) -> tuple:
|
|
963
|
-
"""Build preconditions and precondition_variables sections.
|
|
1614
|
+
"""Build preconditions and precondition_variables sections.
|
|
1615
|
+
|
|
1616
|
+
Handles both simple preconditions like {v_C_47.00} and compound
|
|
1617
|
+
preconditions like {v_C_01.00} and {v_C_05.01} and {v_C_47.00}.
|
|
1618
|
+
|
|
1619
|
+
For compound preconditions, generates a full AST with BinOp nodes
|
|
1620
|
+
for 'and' operators connecting PreconditionItem nodes.
|
|
1621
|
+
|
|
1622
|
+
Uses ExplorerQueryAPI to fetch actual variable_id and variable_vid
|
|
1623
|
+
from the database based on variable codes.
|
|
1624
|
+
|
|
1625
|
+
Args:
|
|
1626
|
+
precondition: Precondition string like "{v_C_01.00}" or
|
|
1627
|
+
"{v_C_01.00} and {v_C_05.01} and {v_C_47.00}"
|
|
1628
|
+
context: Optional context dict
|
|
1629
|
+
operation_code: Operation code to associate with this precondition
|
|
1630
|
+
release_id: Optional release ID for filtering variable versions
|
|
1631
|
+
"""
|
|
964
1632
|
import re
|
|
1633
|
+
from py_dpm.api.dpm.explorer import ExplorerQueryAPI
|
|
965
1634
|
|
|
966
1635
|
preconditions = {}
|
|
967
1636
|
precondition_variables = {}
|
|
968
1637
|
|
|
969
|
-
|
|
970
|
-
|
|
1638
|
+
if not precondition:
|
|
1639
|
+
return preconditions, precondition_variables
|
|
971
1640
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1641
|
+
# Extract all variable codes from precondition (handles both simple and compound)
|
|
1642
|
+
# Pattern matches {v_VARIABLE_CODE} references
|
|
1643
|
+
var_matches = re.findall(r"\{v_([^}]+)\}", precondition)
|
|
1644
|
+
|
|
1645
|
+
if not var_matches:
|
|
1646
|
+
return preconditions, precondition_variables
|
|
1647
|
+
|
|
1648
|
+
# Normalize variable codes (F_44_04 -> F_44.04)
|
|
1649
|
+
variable_codes = [self._normalize_table_code(v) for v in var_matches]
|
|
1650
|
+
|
|
1651
|
+
# Batch lookup variable IDs from database (single query for efficiency)
|
|
1652
|
+
explorer_api = ExplorerQueryAPI()
|
|
1653
|
+
try:
|
|
1654
|
+
variables_info = explorer_api.get_variables_by_codes(
|
|
1655
|
+
variable_codes=variable_codes,
|
|
1656
|
+
release_id=release_id,
|
|
1657
|
+
)
|
|
1658
|
+
finally:
|
|
1659
|
+
explorer_api.close()
|
|
1660
|
+
|
|
1661
|
+
# Build variable infos list preserving order from precondition
|
|
1662
|
+
var_infos = []
|
|
1663
|
+
for var_code in variable_codes:
|
|
1664
|
+
if var_code in variables_info:
|
|
1665
|
+
info = variables_info[var_code]
|
|
1666
|
+
var_infos.append({
|
|
1667
|
+
"variable_code": var_code,
|
|
1668
|
+
"variable_id": info["variable_id"],
|
|
1669
|
+
"variable_vid": info["variable_vid"],
|
|
1670
|
+
})
|
|
1671
|
+
# Add to precondition_variables
|
|
1672
|
+
precondition_variables[str(info["variable_vid"])] = "b"
|
|
1673
|
+
|
|
1674
|
+
if not var_infos:
|
|
1675
|
+
return preconditions, precondition_variables
|
|
1676
|
+
|
|
1677
|
+
# Build the AST based on number of variables
|
|
1678
|
+
if len(var_infos) == 1:
|
|
1679
|
+
# Simple precondition - single PreconditionItem
|
|
1680
|
+
info = var_infos[0]
|
|
1681
|
+
precondition_code = f"p_{info['variable_vid']}"
|
|
1682
|
+
|
|
1683
|
+
preconditions[precondition_code] = {
|
|
1684
|
+
"ast": {
|
|
1685
|
+
"class_name": "PreconditionItem",
|
|
1686
|
+
"variable_id": info["variable_id"],
|
|
1687
|
+
"variable_code": info["variable_code"],
|
|
1688
|
+
},
|
|
1689
|
+
"affected_operations": [operation_code],
|
|
1690
|
+
"version_id": info["variable_vid"],
|
|
1691
|
+
"code": precondition_code,
|
|
1692
|
+
}
|
|
1693
|
+
else:
|
|
1694
|
+
# Compound precondition - build BinOp tree with 'and' operators
|
|
1695
|
+
# Create a unique key based on sorted variable VIDs
|
|
1696
|
+
sorted_var_vids = sorted([info["variable_vid"] for info in var_infos])
|
|
1697
|
+
precondition_code = "p_" + "_".join(str(vid) for vid in sorted_var_vids)
|
|
1698
|
+
|
|
1699
|
+
# Build AST: left-associative chain of BinOp 'and' nodes
|
|
1700
|
+
# E.g., for [A, B, C]: ((A and B) and C)
|
|
1701
|
+
ast = self._build_precondition_item_ast(var_infos[0])
|
|
1702
|
+
for info in var_infos[1:]:
|
|
1703
|
+
right_ast = self._build_precondition_item_ast(info)
|
|
1704
|
+
ast = {
|
|
1705
|
+
"class_name": "BinOp",
|
|
1706
|
+
"op": "and",
|
|
1707
|
+
"left": ast,
|
|
1708
|
+
"right": right_ast,
|
|
998
1709
|
}
|
|
999
1710
|
|
|
1000
|
-
|
|
1711
|
+
# Use the first variable's VID as version_id
|
|
1712
|
+
preconditions[precondition_code] = {
|
|
1713
|
+
"ast": ast,
|
|
1714
|
+
"affected_operations": [operation_code],
|
|
1715
|
+
"version_id": sorted_var_vids[0],
|
|
1716
|
+
"code": precondition_code,
|
|
1717
|
+
}
|
|
1001
1718
|
|
|
1002
1719
|
return preconditions, precondition_variables
|
|
1003
1720
|
|
|
1721
|
+
def _build_precondition_item_ast(self, var_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
1722
|
+
"""Build a PreconditionItem AST node for a single variable."""
|
|
1723
|
+
return {
|
|
1724
|
+
"class_name": "PreconditionItem",
|
|
1725
|
+
"variable_id": var_info["variable_id"],
|
|
1726
|
+
"variable_code": var_info["variable_code"],
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
def _normalize_table_code(self, table_code: str) -> str:
|
|
1730
|
+
"""Normalize table/variable code format (e.g., F_44_04 -> F_44.04)."""
|
|
1731
|
+
import re
|
|
1732
|
+
# Handle format like C_01_00 -> C_01.00 or F_44_04 -> F_44.04
|
|
1733
|
+
match = re.match(r"([A-Z]+)_(\d+)_(\d+)", table_code)
|
|
1734
|
+
if match:
|
|
1735
|
+
return f"{match.group(1)}_{match.group(2)}.{match.group(3)}"
|
|
1736
|
+
# Already in correct format or different format
|
|
1737
|
+
return table_code
|
|
1738
|
+
|
|
1004
1739
|
def _extract_variables_from_ast(self, ast_dict: Dict[str, Any]) -> tuple:
|
|
1005
1740
|
"""
|
|
1006
1741
|
Extract variables from complete AST by table.
|
|
@@ -1121,6 +1856,7 @@ class ASTGeneratorAPI:
|
|
|
1121
1856
|
primary_module_vid: Optional[int],
|
|
1122
1857
|
operation_code: str,
|
|
1123
1858
|
release_id: Optional[int] = None,
|
|
1859
|
+
preferred_module_dependencies: Optional[List[str]] = None,
|
|
1124
1860
|
) -> tuple:
|
|
1125
1861
|
"""
|
|
1126
1862
|
Detect cross-module dependencies for a single expression.
|
|
@@ -1134,6 +1870,8 @@ class ASTGeneratorAPI:
|
|
|
1134
1870
|
primary_module_vid: The module being exported (if known)
|
|
1135
1871
|
operation_code: Current operation code
|
|
1136
1872
|
release_id: Optional release ID for filtering
|
|
1873
|
+
preferred_module_dependencies: Optional list of module codes to prefer when
|
|
1874
|
+
a table belongs to multiple modules
|
|
1137
1875
|
|
|
1138
1876
|
Returns:
|
|
1139
1877
|
Tuple of (dependency_modules, cross_instance_dependencies)
|
|
@@ -1187,30 +1925,54 @@ class ASTGeneratorAPI:
|
|
|
1187
1925
|
return ref or "T"
|
|
1188
1926
|
|
|
1189
1927
|
# Helper to lookup variables for a table
|
|
1190
|
-
|
|
1928
|
+
# For external module tables, fetch from database if not in variables_by_table
|
|
1929
|
+
from py_dpm.api.dpm import DataDictionaryAPI
|
|
1930
|
+
data_dict_api = DataDictionaryAPI(
|
|
1931
|
+
database_path=self.database_path,
|
|
1932
|
+
connection_url=self.connection_url
|
|
1933
|
+
)
|
|
1934
|
+
|
|
1935
|
+
def get_table_variables(table_code: str, table_vid: int = None) -> dict:
|
|
1191
1936
|
if not table_code:
|
|
1192
1937
|
return {}
|
|
1938
|
+
# First try from passed variables_by_table
|
|
1193
1939
|
variables = variables_by_table.get(table_code)
|
|
1194
1940
|
if not variables:
|
|
1195
1941
|
variables = variables_by_table.get(f"t{table_code}", {})
|
|
1942
|
+
# If still empty and table_vid is provided, fetch from database
|
|
1943
|
+
if not variables and table_vid:
|
|
1944
|
+
variables = data_dict_api.get_all_variables_for_table(table_vid)
|
|
1196
1945
|
return variables or {}
|
|
1197
1946
|
|
|
1198
1947
|
# Group external tables by module
|
|
1948
|
+
# If preferred_module_dependencies is set, only include those modules
|
|
1199
1949
|
external_modules = {}
|
|
1950
|
+
|
|
1951
|
+
# TEMPORARY WORKAROUND: Also collect primary module tables to add to dependency_modules
|
|
1952
|
+
# This is conceptually wrong but required for current implementation.
|
|
1953
|
+
# See /docs/dependency_modules_main_tables_workaround.md for how to revert this.
|
|
1954
|
+
primary_module_tables = []
|
|
1955
|
+
|
|
1200
1956
|
for table_info in tables_with_modules:
|
|
1201
1957
|
module_vid = table_info.get("module_vid")
|
|
1202
1958
|
if module_vid == primary_module_vid:
|
|
1203
|
-
|
|
1959
|
+
# Collect primary module tables for later inclusion in dependency_modules
|
|
1960
|
+
primary_module_tables.append(table_info)
|
|
1961
|
+
continue # Skip for now, will add later
|
|
1204
1962
|
|
|
1205
|
-
|
|
1206
|
-
if not
|
|
1963
|
+
ext_module_code = table_info.get("module_code")
|
|
1964
|
+
if not ext_module_code:
|
|
1965
|
+
continue
|
|
1966
|
+
|
|
1967
|
+
# If preferred_module_dependencies is set, only include preferred modules
|
|
1968
|
+
if preferred_module_dependencies and ext_module_code not in preferred_module_dependencies:
|
|
1207
1969
|
continue
|
|
1208
1970
|
|
|
1209
1971
|
# Get module URI
|
|
1210
1972
|
try:
|
|
1211
1973
|
module_uri = ExplorerQuery.get_module_url(
|
|
1212
1974
|
scopes_api.session,
|
|
1213
|
-
module_code=
|
|
1975
|
+
module_code=ext_module_code,
|
|
1214
1976
|
release_id=release_id,
|
|
1215
1977
|
)
|
|
1216
1978
|
if module_uri.endswith(".json"):
|
|
@@ -1237,13 +1999,29 @@ class ASTGeneratorAPI:
|
|
|
1237
1999
|
|
|
1238
2000
|
# Add table and variables
|
|
1239
2001
|
if table_code:
|
|
1240
|
-
|
|
2002
|
+
table_vid = table_info.get("table_vid")
|
|
2003
|
+
table_variables = get_table_variables(table_code, table_vid)
|
|
1241
2004
|
external_modules[module_uri]["tables"][table_code] = {
|
|
1242
2005
|
"variables": table_variables,
|
|
1243
2006
|
"open_keys": {}
|
|
1244
2007
|
}
|
|
1245
2008
|
external_modules[module_uri]["variables"].update(table_variables)
|
|
1246
2009
|
|
|
2010
|
+
# TEMPORARY WORKAROUND: Add primary module tables to each dependency module entry
|
|
2011
|
+
# This includes main module tables/variables in dependency_modules for cross-module validations
|
|
2012
|
+
# See /docs/dependency_modules_main_tables_workaround.md for how to revert this.
|
|
2013
|
+
for uri in external_modules:
|
|
2014
|
+
for table_info in primary_module_tables:
|
|
2015
|
+
table_code = table_info.get("code")
|
|
2016
|
+
if table_code:
|
|
2017
|
+
table_vid = table_info.get("table_vid")
|
|
2018
|
+
table_variables = get_table_variables(table_code, table_vid)
|
|
2019
|
+
external_modules[uri]["tables"][table_code] = {
|
|
2020
|
+
"variables": table_variables,
|
|
2021
|
+
"open_keys": {}
|
|
2022
|
+
}
|
|
2023
|
+
external_modules[uri]["variables"].update(table_variables)
|
|
2024
|
+
|
|
1247
2025
|
# Get date info from scopes metadata
|
|
1248
2026
|
scopes_metadata = scopes_api.get_scopes_with_metadata_from_expression(
|
|
1249
2027
|
expression=expression,
|
|
@@ -1286,6 +2064,8 @@ class ASTGeneratorAPI:
|
|
|
1286
2064
|
"to_reference_date": str(to_date) if to_date else ""
|
|
1287
2065
|
})
|
|
1288
2066
|
|
|
2067
|
+
# Close data_dict_api before returning
|
|
2068
|
+
data_dict_api.close()
|
|
1289
2069
|
return dependency_modules, cross_instance_dependencies
|
|
1290
2070
|
|
|
1291
2071
|
except Exception as e:
|
|
@@ -1297,53 +2077,96 @@ class ASTGeneratorAPI:
|
|
|
1297
2077
|
def _add_coordinates_to_ast(
|
|
1298
2078
|
self, ast_dict: Dict[str, Any], context: Optional[Dict[str, Any]]
|
|
1299
2079
|
) -> Dict[str, Any]:
|
|
1300
|
-
"""
|
|
2080
|
+
"""
|
|
2081
|
+
Add x/y/z coordinates to data entries in AST.
|
|
2082
|
+
|
|
2083
|
+
Coordinates are assigned based on:
|
|
2084
|
+
- x: row position (1-indexed)
|
|
2085
|
+
- y: column position (1-indexed)
|
|
2086
|
+
- z: sheet position (1-indexed)
|
|
2087
|
+
|
|
2088
|
+
If context provides column/row/sheet lists, those are used for ordering.
|
|
2089
|
+
Otherwise, the order is extracted from the data entries themselves.
|
|
2090
|
+
"""
|
|
1301
2091
|
import copy
|
|
1302
2092
|
|
|
1303
2093
|
def add_coords_to_node(node):
|
|
1304
2094
|
if isinstance(node, dict):
|
|
1305
2095
|
# Handle VarID nodes with data arrays
|
|
1306
2096
|
if node.get("class_name") == "VarID" and "data" in node:
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
#
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
2097
|
+
data_entries = node["data"]
|
|
2098
|
+
if not data_entries:
|
|
2099
|
+
return
|
|
2100
|
+
|
|
2101
|
+
# Get context lists (may be empty)
|
|
2102
|
+
context_cols = []
|
|
2103
|
+
context_rows = []
|
|
2104
|
+
context_sheets = []
|
|
2105
|
+
if context:
|
|
2106
|
+
context_cols = context.get("columns") or []
|
|
2107
|
+
context_rows = context.get("rows") or []
|
|
2108
|
+
context_sheets = context.get("sheets") or []
|
|
2109
|
+
|
|
2110
|
+
# Extract unique rows, columns, sheets from data entries
|
|
2111
|
+
# Use these if context doesn't provide them
|
|
2112
|
+
data_rows = []
|
|
2113
|
+
data_cols = []
|
|
2114
|
+
data_sheets = []
|
|
2115
|
+
seen_rows = set()
|
|
2116
|
+
seen_cols = set()
|
|
2117
|
+
seen_sheets = set()
|
|
2118
|
+
|
|
2119
|
+
for entry in data_entries:
|
|
2120
|
+
row = entry.get("row", "")
|
|
2121
|
+
col = entry.get("column", "")
|
|
2122
|
+
sheet = entry.get("sheet", "")
|
|
2123
|
+
if row and row not in seen_rows:
|
|
2124
|
+
data_rows.append(row)
|
|
2125
|
+
seen_rows.add(row)
|
|
2126
|
+
if col and col not in seen_cols:
|
|
2127
|
+
data_cols.append(col)
|
|
2128
|
+
seen_cols.add(col)
|
|
2129
|
+
if sheet and sheet not in seen_sheets:
|
|
2130
|
+
data_sheets.append(sheet)
|
|
2131
|
+
seen_sheets.add(sheet)
|
|
2132
|
+
|
|
2133
|
+
# Sort for consistent ordering
|
|
2134
|
+
data_rows.sort()
|
|
2135
|
+
data_cols.sort()
|
|
2136
|
+
data_sheets.sort()
|
|
2137
|
+
|
|
2138
|
+
# Use context lists if provided, otherwise use extracted lists
|
|
2139
|
+
rows = context_rows if context_rows else data_rows
|
|
2140
|
+
cols = context_cols if context_cols else data_cols
|
|
2141
|
+
sheets = context_sheets if context_sheets else data_sheets
|
|
2142
|
+
|
|
2143
|
+
# Assign coordinates to each data entry
|
|
2144
|
+
for entry in data_entries:
|
|
2145
|
+
row_code = entry.get("row", "")
|
|
2146
|
+
col_code = entry.get("column", "")
|
|
2147
|
+
sheet_code = entry.get("sheet", "")
|
|
2148
|
+
|
|
2149
|
+
# Calculate x coordinate (row position)
|
|
2150
|
+
if rows and row_code in rows:
|
|
2151
|
+
x_index = rows.index(row_code) + 1
|
|
2152
|
+
# Only add x if there are multiple rows
|
|
1343
2153
|
if len(rows) > 1:
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
2154
|
+
entry["x"] = x_index
|
|
2155
|
+
|
|
2156
|
+
# Calculate y coordinate (column position)
|
|
2157
|
+
if cols and col_code in cols:
|
|
2158
|
+
y_index = cols.index(col_code) + 1
|
|
2159
|
+
entry["y"] = y_index
|
|
2160
|
+
elif not cols:
|
|
2161
|
+
# Default to 1 if no column info
|
|
2162
|
+
entry["y"] = 1
|
|
2163
|
+
|
|
2164
|
+
# Calculate z coordinate (sheet position)
|
|
2165
|
+
if sheets and sheet_code in sheets:
|
|
2166
|
+
z_index = sheets.index(sheet_code) + 1
|
|
2167
|
+
# Only add z if there are multiple sheets
|
|
2168
|
+
if len(sheets) > 1:
|
|
2169
|
+
entry["z"] = z_index
|
|
1347
2170
|
|
|
1348
2171
|
# Recursively process child nodes
|
|
1349
2172
|
for key, value in node.items():
|
|
@@ -1358,6 +2181,44 @@ class ASTGeneratorAPI:
|
|
|
1358
2181
|
add_coords_to_node(result)
|
|
1359
2182
|
return result
|
|
1360
2183
|
|
|
2184
|
+
def _clean_ast_data_entries(self, ast_dict: Dict[str, Any]) -> Dict[str, Any]:
|
|
2185
|
+
"""
|
|
2186
|
+
Remove extra fields from data entries in the AST.
|
|
2187
|
+
|
|
2188
|
+
Keeps only the fields required by the engine:
|
|
2189
|
+
- datapoint, operand_reference_id, y, column, x (if multiple rows), z (if multiple sheets)
|
|
2190
|
+
|
|
2191
|
+
Removes internal/debug fields:
|
|
2192
|
+
- data_type, cell_code, table_code, table_vid, row
|
|
2193
|
+
"""
|
|
2194
|
+
# Fields to keep in data entries
|
|
2195
|
+
ALLOWED_FIELDS = {"datapoint", "operand_reference_id", "x", "y", "z", "column", "sheet"}
|
|
2196
|
+
|
|
2197
|
+
def clean_node(node):
|
|
2198
|
+
if isinstance(node, dict):
|
|
2199
|
+
# Handle VarID nodes with data arrays
|
|
2200
|
+
if node.get("class_name") == "VarID" and "data" in node:
|
|
2201
|
+
cleaned_data = []
|
|
2202
|
+
for data_entry in node["data"]:
|
|
2203
|
+
# Keep only allowed fields
|
|
2204
|
+
cleaned_entry = {
|
|
2205
|
+
k: v for k, v in data_entry.items() if k in ALLOWED_FIELDS
|
|
2206
|
+
}
|
|
2207
|
+
cleaned_data.append(cleaned_entry)
|
|
2208
|
+
node["data"] = cleaned_data
|
|
2209
|
+
|
|
2210
|
+
# Recursively process child nodes
|
|
2211
|
+
for key, value in node.items():
|
|
2212
|
+
if isinstance(value, (dict, list)):
|
|
2213
|
+
clean_node(value)
|
|
2214
|
+
elif isinstance(node, list):
|
|
2215
|
+
for item in node:
|
|
2216
|
+
clean_node(item)
|
|
2217
|
+
|
|
2218
|
+
# Modify in place (ast_dict is already a copy from _add_coordinates_to_ast)
|
|
2219
|
+
clean_node(ast_dict)
|
|
2220
|
+
return ast_dict
|
|
2221
|
+
|
|
1361
2222
|
|
|
1362
2223
|
# Convenience functions for simple usage
|
|
1363
2224
|
|
|
@@ -1389,18 +2250,3 @@ def validate_expression(expression: str) -> bool:
|
|
|
1389
2250
|
generator = ASTGeneratorAPI()
|
|
1390
2251
|
result = generator.validate_expression(expression)
|
|
1391
2252
|
return result['valid']
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
def parse_batch(expressions: List[str], compatibility_mode: str = "auto") -> List[Dict[str, Any]]:
|
|
1395
|
-
"""
|
|
1396
|
-
Simple function to parse multiple expressions.
|
|
1397
|
-
|
|
1398
|
-
Args:
|
|
1399
|
-
expressions: List of DPM-XL expression strings
|
|
1400
|
-
compatibility_mode: Version compatibility mode
|
|
1401
|
-
|
|
1402
|
-
Returns:
|
|
1403
|
-
List of parse results
|
|
1404
|
-
"""
|
|
1405
|
-
generator = ASTGeneratorAPI(compatibility_mode=compatibility_mode)
|
|
1406
|
-
return generator.parse_batch(expressions)
|