pydpm_xl 0.2.4__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/PKG-INFO +54 -1
  2. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/README.md +53 -0
  3. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/__init__.py +1 -1
  4. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm_xl/ast_generator.py +296 -13
  5. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm_xl/complete_ast.py +10 -0
  6. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm/models.py +257 -7
  7. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/utils/scopes_calculator.py +86 -30
  8. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/pydpm_xl.egg-info/PKG-INFO +54 -1
  9. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/pyproject.toml +2 -2
  10. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/LICENSE +0 -0
  11. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/__init__.py +0 -0
  12. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm/__init__.py +0 -0
  13. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm/data_dictionary.py +0 -0
  14. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm/explorer.py +0 -0
  15. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm/hierarchical_queries.py +0 -0
  16. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm/instance.py +0 -0
  17. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm/migration.py +0 -0
  18. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm_xl/__init__.py +0 -0
  19. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm_xl/operation_scopes.py +0 -0
  20. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm_xl/semantic.py +0 -0
  21. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/api/dpm_xl/syntax.py +0 -0
  22. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/cli/__init__.py +0 -0
  23. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/cli/commands/__init__.py +0 -0
  24. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/cli/main.py +0 -0
  25. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm/__init__.py +0 -0
  26. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm/migration.py +0 -0
  27. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm/queries/base.py +0 -0
  28. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm/queries/basic_objects.py +0 -0
  29. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm/queries/explorer_queries.py +0 -0
  30. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm/queries/filters.py +0 -0
  31. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm/queries/glossary.py +0 -0
  32. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm/queries/hierarchical_queries.py +0 -0
  33. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm/queries/tables.py +0 -0
  34. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm/utils.py +0 -0
  35. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/__init__.py +0 -0
  36. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/ast/__init__.py +0 -0
  37. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/ast/constructor.py +0 -0
  38. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/ast/ml_generation.py +0 -0
  39. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/ast/module_analyzer.py +0 -0
  40. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/ast/module_dependencies.py +0 -0
  41. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/ast/nodes.py +0 -0
  42. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/ast/operands.py +0 -0
  43. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/ast/template.py +0 -0
  44. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/ast/visitor.py +0 -0
  45. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/ast/where_clause.py +0 -0
  46. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/grammar/__init__.py +0 -0
  47. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/grammar/generated/__init__.py +0 -0
  48. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.interp +0 -0
  49. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.py +0 -0
  50. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.tokens +0 -0
  51. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.interp +0 -0
  52. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.py +0 -0
  53. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.tokens +0 -0
  54. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/grammar/generated/dpm_xlParserListener.py +0 -0
  55. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/grammar/generated/dpm_xlParserVisitor.py +0 -0
  56. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/grammar/generated/listeners.py +0 -0
  57. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/operators/__init__.py +0 -0
  58. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/operators/aggregate.py +0 -0
  59. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/operators/arithmetic.py +0 -0
  60. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/operators/base.py +0 -0
  61. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/operators/boolean.py +0 -0
  62. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/operators/clause.py +0 -0
  63. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/operators/comparison.py +0 -0
  64. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/operators/conditional.py +0 -0
  65. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/operators/string.py +0 -0
  66. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/operators/time.py +0 -0
  67. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/semantic_analyzer.py +0 -0
  68. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/symbols.py +0 -0
  69. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/types/__init__.py +0 -0
  70. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/types/promotion.py +0 -0
  71. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/types/scalar.py +0 -0
  72. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/types/time.py +0 -0
  73. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/utils/__init__.py +0 -0
  74. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/utils/data_handlers.py +0 -0
  75. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/utils/operands_mapping.py +0 -0
  76. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/utils/operator_mapping.py +0 -0
  77. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/utils/serialization.py +0 -0
  78. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/dpm_xl/utils/tokens.py +0 -0
  79. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/exceptions/__init__.py +0 -0
  80. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/exceptions/exceptions.py +0 -0
  81. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/exceptions/messages.py +0 -0
  82. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/instance/__init__.py +0 -0
  83. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/py_dpm/instance/instance.py +0 -0
  84. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/pydpm_xl.egg-info/SOURCES.txt +0 -0
  85. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/pydpm_xl.egg-info/dependency_links.txt +0 -0
  86. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/pydpm_xl.egg-info/entry_points.txt +0 -0
  87. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/pydpm_xl.egg-info/requires.txt +0 -0
  88. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/pydpm_xl.egg-info/top_level.txt +0 -0
  89. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/setup.cfg +0 -0
  90. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/tests/test_cli_semantic.py +0 -0
  91. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/tests/test_data_dictionary_releases.py +0 -0
  92. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/tests/test_db_connection_handling.py +0 -0
  93. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/tests/test_get_table_details.py +0 -0
  94. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/tests/test_get_tables_date_filter.py +0 -0
  95. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/tests/test_get_tables_release_code.py +0 -0
  96. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/tests/test_hierarchical_query.py +0 -0
  97. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/tests/test_query_refactor.py +0 -0
  98. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/tests/test_release_filters_semantic.py +0 -0
  99. {pydpm_xl-0.2.4 → pydpm_xl-0.2.5}/tests/test_semantic_release.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydpm_xl
