pydpm_xl 0.2.3__py3-none-any.whl → 0.2.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
py_dpm/__init__.py CHANGED
@@ -41,7 +41,7 @@ Available packages:
41
41
  - pydpm.api: Main APIs for migration, syntax, and semantic analysis
42
42
  """
43
43
 
44
- __version__ = "0.2.3"
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"
@@ -31,7 +31,7 @@ class InstanceAPI:
31
31
  Args:
32
32
  instance_data: Dictionary with the instance configuration.
33
33
  Must contain: module_code, parameters (with refPeriod),
34
- and operands (list of facts)
34
+ and facts (list of facts)
35
35
  output_folder: Directory where the output ZIP file will be created
36
36
  file_prefix: Optional prefix for the output filename
37
37
 
@@ -49,7 +49,7 @@ class InstanceAPI:
49
49
  ... "parameters": {
50
50
  ... "refPeriod": "2024-12-31"
51
51
  ... },
52
- ... "operands": [
52
+ ... "facts": [
53
53
  ... {
54
54
  ... "table_code": "t001",
55
55
  ... "row_code": "r010",
@@ -78,7 +78,7 @@ class InstanceAPI:
78
78
  Args:
79
79
  json_file: Path to JSON file with instance configuration.
80
80
  Must contain: module_code, parameters (with refPeriod),
81
- and operands (list of facts)
81
+ and facts (list of facts)
82
82
  output_folder: Directory where the output ZIP file will be created
83
83
  file_prefix: Optional prefix for the output filename
84
84
 
@@ -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
  )
py_dpm/dpm/models.py CHANGED
@@ -720,16 +720,20 @@ class ModuleVersion(Base):
720
720
  release_id: Optional release ID to filter modules by release range
721
721
 
722
722
  Returns:
723
- pandas DataFrame with columns: ModuleVID, TableVID (as VARIABLE_VID),
724
- FromReferenceDate, ToReferenceDate, EndReleaseID
723
+ pandas DataFrame with columns: ModuleVID, TableVID (as variable_vid),
724
+ ModuleCode, VersionNumber, FromReferenceDate, ToReferenceDate,
725
+ StartReleaseID, EndReleaseID
725
726
  """
726
727
  if not tables_vids:
727
728
  return pd.DataFrame(
728
729
  columns=[
729
730
  "ModuleVID",
730
731
  "variable_vid",
732
+ "ModuleCode",
733
+ "VersionNumber",
731
734
  "FromReferenceDate",
732
735
  "ToReferenceDate",
736
+ "StartReleaseID",
733
737
  "EndReleaseID",
734
738
  ]
735
739
  )
@@ -738,8 +742,11 @@ class ModuleVersion(Base):
738
742
  session.query(
739
743
  cls.modulevid.label("ModuleVID"),
740
744
  ModuleVersionComposition.tablevid.label("variable_vid"),
745
+ cls.code.label("ModuleCode"),
746
+ cls.versionnumber.label("VersionNumber"),
741
747
  cls.fromreferencedate.label("FromReferenceDate"),
742
748
  cls.toreferencedate.label("ToReferenceDate"),
749
+ cls.startreleaseid.label("StartReleaseID"),
743
750
  cls.endreleaseid.label("EndReleaseID"),
744
751
  )
745
752
  .join(
@@ -759,16 +766,20 @@ class ModuleVersion(Base):
759
766
  )
760
767
 
761
768
  results = query.all()