3
- Version: 0.2.4
3
+ Version: 0.2.5
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
@@ -241,6 +241,59 @@ migration_api.migrate_from_access(
241
241
  )
242
242
  ```
243
243
 
244
+ #### XBRL-CSV Instance Generation
245
+
246
+ ```python
247
+ from py_dpm.api import InstanceAPI
248
+
249
+ api = InstanceAPI()
250
+
251
+ # Build package from dictionary
252
+ data = {
253
+ "module_code": "F_01.01",
254
+ "parameters": {"refPeriod": "2024-12-31"},
255
+ "facts": [
256
+ {"table_code": "t001", "row_code": "r010", "column_code": "c010", "value": 1000000}
257
+ ]
258
+ }
259
+ output_path = api.build_package_from_dict(data, "/tmp/output")
260
+
261
+ # Build package from JSON file
262
+ output_path = api.build_package_from_json("instance_data.json", "/tmp/output")
263
+ ```
264
+
265
+ #### DPM Explorer - Introspection Queries
266
+
267
+ ```python
268
+ from py_dpm.api import ExplorerQueryAPI
269
+
270
+ with ExplorerQueryAPI() as api:
271
+ # Find all properties using a specific item
272
+ properties = api.get_properties_using_item("EUR")
273
+
274
+ # Get module URL for documentation
275
+ url = api.get_module_url(module_code="F_01.01")
276
+
277
+ # Explore variable usage
278
+ tables = api.get_tables_using_variable(variable_code="mi123")
279
+ ```
280
+
281
+ #### Hierarchical Queries
282
+
283
+ ```python
284
+ from py_dpm.api import HierarchicalQueryAPI
285
+
286
+ with HierarchicalQueryAPI() as api:
287
+ # Get hierarchy for a domain
288
+ hierarchy = api.get_hierarchy(domain_code="DOM_001")
289
+
290
+ # Navigate parent-child relationships
291
+ children = api.get_children(item_code="PARENT_001")
292
+
293
+ # Get all ancestors
294
+ ancestors = api.get_ancestors(item_code="LEAF_001")
295
+ ```
296
+
244
297
  ## Development
245
298
 
246
299
  ### Running Tests
@@ -208,6 +208,59 @@ migration_api.migrate_from_access(
208
208
  )
209
209
  ```
210
210
 
211
+ #### XBRL-CSV Instance Generation
212
+
213
+ ```python
214
+ from py_dpm.api import InstanceAPI
215
+
216
+ api = InstanceAPI()
217
+
218
+ # Build package from dictionary
219
+ data = {
220
+ "module_code": "F_01.01",
221
+ "parameters": {"refPeriod": "2024-12-31"},
222
+ "facts": [
223
+ {"table_code": "t001", "row_code": "r010", "column_code": "c010", "value": 1000000}
224
+ ]
225
+ }
226
+ output_path = api.build_package_from_dict(data, "/tmp/output")
227
+
228
+ # Build package from JSON file
229
+ output_path = api.build_package_from_json("instance_data.json", "/tmp/output")
230
+ ```
231
+
232
+ #### DPM Explorer - Introspection Queries
233
+
234
+ ```python
235
+ from py_dpm.api import ExplorerQueryAPI
236
+
237
+ with ExplorerQueryAPI() as api:
238
+ # Find all properties using a specific item
239
+ properties = api.get_properties_using_item("EUR")
240
+
241
+ # Get module URL for documentation
242
+ url = api.get_module_url(module_code="F_01.01")
243
+
244
+ # Explore variable usage
245
+ tables = api.get_tables_using_variable(variable_code="mi123")
246
+ ```
247
+
248
+ #### Hierarchical Queries
249
+
250
+ ```python
251
+ from py_dpm.api import HierarchicalQueryAPI
252
+
253
+ with HierarchicalQueryAPI() as api:
254
+ # Get hierarchy for a domain
255
+ hierarchy = api.get_hierarchy(domain_code="DOM_001")
256
+
257
+ # Navigate parent-child relationships
258
+ children = api.get_children(item_code="PARENT_001")
259
+
260
+ # Get all ancestors
261
+ ancestors = api.get_ancestors(item_code="LEAF_001")
262
+ ```
263
+
211
264
  ## Development
212
265
 
213
266
  ### Running Tests
@@ -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.4"
44
+ __version__ = "0.2.5"
45
45
  __author__ = "MeaningfulData S.L."
46
46
  __email__ = "info@meaningfuldata.eu"
47
47
  __license__ = "GPL-3.0-or-later"
@@ -369,6 +369,7 @@ class ASTGeneratorAPI:
369
369
  precondition: Optional[str] = None,
370
370
  release_id: Optional[int] = None,
371
371
  output_path: Optional[Union[str, Path]] = None,
372
+ primary_module_vid: Optional[int] = None,
372
373
  ) -> Dict[str, Any]:
373
374
  """
374
375
  Generate enriched, engine-ready AST with framework structure (Level 3).
@@ -381,13 +382,14 @@ class ASTGeneratorAPI:
381
382
  - Everything from generate_complete_ast() PLUS:
382
383
  - Framework structure: operations, variables, tables, preconditions
383
384
  - Module metadata: version, release info, dates
384
- - Dependency information
385
+ - Dependency information (including cross-module dependencies)
385
386
  - Coordinates (x/y/z) added to data entries
386
387
 
387
388
  **Typical use case:**
388
389
  - Feeding AST to business rule execution engines
389
390
  - Validation framework integration
390
391
  - Production rule processing
392
+ - Module exports with cross-module dependency tracking
391
393
 
392
394
  Args:
393
395
  expression: DPM-XL expression string
@@ -399,6 +401,11 @@ class ASTGeneratorAPI:
399
401
  If None, uses all available data (release-agnostic).
400
402
  output_path: Optional path (string or Path) to save the enriched_ast as JSON file.
401
403
  If provided, the enriched_ast will be automatically saved to this location.
404
+ primary_module_vid: Optional module version ID of the module being exported.
405
+ When provided, enables detection of cross-module dependencies - tables from
406
+ other modules will be identified and added to dependency_modules and
407
+ cross_instance_dependencies fields. If None, cross-module detection uses
408
+ the first table's module as the primary module.
402
409
 
403
410
  Returns:
404
411
  dict: {
@@ -416,14 +423,17 @@ class ASTGeneratorAPI:
416
423
  ... )
417
424
  >>> # result['enriched_ast'] contains framework structure ready for engines
418
425
  >>>
419
- >>> # Or save directly to a file:
426
+ >>> # For module exports with cross-module dependency tracking:
420
427
  >>> result = generator.generate_enriched_ast(
421
- ... "{tF_01.00, r0010, c0010}",
428
+ ... "{tC_26.00, r030, c010} * {tC_01.00, r0015, c0010}",
422
429
  ... dpm_version="4.2",
423
- ... operation_code="my_validation",
424
- ... output_path="./output/enriched_ast.json"
430
+ ... operation_code="v2814_m",
431
+ ... primary_module_vid=123, # Module being exported
432
+ ... release_id=42
425
433
  ... )
426
- >>> # The enriched_ast is automatically saved to the specified path
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
427
437
  """