762
- return pd.DataFrame(
769
+ df = pd.DataFrame(
763
770
  results,
764
771
  columns=[
765
772
  "ModuleVID",
766
773
  "variable_vid",
774
+ "ModuleCode",
775
+ "VersionNumber",
767
776
  "FromReferenceDate",
768
777
  "ToReferenceDate",
778
+ "StartReleaseID",
769
779
  "EndReleaseID",
770
780
  ],
771
781
  )
782
+ return cls._apply_fallback_for_equal_dates(session, df)
772
783
 
773
784
  @classmethod
774
785
  def get_from_table_codes(cls, session, table_codes, release_id=None):
@@ -790,6 +801,8 @@ class ModuleVersion(Base):
790
801
  columns=[
791
802
  "ModuleVID",
792
803
  "variable_vid",
804
+ "ModuleCode",
805
+ "VersionNumber",
793
806
  "FromReferenceDate",
794
807
  "ToReferenceDate",
795
808
  "StartReleaseID",
@@ -804,6 +817,8 @@ class ModuleVersion(Base):
804
817
  session.query(
805
818
  cls.modulevid.label("ModuleVID"),
806
819
  ModuleVersionComposition.tablevid.label("variable_vid"),
820
+ cls.code.label("ModuleCode"),
821
+ cls.versionnumber.label("VersionNumber"),
807
822
  cls.fromreferencedate.label("FromReferenceDate"),
808
823
  cls.toreferencedate.label("ToReferenceDate"),
809
824
  cls.startreleaseid.label("StartReleaseID"),
@@ -831,11 +846,13 @@ class ModuleVersion(Base):
831
846
  )
832
847
 
833
848
  results = query.all()
834
- return pd.DataFrame(
849
+ df = pd.DataFrame(
835
850
  results,
836
851
  columns=[
837
852
  "ModuleVID",
838
853
  "variable_vid",
854
+ "ModuleCode",
855
+ "VersionNumber",
839
856
  "FromReferenceDate",
840
857
  "ToReferenceDate",
841
858
  "StartReleaseID",
@@ -843,6 +860,7 @@ class ModuleVersion(Base):
843
860
  "TableCode",
844
861
  ],
845
862
  )
863
+ return cls._apply_fallback_for_equal_dates(session, df)
846
864
 
847
865
  @classmethod
848
866
  def get_precondition_module_versions(
@@ -857,16 +875,21 @@ class ModuleVersion(Base):
857
875
  release_id: Optional release ID to filter modules by release range
858
876
 
859
877
  Returns:
860
- pandas DataFrame with columns: ModuleVID, VariableVID (as VARIABLE_VID),
861
- FromReferenceDate, ToReferenceDate, Code
878
+ pandas DataFrame with columns: ModuleVID, variable_vid (VariableVID),
879
+ ModuleCode, VersionNumber, FromReferenceDate, ToReferenceDate,
880
+ StartReleaseID, EndReleaseID, Code
862
881
  """
863
882
  if not precondition_items:
864
883
  return pd.DataFrame(
865
884
  columns=[
866
885
  "ModuleVID",
867
886
  "variable_vid",
887
+ "ModuleCode",
888
+ "VersionNumber",
868
889
  "FromReferenceDate",
869
890
  "ToReferenceDate",
891
+ "StartReleaseID",
892
+ "EndReleaseID",
870
893
  "Code",
871
894
  ]
872
895
  )
@@ -875,8 +898,12 @@ class ModuleVersion(Base):
875
898
  session.query(
876
899
  cls.modulevid.label("ModuleVID"),
877
900
  VariableVersion.variablevid.label("variable_vid"),
901
+ cls.code.label("ModuleCode"),
902
+ cls.versionnumber.label("VersionNumber"),
878
903
  cls.fromreferencedate.label("FromReferenceDate"),
879
904
  cls.toreferencedate.label("ToReferenceDate"),
905
+ cls.startreleaseid.label("StartReleaseID"),
906
+ cls.endreleaseid.label("EndReleaseID"),
880
907
  VariableVersion.code.label("Code"),
881
908
  )
882
909
  .join(ModuleParameters, cls.modulevid == ModuleParameters.modulevid)
@@ -899,16 +926,21 @@ class ModuleVersion(Base):
899
926
  )
900
927
 
901
928
  results = query.all()
902
- return pd.DataFrame(
929
+ df = pd.DataFrame(
903
930
  results,
904
931
  columns=[
905
932
  "ModuleVID",
906
933
  "variable_vid",
934
+ "ModuleCode",
935
+ "VersionNumber",
907
936
  "FromReferenceDate",
908
937
  "ToReferenceDate",
938
+ "StartReleaseID",
939
+ "EndReleaseID",
909
940
  "Code",
910
941
  ],
911
942
  )
943
+ return cls._apply_fallback_for_equal_dates(session, df)
912
944
 
913
945
  @classmethod
914
946
  def get_module_version_by_vid(cls, session, vid):
@@ -946,6 +978,170 @@ class ModuleVersion(Base):
946
978
  ],
947
979
  )
948
980
 
981
+ @classmethod
982
+ def _apply_fallback_for_equal_dates(cls, session, df, module_vid_col="ModuleVID"):
983
+ """
984
+ Apply fallback logic for rows where FromReferenceDate == ToReferenceDate.
985
+
986
+ For each such row, find the previous module version (same moduleid,
987
+ highest startreleaseid less than current) and replace module-specific
988
+ columns while preserving association columns (variable_vid, TableCode, Code).
989
+
990
+ Args:
991
+ session: SQLAlchemy session
992
+ df: pandas DataFrame with module version data
993
+ module_vid_col: Column name for module version ID (default: "ModuleVID")
994
+
995
+ Returns:
996
+ pandas DataFrame with fallback logic applied
997
+ """
998
+ if df.empty:
999
+ return df
1000
+
1001
+ # Identify rows needing fallback
1002
+ mask = df["FromReferenceDate"] == df["ToReferenceDate"]
1003
+ rows_needing_fallback = df[mask]
1004
+
1005
+ if rows_needing_fallback.empty:
1006
+ return df
1007
+
1008
+ # Get unique ModuleVIDs that need fallback
1009
+ module_vids_needing_fallback = (
1010
+ rows_needing_fallback[module_vid_col].unique().tolist()
1011
+ )
1012
+
1013
+ # Batch query: get module info (moduleid, startreleaseid) for affected rows
1014
+ current_modules = (
1015
+ session.query(
1016
+ cls.modulevid,
1017
+ cls.moduleid,
1018
+ cls.startreleaseid,
1019
+ )
1020
+ .filter(cls.modulevid.in_(module_vids_needing_fallback))
1021
+ .all()
1022
+ )
1023
+
1024
+ # Build mapping: current_modulevid -> (moduleid, startreleaseid)
1025
+ current_module_info = {
1026
+ row.modulevid: (row.moduleid, row.startreleaseid) for row in current_modules
1027
+ }
1028
+
1029
+ # Get all potential previous versions for the affected modules
1030
+ unique_module_ids = list(set(info[0] for info in current_module_info.values()))
1031
+
1032
+ previous_versions_query = (
1033
+ session.query(cls)
1034
+ .filter(cls.moduleid.in_(unique_module_ids))
1035
+ .order_by(cls.moduleid, cls.startreleaseid.desc())
1036
+ .all()
1037
+ )
1038
+
1039
+ # Build lookup: moduleid -> list of versions sorted by startreleaseid desc
1040
+ versions_by_moduleid = {}
1041
+ for mv in previous_versions_query:
1042
+ if mv.moduleid not in versions_by_moduleid:
1043
+ versions_by_moduleid[mv.moduleid] = []
1044
+ versions_by_moduleid[mv.moduleid].append(mv)
1045
+
1046
+ # For each current modulevid, find the previous version
1047
+ replacement_map = {} # current_modulevid -> previous_moduleversion
1048
+ for current_vid, (moduleid, current_startreleaseid) in current_module_info.items():
1049
+ versions = versions_by_moduleid.get(moduleid, [])
1050
+ for mv in versions:
1051
+ if mv.startreleaseid < current_startreleaseid:
1052
+ replacement_map[current_vid] = mv
1053
+ break # Already sorted desc, so first match is highest
1054
+
1055
+ # Apply replacements to DataFrame
1056
+ if not replacement_map:
1057
+ return df
1058
+
1059
+ # Create a copy to avoid modifying original
1060
+ result_df = df.copy()
1061
+
1062
+ for idx, row in result_df.iterrows():
1063
+ if row["FromReferenceDate"] == row["ToReferenceDate"]:
1064
+ current_vid = row[module_vid_col]
1065
+ if current_vid in replacement_map:
1066
+ prev_mv = replacement_map[current_vid]
1067
+ result_df.at[idx, "ModuleVID"] = prev_mv.modulevid
1068
+ result_df.at[idx, "ModuleCode"] = prev_mv.code
1069
+ result_df.at[idx, "VersionNumber"] = prev_mv.versionnumber
1070
+ result_df.at[idx, "FromReferenceDate"] = prev_mv.fromreferencedate
1071
+ result_df.at[idx, "ToReferenceDate"] = prev_mv.toreferencedate
1072
+ if "StartReleaseID" in result_df.columns:
1073
+ result_df.at[idx, "StartReleaseID"] = prev_mv.startreleaseid
1074
+ if "EndReleaseID" in result_df.columns:
1075
+ result_df.at[idx, "EndReleaseID"] = prev_mv.endreleaseid
1076
+
1077
+ return result_df
1078
+
1079
+ @classmethod
1080
+ def get_from_release_id(
1081
+ cls, session, release_id, module_id=None, module_code=None
1082
+ ):
1083
+ """
1084
+ Get the module version applicable to a given release for a specific module.
1085
+
1086
+ If the resulting module version has fromreferencedate == toreferencedate,
1087
+ the previous module version for the same module is returned instead.
1088
+
1089
+ Args:
1090
+ session: SQLAlchemy session
1091
+ release_id: The release ID to filter for
1092
+ module_id: Optional module ID (mutually exclusive with module_code)
1093
+ module_code: Optional module code (mutually exclusive with module_id)
1094
+
1095
+ Returns:
1096
+ ModuleVersion instance or None if not found
1097
+
1098
+ Raises:
1099
+ ValueError: If neither module_id nor module_code is provided,
1100
+ or if both are provided
1101
+ """
1102
+ if module_id is None and module_code is None:
1103
+ raise ValueError("Either module_id or module_code must be provided.")
1104
+ if module_id is not None and module_code is not None:
1105
+ raise ValueError(
1106
+ "Specify only one of module_id or module_code, not both."
1107
+ )
1108
+
1109
+ # Build the base query with release filtering
1110
+ query = session.query(cls).filter(
1111
+ and_(
1112
+ cls.startreleaseid <= release_id,
1113
+ or_(cls.endreleaseid > release_id, cls.endreleaseid.is_(None)),
1114
+ )
1115
+ )
1116
+
1117
+ # Apply module filter
1118
+ if module_id is not None:
1119
+ query = query.filter(cls.moduleid == module_id)
1120
+ else: # module_code
1121
+ query = query.filter(cls.code == module_code)
1122
+
1123
+ module_version = query.first()
1124
+
1125
+ if module_version is None:
1126
+ return None
1127
+
1128
+ # Check if fromreferencedate == toreferencedate
1129
+ if module_version.fromreferencedate == module_version.toreferencedate:
1130
+ # Get the previous module version for the same module
1131
+ prev_query = (
1132
+ session.query(cls)
1133
+ .filter(
1134
+ cls.moduleid == module_version.moduleid,
1135
+ cls.startreleaseid < module_version.startreleaseid,
1136
+ )
1137
+ .order_by(cls.startreleaseid.desc())
1138
+ )
1139
+ prev_module_version = prev_query.first()
1140
+ if prev_module_version:
1141
+ return prev_module_version
1142
+
1143
+ return module_version
1144
+
949
1145
  @classmethod
950
1146
  def get_last_release(cls, session):
951
1147
  """
@@ -1119,6 +1315,60 @@ class OperationScope(Base):
1119
1315
  "OperationScopeComposition", back_populates="operation_scope"
1120
1316
  )
1121
1317
 
1318
+ def to_dict(self):
1319
+ """
1320
+ Convert the operation scope to a dictionary representation.
1321
+
1322
+ Returns:
1323
+ dict: A dictionary with module codes as keys and module details as values.
1324
+ Format: {
1325
+ "<module_code>": {
1326
+ "module_version_number": <versionnumber>,
1327
+ "from_reference_date": <fromreferencedate>,
1328
+ "to_reference_date": <toreferencedate>
1329
+ },
1330
+ ...
1331
+ }
1332
+ """
1333
+ from sqlalchemy.orm import object_session
1334
+
1335
+ def format_date(date_value):
1336
+ """Format date to string (YYYY-MM-DD) or None if NaT/None."""
1337
+ if date_value is None:
1338
+ return None
1339
+ if pd.isna(date_value):
1340
+ return None
1341
+ if hasattr(date_value, "strftime"):
1342
+ return date_value.strftime("%Y-%m-%d")
1343
+ return str(date_value)
1344
+
1345
+ result = {}
1346
+ for composition in self.operation_scope_compositions:
1347
+ # For new/proposed scopes, use transient _module_info attribute
1348
+ if hasattr(composition, "_module_info") and composition._module_info:
1349
+ info = composition._module_info
1350
+ result[info["code"]] = {
1351
+ "module_version_number": info["version_number"],
1352
+ "from_reference_date": format_date(info["from_reference_date"]),
1353
+ "to_reference_date": format_date(info["to_reference_date"]),
1354
+ }
1355
+ else:
1356
+ # For existing scopes from DB, use relationship or query
1357
+ module_version = composition.module_version
1358
+ if module_version is None:
1359
+ session = object_session(self)
1360
+ if session is not None:
1361
+ module_version = session.query(ModuleVersion).filter(
1362
+ ModuleVersion.modulevid == composition.modulevid
1363
+ ).first()
1364
+ if module_version is not None:
1365
+ result[module_version.code] = {
1366
+ "module_version_number": module_version.versionnumber,
1367
+ "from_reference_date": format_date(module_version.fromreferencedate),
1368
+ "to_reference_date": format_date(module_version.toreferencedate),
1369
+ }
1370
+ return result
1371
+
1122
1372
 
1123
1373
  class OperationScopeComposition(Base):
1124
1374
  __tablename__ = "OperationScopeComposition"
@@ -71,9 +71,19 @@ class OperationScopeService:
71
71
  if len(modules_info_dataframe) == 1:
72
72
  module_vid = modules_vids[0]
73
73
  from_date = modules_info_dataframe["FromReferenceDate"].values[0]
74
+ to_date = modules_info_dataframe["ToReferenceDate"].values[0]
75
+ module_code = modules_info_dataframe["ModuleCode"].values[0]
76
+ version_number = modules_info_dataframe["VersionNumber"].values[0]
74
77
  operation_scope = self.create_operation_scope(from_date)
75
78
  self.create_operation_scope_composition(
76
- operation_scope=operation_scope, module_vid=module_vid
79
+ operation_scope=operation_scope,
80
+ module_vid=module_vid,
81
+ module_info={
82
+ "code": module_code,
83
+ "version_number": version_number,
84
+ "from_reference_date": from_date,
85
+ "to_reference_date": to_date,
86
+ },
77
87
  )
78
88
  else:
79
89
  intra_modules = []
@@ -83,11 +93,10 @@ class OperationScopeService:
83
93
  if table_codes:
84
94
  unique_operands_number = len(table_codes) + len(precondition_items)
85
95
 
86
- # Categorize modules by lifecycle: starting vs ending in this release
87
- starting_modules = (
88
- {}
89
- ) # Modules that START in this release (replacements)
90
- ending_modules = {} # Modules that END in this release (being replaced)
96
+ # First pass: categorize modules by table code and lifecycle
97
+ # We track lifecycle to handle version transitions within the SAME module
98
+ starting_by_code = {} # table_code -> [module_vids that START in this release]
99
+ ending_by_code = {} # table_code -> [module_vids that END or are active]
91
100
 
92
101
  for module_vid, group_df in modules_info_dataframe.groupby(MODULE_VID):
93
102
  table_codes_in_module = (
@@ -105,31 +114,55 @@ class OperationScopeService:
105
114
  end_release = group_df["EndReleaseID"].values[0]
106
115
 
107
116
  # Determine if this is a "new" module starting in this release
108
- # or an "old" module ending in this release
109
117
  is_starting = start_release == release_id
110
- is_ending = end_release == release_id or end_release == float(
111
- release_id
112
- )
113
118
 
114
119
  if len(table_codes_in_module) == unique_operands_number:
115
120
  # Intra-module: include ALL modules active in the release
116
- # (don't filter by lifecycle - that's only for cross-module)
117
121
  intra_modules.append(module_vid)
118
122
  else:
119
- # For cross-module, group by table code AND lifecycle stage
120
- target_dict = (
121
- starting_modules if is_starting else ending_modules
122
- )
123
+ # Track modules by table code and lifecycle
123
124
  for table_code in table_codes_in_module:
124
- if table_code not in target_dict:
125
- target_dict[table_code] = []
126
- target_dict[table_code].append(module_vid)
127
-
128
- # Process cross-module scopes separately for each generation
129
- if starting_modules:
130
- cross_modules["_starting"] = starting_modules
131
- if ending_modules:
132
- cross_modules["_ending"] = ending_modules
125
+ if is_starting:
126
+ if table_code not in starting_by_code:
127
+ starting_by_code[table_code] = []
128
+ starting_by_code[table_code].append(module_vid)
129
+ else:
130
+ if table_code not in ending_by_code:
131
+ ending_by_code[table_code] = []
132
+ ending_by_code[table_code].append(module_vid)
133
+
134
+ # Second pass: determine if lifecycle separation is needed
135
+ # Only separate if a table code has modules in BOTH starting and ending
136
+ # (indicating a version transition for that table)
137
+ needs_lifecycle_separation = any(
138
+ code in starting_by_code and code in ending_by_code
139
+ for code in set(starting_by_code.keys()) | set(ending_by_code.keys())
140
+ )
141
+
142
+ if needs_lifecycle_separation:
143
+ # Separate into starting and ending scopes
144
+ starting_modules = {}
145
+ ending_modules = {}
146
+ for code, vids in starting_by_code.items():
147
+ starting_modules[code] = vids
148
+ for code, vids in ending_by_code.items():
149
+ ending_modules[code] = vids
150
+ if starting_modules:
151
+ cross_modules["_starting"] = starting_modules
152
+ if ending_modules:
153
+ cross_modules["_ending"] = ending_modules
154
+ else:
155
+ # No version transitions - combine all modules by table code
156
+ all_by_code = {}
157
+ for code, vids in starting_by_code.items():
158
+ if code not in all_by_code:
159
+ all_by_code[code] = []
160
+ all_by_code[code].extend(vids)
161
+ for code, vids in ending_by_code.items():
162
+ if code not in all_by_code:
163
+ all_by_code[code] = []
164
+ all_by_code[code].extend(vids)
165
+ cross_modules = all_by_code
133
166
  else:
134
167
  # Original logic for table VIDs
135
168
  unique_operands_number = len(tables_vids) + len(precondition_items)
@@ -268,12 +301,21 @@ class OperationScopeService:
268
301
  :param modules_vids: list with module version ids
269
302
  """
270
303
  for module_vid in modules_vids:
271
- from_date = modules_info[modules_info["ModuleVID"] == module_vid][
272
- "FromReferenceDate"
273
- ].values[0]
304
+ module_row = modules_info[modules_info["ModuleVID"] == module_vid].iloc[0]
305
+ from_date = module_row["FromReferenceDate"]
306
+ to_date = module_row["ToReferenceDate"]
307
+ module_code = module_row["ModuleCode"]
308
+ version_number = module_row["VersionNumber"]
274
309
  operation_scope = self.create_operation_scope(from_date)
275
310
  self.create_operation_scope_composition(
276
- operation_scope=operation_scope, module_vid=module_vid
311
+ operation_scope=operation_scope,
312
+ module_vid=module_vid,
313
+ module_info={
314
+ "code": module_code,
315
+ "version_number": version_number,
316
+ "from_reference_date": from_date,
317
+ "to_reference_date": to_date,
318
+ },
277
319
  )
278
320
 
279
321
  def process_cross_module(self, cross_modules, modules_dataframe):
@@ -313,8 +355,18 @@ class OperationScopeService:
313
355
  operation_scope = self.create_operation_scope(from_submission_date)
314
356
  combination = set(combination)
315
357
  for module in combination:
358
+ module_row = modules_dataframe[
359
+ modules_dataframe[MODULE_VID] == module
360
+ ].iloc[0]
316
361
  self.create_operation_scope_composition(
317
- operation_scope=operation_scope, module_vid=module
362
+ operation_scope=operation_scope,
363
+ module_vid=module,
364
+ module_info={
365
+ "code": module_row["ModuleCode"],
366
+ "version_number": module_row["VersionNumber"],
367
+ "from_reference_date": module_row[FROM_REFERENCE_DATE],
368
+ "to_reference_date": module_row[TO_REFERENCE_DATE],
369
+ },
318
370
  )
319
371
 
320
372
  def create_operation_scope(self, submission_date):
@@ -340,17 +392,21 @@ class OperationScopeService:
340
392
  self.session.add(operation_scope)
341
393
  return operation_scope
342
394
 
343
- def create_operation_scope_composition(self, operation_scope, module_vid):
395
+ def create_operation_scope_composition(self, operation_scope, module_vid, module_info=None):
344
396
  """
345
397
  Method to populate OperationScopeComposition table
346
398
  :param operation_scope: Operation scope data
347
399
  :param module_vid: Module version id
400
+ :param module_info: Optional dict with module info (code, from_reference_date, to_reference_date)
348
401
  """
349
402
  operation_scope_composition = OperationScopeComposition(
350
403
  operation_scope=operation_scope,
351
404
  modulevid=module_vid,
352
405
  rowguid=str(uuid.uuid4()),
353
406
  )
407
+ # Store module info as transient attribute for to_dict() access
408
+ if module_info:
409
+ operation_scope_composition._module_info = module_info
354
410
  self.session.add(operation_scope_composition)
355
411
 
356
412
  def get_scopes_with_status(self):
@@ -25,8 +25,7 @@ class Fact:
25
25
  self.open_values = open_values
26
26
  self.value = value
27
27
  self.variable_id = None
28
-
29
- self.resolve_datapoint_id(date)
28
+ self._date = date
30
29
 
31
30
  def __str__(self):
32
31
  return f"Operand(table={self.table_code}, column={self.column_code}, row={self.row_code}, sheet={self.sheet_code}, open_values={self.open_values}, value={self.value})"
@@ -116,7 +115,7 @@ class Instance:
116
115
 
117
116
  @staticmethod
118
117
  def _validate_dict_structure(instance_json: dict):
119
- required_keys = {"module_code", "parameters", "operands"}
118
+ required_keys = {"module_code", "parameters", "facts"}
120
119
  if required_keys != set(instance_json.keys()):
121
120
  missing = required_keys - set(instance_json.keys())
122
121
  raise ValueError(f"Missing required keys: {missing}")
@@ -130,8 +129,8 @@ class Instance:
130
129
  if "refPeriod" not in instance_json["parameters"]:
131
130
  raise ValueError("parameters must contain 'refPeriod'")
132
131
 
133
- if not isinstance(instance_json["operands"], list):
134
- raise TypeError("operands must be a list")
132
+ if not isinstance(instance_json["facts"], list):
133
+ raise TypeError("facts must be a list")
135
134
 
136
135
  @classmethod
137
136
  def from_json_file(cls, json_file: Path):
@@ -144,21 +143,58 @@ class Instance:
144
143
  def from_dict(cls, instance_json: dict):
145
144
  cls._validate_dict_structure(instance_json)
146
145
 
147
- url = ExplorerQueryAPI().get_module_url(
148
- module_code=instance_json["module_code"],
149
- date=instance_json["parameters"]["refPeriod"],
150
- )
151
-
152
146
  parameters = cls.PARAMETERS_DEFAULT.copy()
153
147
  parameters.update(instance_json["parameters"])
154
148
 
155
- operands = {}
156
- for operand in instance_json["operands"]:
157
- operand["date"] = parameters["refPeriod"]
158
- operand = Fact.from_dict(operand)
159
- if operand.table_code not in operands:
160
- operands[operand.table_code] = []
161
- operands[operand.table_code].append(operand)
149
+ ref_period = parameters["refPeriod"]
150
+
151
+ with ExplorerQueryAPI() as explorer:
152
+ url = explorer.get_module_url(
153
+ module_code=instance_json["module_code"],
154
+ date=ref_period,
155
+ )
156
+
157
+ # Build Fact objects grouped by table without triggering DB lookups
158
+ operands = {}
159
+ for fact_data in instance_json["facts"]:
160
+ fact = Fact.from_dict(fact_data)
161
+ if fact.table_code not in operands:
162
+ operands[fact.table_code] = []
163
+ operands[fact.table_code].append(fact)
164
+
165
+ # Resolve datapoint IDs in batches per table
166
+ for table_code, facts in operands.items():
167
+ variables = explorer.get_variable_from_cell_address(
168
+ table_code=table_code,
169
+ row_code=None,
170
+ column_code=None,
171
+ sheet_code=None,
172
+ date=ref_period,
173
+ )
174
+
175
+ # Build mapping from (row, column, sheet) -> list of variable rows
176
+ variable_map = {}
177
+ for var in variables:
178
+ key = (
179
+ var.get("row_code"),
180
+ var.get("column_code"),
181
+ var.get("sheet_code"),
182
+ )
183
+ variable_map.setdefault(key, []).append(var)
184
+
185
+ # Assign variable_id to each fact, preserving previous error semantics
186
+ for fact in facts:
187
+ key = (fact.row_code, fact.column_code, fact.sheet_code)
188
+ candidates = variable_map.get(key, [])
189
+
190
+ if len(candidates) == 0:
191
+ raise ValueError(f"No mapping found for {fact.operand_code}")
192
+ if len(candidates) > 1:
193
+ raise ValueError(
194
+ f"Multiple mappings found for {fact.operand_code}"
195
+ )
196
+
197
+ fact.variable_id = candidates[0]["variable_id"]
162
198
 
163
199
  instance = cls(
164
200
  module_url=url,
@@ -212,12 +248,15 @@ class Instance:
212
248
 
213
249
  # FilingIndicators.csv
214
250
  filing_indicators_lines = ["templateID,reported"]
251
+ seen_templates = set()
215
252
  for table in self.operands.keys():
216
- if "." in table:
217
- table = table.split(".")
218
- table = table[0] + "." + table[1]
219
- if f"{table},true" not in filing_indicators_lines:
220
- filing_indicators_lines.append(f"{table},true")
253
+ normalized_table = table
254
+ if "." in normalized_table:
255
+ parts = normalized_table.split(".")
256
+ normalized_table = parts[0] + "." + parts[1]
257
+ if normalized_table not in seen_templates:
258
+ seen_templates.add(normalized_table)
259
+ filing_indicators_lines.append(f"{normalized_table},true")
221
260
 
222
261
  (reports_dir / "FilingIndicators.csv").write_text(
223
262
  "\n".join(filing_indicators_lines)
@@ -262,4 +301,4 @@ class Instance:
262
301
 
263
302
  print(f"Instance package written to: {output_path}")
264
303
 
265
- return output_path
304
+ return output_path
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydpm_xl
3
- Version: 0.2.3
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
@@ -1,14 +1,14 @@
1
- py_dpm/__init__.py,sha256=AsLh4kv-iu9KOJD1YfgPwAP2PD-WGt2ZHfgNtpU25tw,1858
1
+ py_dpm/__init__.py,sha256=SPXDScW5Pf4DjIIpaWm9UcOpbqRo_2u7nEZIIJaMs-k,1858
2
2
  py_dpm/api/__init__.py,sha256=n79vAD7qatlYaXaI2N5IAD9m_8Fgb00EOdapVXZYTpI,1081
3
3
  py_dpm/api/dpm/__init__.py,sha256=HQflgiRbs1eDi3KTadNhxS1NoaG6PGQDVMvFnuIEfXo,506
4
4
  py_dpm/api/dpm/data_dictionary.py,sha256=g0h6Yfschz7rboYly9LTbP-2SS5UxltU3AXu0v0tqrU,29457
5
5
  py_dpm/api/dpm/explorer.py,sha256=gW2RC59XwGl9YbEA-M4syHAs6MvqPWVw4wR_XdVFJ4Y,7888
6
6
  py_dpm/api/dpm/hierarchical_queries.py,sha256=X4AbpsWy3iItOTVIdVbtaTmRgOHPf0Y64Ig-_377uns,4054
7
- py_dpm/api/dpm/instance.py,sha256=OSq-sEZ9bn3zNZFQV6vI3Ed0c20s5VzhDwYEhemZ4Dc,3804
7
+ py_dpm/api/dpm/instance.py,sha256=v3DWzdaM5gPCecLjwjZ49FGfqZzUR3dPC0U8zGwdttk,3795
8
8
  py_dpm/api/dpm/migration.py,sha256=9FT7zzz4QdUIRR6MD01gMODBtfq9HH_RF4hRgZqMcZc,2404
9
9
  py_dpm/api/dpm_xl/__init__.py,sha256=aRjaMAf_i2a33UAGTg-TF1BfO6miOOrbCydTUqAVvRU,910
10
- py_dpm/api/dpm_xl/ast_generator.py,sha256=maVFlxBnBBZ6UYgVxbvewPFTbrTUqXfdKpmjB0ue0qc,42979
11
- py_dpm/api/dpm_xl/complete_ast.py,sha256=sOaN9uSQo-Nns8twElVQHBg7MnaDSo_8EuZPSIqGdU0,7125
10
+ py_dpm/api/dpm_xl/ast_generator.py,sha256=GHu_F3YKaMpqvu16ABIvj8BwlmWXehvk0eUJVQkCphE,55204
11
+ py_dpm/api/dpm_xl/complete_ast.py,sha256=VkmcBatrydu97Inwp3pHjz93F38q2JRo-4Lohdu30RY,7684
12
12
  py_dpm/api/dpm_xl/operation_scopes.py,sha256=7AyOFAn9h012JPF9H5EtZ3sPzv6DOxkoinpj5ArzVOc,48492
13
13
  py_dpm/api/dpm_xl/semantic.py,sha256=Buo_t-sEv65r6RmYDy1xkCWGlU2pB2WQsDM-X-FX4cc,13629
14
14
  py_dpm/api/dpm_xl/syntax.py,sha256=Ke_kKd9ModoJ6siL3GPT9j9QClmopryCRcdDAT3M5-E,5954
@@ -17,7 +17,7 @@ py_dpm/cli/main.py,sha256=v8ZgIjg4Zqf6UWNv2bydYlZV6KfDLnPACyqplNIJyNE,22447
17
17
  py_dpm/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  py_dpm/dpm/__init__.py,sha256=moagUo5Gxf24-Tl9FL_3n2wmVoD_oXtpC-YIGktH_rc,212
19
19
  py_dpm/dpm/migration.py,sha256=ivO_ObvKzVomTns6qfo-o5FuciWxkXbMd_gJ4_tu7Xc,14110
20
- py_dpm/dpm/models.py,sha256=d70oZ_3wXsKBRGM2TJRlsuDrHWSVkzetLAZeMIoImwc,124821
20
+ py_dpm/dpm/models.py,sha256=nv6KMk0nYlXGWFrJEcqyzKHCaIDnMF5IQh0l5Ww8PTI,135212
21
21
  py_dpm/dpm/utils.py,sha256=JNdAeOXjzQtye94jLPRHHGUMcvkGtTsjA5HFl92rWig,12783
22
22
  py_dpm/dpm/queries/base.py,sha256=EddMeJMwtp63DyyIFO7_XxGvdlCtJQWWpeOVImlKp4I,3648
23
23
  py_dpm/dpm/queries/basic_objects.py,sha256=JOXC235lMDfVENrFAhZAl7_nqePJ4RrwJhFF0WDyk0M,955
@@ -68,17 +68,17 @@ py_dpm/dpm_xl/utils/__init__.py,sha256=4-jXa7AdHjx2DpikAzjZVKqBktdrHgSAx6pibb4sM
68
68
  py_dpm/dpm_xl/utils/data_handlers.py,sha256=a0E-IaP_-CDKLcj-Gt2ggAziKIOUiwnT2D9IkWCS68o,4402
69
69
  py_dpm/dpm_xl/utils/operands_mapping.py,sha256=LG0hPlUuTM2X2uWOtiD6HkmNeDEJkWJ8gV-Fxej_8QM,2241
70
70
  py_dpm/dpm_xl/utils/operator_mapping.py,sha256=BFgbVbSCSuutFNHJ4gtgm5VuG38pcl8Kmfi-sefg6JU,1913
71
- py_dpm/dpm_xl/utils/scopes_calculator.py,sha256=nCx2mz_qtw61BESp38ORQYlF2uRT8SyUKawSX9OQljM,17832
71
+ py_dpm/dpm_xl/utils/scopes_calculator.py,sha256=do_emsUqD1TbrjguKlOOqFleaVhxzqm-NnlgdrdIb6I,20906
72
72
  py_dpm/dpm_xl/utils/serialization.py,sha256=LPcmudFfzHeEjIIr57kr5BvGPZbxshOAAeUYOrLl7XM,32482
73
73
  py_dpm/dpm_xl/utils/tokens.py,sha256=VRIrPDi5ttwgH-on5Qt4-l4ho4bLA755-nfTalponcA,3496
74
74
  py_dpm/exceptions/__init__.py,sha256=yDERfUxYW7NUUEiTQChGpuJx6abr7IDe2XUpwVFPtvM,416
75
75
  py_dpm/exceptions/exceptions.py,sha256=6S3p-_i5O1oStvSMixt_JQG0xwTeSfBcdzrwL8yBy6Q,2413
76
76
  py_dpm/exceptions/messages.py,sha256=UwY6QIK8c-POcDCc9HYbZFGArCIYAanUGNh2LNKPx3U,7534
77
77
  py_dpm/instance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
- py_dpm/instance/instance.py,sha256=ROUXOcd2lBV5fKdGHBAli6lnmL-YP-n02bUhtwARZhE,9119
79
- pydpm_xl-0.2.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
80
- pydpm_xl-0.2.3.dist-info/METADATA,sha256=cYguvXm7dSViN9mNqxZf9kKo9vlg-eP2lpctZu6oJDI,7974
81
- pydpm_xl-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
82
- pydpm_xl-0.2.3.dist-info/entry_points.txt,sha256=6DDmBfw-AjtgvMHgq_I730i_LAAs_7-N3C95HD_bRr4,47
83
- pydpm_xl-0.2.3.dist-info/top_level.txt,sha256=495PvWZRoKl2NvbQU25W7dqWIBHqY-mFMPt83uxPpcM,7
84
- pydpm_xl-0.2.3.dist-info/RECORD,,
78
+ py_dpm/instance/instance.py,sha256=OPSEPgSYAxhgqhKuxbMpMPTfBnaFNzURTrUUT4kvGKc,10820
79
+ pydpm_xl-0.2.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
80
+ pydpm_xl-0.2.5.dist-info/METADATA,sha256=4zUBTstmgwrkyneDaFLnbB89XiG7fY67Prjj-_UOw3E,9302
81
+ pydpm_xl-0.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
82
+ pydpm_xl-0.2.5.dist-info/entry_points.txt,sha256=6DDmBfw-AjtgvMHgq_I730i_LAAs_7-N3C95HD_bRr4,47
83
+ pydpm_xl-0.2.5.dist-info/top_level.txt,sha256=495PvWZRoKl2NvbQU25W7dqWIBHqY-mFMPt83uxPpcM,7
84
+ pydpm_xl-0.2.5.dist-info/RECORD,,