428
438
  try:
429
439
  # Generate complete AST first
@@ -447,6 +457,8 @@ class ASTGeneratorAPI:
447
457
  dpm_version=dpm_version,
448
458
  operation_code=operation_code,
449
459
  precondition=precondition,
460
+ release_id=release_id,
461
+ primary_module_vid=primary_module_vid,
450
462
  )
451
463
 
452
464
  # Save to file if output_path is provided
@@ -723,11 +735,23 @@ class ASTGeneratorAPI:
723
735
  dpm_version: Optional[str] = None,
724
736
  operation_code: Optional[str] = None,
725
737
  precondition: Optional[str] = None,
738
+ release_id: Optional[int] = None,
739
+ primary_module_vid: Optional[int] = None,
726
740
  ) -> Dict[str, Any]:
727
741
  """
728
742
  Add framework structure (operations, variables, tables, preconditions) to complete AST.
729
743
 
730
744
  This creates the engine-ready format with all metadata sections.
745
+
746
+ Args:
747
+ ast_dict: Complete AST dictionary
748
+ expression: Original DPM-XL expression
749
+ context: Context dict with table, rows, columns, sheets, default, interval
750
+ dpm_version: DPM version code (e.g., "4.2")
751
+ operation_code: Operation code (defaults to "default_code")
752
+ precondition: Precondition variable reference (e.g., {v_F_44_04})
753
+ release_id: Optional release ID to filter database lookups
754
+ primary_module_vid: Module VID being exported (to identify external dependencies)
731
755
  """
732
756
  from py_dpm.dpm.utils import get_engine
733
757
  import copy
@@ -787,7 +811,7 @@ class ASTGeneratorAPI:
787
811
  preconditions = {}
788
812
  precondition_variables = {}
789
813
 
790
- if precondition or (context and "table" in context):
814
+ if precondition:
791
815
  preconditions, precondition_variables = self._build_preconditions(
792
816
  precondition=precondition,
793
817
  context=context,
@@ -795,15 +819,21 @@ class ASTGeneratorAPI:
795
819
  engine=engine,
796
820
  )
797
821
 
822
+ # Detect cross-module dependencies
823
+ dependency_modules, cross_instance_dependencies = self._detect_cross_module_dependencies(
824
+ expression=expression,
825
+ variables_by_table=variables_by_table,
826
+ primary_module_vid=primary_module_vid,
827
+ operation_code=operation_code,
828
+ release_id=release_id,
829
+ )
830
+
798
831
  # Build dependency information
799
832
  dependency_info = {
800
833
  "intra_instance_validations": [operation_code],
801
- "cross_instance_dependencies": [],
834
+ "cross_instance_dependencies": cross_instance_dependencies,
802
835
  }
803
836
 
804
- # Build dependency modules
805
- dependency_modules = {}
806
-
807
837
  # Build complete structure
808
838
  namespace = "default_module"
809
839
 
@@ -944,8 +974,6 @@ class ASTGeneratorAPI:
944
974
  match = re.match(r"\{v_([^}]+)\}", precondition)
945
975
  if match:
946
976
  table_code = match.group(1)
947
- elif context and "table" in context:
948
- table_code = context["table"]
949
977
 
950
978
  if table_code:
951
979
  # Query database for actual variable ID and version
@@ -1009,6 +1037,261 @@ class ASTGeneratorAPI:
1009
1037
  extract_from_node(ast_dict)
1010
1038
  return all_variables, variables_by_table
1011
1039
 
1040
+ def _extract_time_shifts_by_table(self, expression: str) -> Dict[str, str]:
1041
+ """
1042
+ Extract time shift information for each table in the expression.
1043
+
1044
+ Uses the AST to properly parse the expression and find TimeShiftOp nodes
1045
+ to determine the ref_period for each table reference.
1046
+
1047
+ Args:
1048
+ expression: DPM-XL expression
1049
+
1050
+ Returns:
1051
+ Dict mapping table codes to ref_period values (e.g., {"C_01.00": "T-1Q"})
1052
+ Tables without time shifts default to "T".
1053
+ """
1054
+ from py_dpm.dpm_xl.ast.template import ASTTemplate
1055
+
1056
+ time_shifts = {}
1057
+ current_period = ["t"] # Use list to allow mutation in nested function
1058
+
1059
+ class TimeShiftExtractor(ASTTemplate):
1060
+ """Lightweight AST visitor that extracts time shifts for each table."""
1061
+
1062
+ def visit_TimeShiftOp(self, node):
1063
+ # Save current time period and compute new one
1064
+ previous_period = current_period[0]
1065
+
1066
+ period_indicator = node.period_indicator
1067
+ shift_number = node.shift_number
1068
+
1069
+ # Compute time period (same logic as ModuleDependencies)
1070
+ if "-" in str(shift_number):
1071
+ current_period[0] = f"t+{period_indicator}{shift_number}"
1072
+ else:
1073
+ current_period[0] = f"t-{period_indicator}{shift_number}"
1074
+
1075
+ # Visit operand (which contains the VarID)
1076
+ self.visit(node.operand)
1077
+
1078
+ # Restore previous time period
1079
+ current_period[0] = previous_period
1080
+
1081
+ def visit_VarID(self, node):
1082
+ if node.table and current_period[0] != "t":
1083
+ time_shifts[node.table] = current_period[0]
1084
+
1085
+ def convert_to_ref_period(internal_period: str) -> str:
1086
+ """Convert internal time period format to ref_period format.
1087
+
1088
+ Internal format: "t+Q-1" or "t-Q1"
1089
+ Output format: "T-1Q" for one quarter back
1090
+ """
1091
+ if internal_period.startswith("t+"):
1092
+ # e.g., "t+Q-1" -> "T-1Q"
1093
+ indicator = internal_period[2]
1094
+ number = internal_period[3:]
1095
+ if number.startswith("-"):
1096
+ return f"T{number}{indicator}"
1097
+ return f"T+{number}{indicator}"
1098
+ elif internal_period.startswith("t-"):
1099
+ # e.g., "t-Q1" -> "T-1Q"
1100
+ indicator = internal_period[2]
1101
+ number = internal_period[3:]
1102
+ return f"T-{number}{indicator}"
1103
+ return "T"
1104
+
1105
+ try:
1106
+ ast = self.syntax_api.parse_expression(expression)
1107
+ extractor = TimeShiftExtractor()
1108
+ extractor.visit(ast)
1109
+
1110
+ return {table: convert_to_ref_period(period) for table, period in time_shifts.items()}
1111
+
1112
+ except Exception:
1113
+ return {}
1114
+
1115
+ def _detect_cross_module_dependencies(
1116
+ self,
1117
+ expression: str,
1118
+ variables_by_table: Dict[str, Dict[str, str]],
1119
+ primary_module_vid: Optional[int],
1120
+ operation_code: str,
1121
+ release_id: Optional[int] = None,
1122
+ ) -> tuple:
1123
+ """
1124
+ Detect cross-module dependencies for a single expression.
1125
+
1126
+ Uses existing OperationScopesAPI and ExplorerQuery to detect external module
1127
+ references in cross-module expressions.
1128
+
1129
+ Args:
1130
+ expression: DPM-XL expression
1131
+ variables_by_table: Variables by table code (from _extract_variables_from_ast)
1132
+ primary_module_vid: The module being exported (if known)
1133
+ operation_code: Current operation code
1134
+ release_id: Optional release ID for filtering
1135
+
1136
+ Returns:
1137
+ Tuple of (dependency_modules, cross_instance_dependencies)
1138
+ - dependency_modules: {uri: {tables: {...}, variables: {...}}}
1139
+ - cross_instance_dependencies: [{modules: [...], affected_operations: [...], ...}]
1140
+ """
1141
+ from py_dpm.api.dpm_xl.operation_scopes import OperationScopesAPI
1142
+ from py_dpm.dpm.queries.explorer_queries import ExplorerQuery
1143
+ import logging
1144
+
1145
+ scopes_api = OperationScopesAPI(
1146
+ database_path=self.database_path,
1147
+ connection_url=self.connection_url
1148
+ )
1149
+
1150
+ try:
1151
+ # Get tables with module info (includes module_version)
1152
+ tables_with_modules = scopes_api.get_tables_with_metadata_from_expression(
1153
+ expression=expression,
1154
+ release_id=release_id
1155
+ )
1156
+
1157
+ # Check if cross-module
1158
+ scope_result = scopes_api.calculate_scopes_from_expression(
1159
+ expression=expression,
1160
+ release_id=release_id,
1161
+ read_only=True
1162
+ )
1163
+
1164
+ if scope_result.has_error or not scope_result.is_cross_module:
1165
+ return {}, []
1166
+
1167
+ # Extract time shifts for each table from expression
1168
+ time_shifts_by_table = self._extract_time_shifts_by_table(expression)
1169
+
1170
+ # Determine primary module from first table if not provided
1171
+ if primary_module_vid is None and tables_with_modules:
1172
+ primary_module_vid = tables_with_modules[0].get("module_vid")
1173
+
1174
+ # Helper to normalize table code (remove 't' prefix if present)
1175
+ def normalize_table_code(code: str) -> str:
1176
+ return code[1:] if code and code.startswith('t') else code
1177
+
1178
+ # Helper to lookup ref_period for a table
1179
+ def get_ref_period(table_code: str) -> str:
1180
+ if not table_code:
1181
+ return "T"
1182
+ ref = time_shifts_by_table.get(table_code)
1183
+ if not ref:
1184
+ ref = time_shifts_by_table.get(normalize_table_code(table_code))
1185
+ return ref or "T"
1186
+
1187
+ # Helper to lookup variables for a table
1188
+ def get_table_variables(table_code: str) -> dict:
1189
+ if not table_code:
1190
+ return {}
1191
+ variables = variables_by_table.get(table_code)
1192
+ if not variables:
1193
+ variables = variables_by_table.get(f"t{table_code}", {})
1194
+ return variables or {}
1195
+
1196
+ # Group external tables by module
1197
+ external_modules = {}
1198
+ for table_info in tables_with_modules:
1199
+ module_vid = table_info.get("module_vid")
1200
+ if module_vid == primary_module_vid:
1201
+ continue # Skip primary module
1202
+
1203
+ module_code = table_info.get("module_code")
1204
+ if not module_code:
1205
+ continue
1206
+
1207
+ # Get module URI
1208
+ try:
1209
+ module_uri = ExplorerQuery.get_module_url(
1210
+ scopes_api.session,
1211
+ module_code=module_code,
1212
+ release_id=release_id,
1213
+ )
1214
+ if module_uri.endswith(".json"):
1215
+ module_uri = module_uri[:-5]
1216
+ except Exception:
1217
+ continue
1218
+
1219
+ table_code = table_info.get("code")
1220
+ ref_period = get_ref_period(table_code)
1221
+
1222
+ if module_uri not in external_modules:
1223
+ external_modules[module_uri] = {
1224
+ "module_vid": module_vid,
1225
+ "module_version": table_info.get("module_version"), # Already in table_info
1226
+ "ref_period": ref_period,
1227
+ "tables": {},
1228
+ "variables": {},
1229
+ "from_date": None,
1230
+ "to_date": None
1231
+ }
1232
+ elif ref_period != "T":
1233
+ # Keep most specific ref_period (non-T takes precedence)
1234
+ external_modules[module_uri]["ref_period"] = ref_period
1235
+
1236
+ # Add table and variables
1237
+ if table_code:
1238
+ table_variables = get_table_variables(table_code)
1239
+ external_modules[module_uri]["tables"][table_code] = {
1240
+ "variables": table_variables,
1241
+ "open_keys": {}
1242
+ }
1243
+ external_modules[module_uri]["variables"].update(table_variables)
1244
+
1245
+ # Get date info from scopes metadata
1246
+ scopes_metadata = scopes_api.get_scopes_with_metadata_from_expression(
1247
+ expression=expression,
1248
+ release_id=release_id
1249
+ )
1250
+ for scope_info in scopes_metadata:
1251
+ for module in scope_info.module_versions:
1252
+ mvid = module.get("module_vid")
1253
+ for uri, data in external_modules.items():
1254
+ if data["module_vid"] == mvid:
1255
+ data["from_date"] = module.get("from_reference_date")
1256
+ data["to_date"] = module.get("to_reference_date")
1257
+
1258
+ # Build output structures
1259
+ dependency_modules = {}
1260
+ cross_instance_dependencies = []
1261
+
1262
+ for uri, data in external_modules.items():
1263
+ # dependency_modules entry
1264
+ dependency_modules[uri] = {
1265
+ "tables": data["tables"],
1266
+ "variables": data["variables"]
1267
+ }
1268
+
1269
+ # cross_instance_dependencies entry (one per external module)
1270
+ from_date = data["from_date"]
1271
+ to_date = data["to_date"]
1272
+ module_entry = {
1273
+ "URI": uri,
1274
+ "ref_period": data["ref_period"]
1275
+ }
1276
+ # Add module_version if available
1277
+ if data["module_version"]:
1278
+ module_entry["module_version"] = data["module_version"]
1279
+
1280
+ cross_instance_dependencies.append({
1281
+ "modules": [module_entry],
1282
+ "affected_operations": [operation_code],
1283
+ "from_reference_date": str(from_date) if from_date else "",
1284
+ "to_reference_date": str(to_date) if to_date else ""
1285
+ })
1286
+
1287
+ return dependency_modules, cross_instance_dependencies
1288
+
1289
+ except Exception as e:
1290
+ logging.warning(f"Failed to detect cross-module dependencies: {e}")
1291
+ return {}, []
1292
+ finally:
1293
+ scopes_api.close()
1294
+
1012
1295
  def _add_coordinates_to_ast(
1013
1296
  self, ast_dict: Dict[str, Any], context: Optional[Dict[str, Any]]
1014
1297
  ) -> Dict[str, Any]:
@@ -117,6 +117,7 @@ def generate_enriched_ast(
117
117
  table_context: Optional[Dict[str, Any]] = None,
118
118
  precondition: Optional[str] = None,
119
119
  release_id: Optional[int] = None,
120
+ primary_module_vid: Optional[int] = None,
120
121
  ) -> Dict[str, Any]:
121
122
  """
122
123
  Generate enriched, engine-ready AST from DPM-XL expression.
@@ -133,6 +134,8 @@ def generate_enriched_ast(
133
134
  precondition: Optional precondition variable reference (e.g., {v_F_44_04})
134
135
  release_id: Optional release ID to filter database lookups by specific release.
135
136
  If None, uses all available data (release-agnostic).
137
+ primary_module_vid: Optional module version ID of the module being exported.
138
+ When provided, enables detection of cross-module dependencies.
136
139
 
137
140
  Returns:
138
141
  dict: {
@@ -153,6 +156,7 @@ def generate_enriched_ast(
153
156
  table_context=table_context,
154
157
  precondition=precondition,
155
158
  release_id=release_id,
159
+ primary_module_vid=primary_module_vid,
156
160
  )
157
161
 
158
162
 
@@ -165,6 +169,8 @@ def enrich_ast_with_metadata(
165
169
  dpm_version: Optional[str] = None,
166
170
  operation_code: Optional[str] = None,
167
171
  precondition: Optional[str] = None,
172
+ release_id: Optional[int] = None,
173
+ primary_module_vid: Optional[int] = None,
168
174
  ) -> Dict[str, Any]:
169
175
  """
170
176
  Add framework structure (operations, variables, tables, preconditions) to complete AST.
@@ -180,6 +186,8 @@ def enrich_ast_with_metadata(
180
186
  dpm_version: DPM version code (e.g., "4.2")
181
187
  operation_code: Operation code (defaults to "default_code")
182
188
  precondition: Precondition variable reference (e.g., {v_F_44_04})
189
+ release_id: Optional release ID to filter database lookups
190
+ primary_module_vid: Optional module VID of the module being exported
183
191
 
184
192
  Returns:
185
193
  dict: Engine-ready AST with framework structure
@@ -196,4 +204,6 @@ def enrich_ast_with_metadata(
196
204
  dpm_version=dpm_version,
197
205
  operation_code=operation_code,
198
206
  precondition=precondition,
207
+ release_id=release_id,
208
+ primary_module_vid=primary_module_vid,
199
209
  )