pydpm_xl 0.2.5rc1__py3-none-any.whl → 0.2.5rc2__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.5rc1"
44
+ __version__ = "0.2.5rc2"
45
45
  __author__ = "MeaningfulData S.L."
46
46
  __email__ = "info@meaningfuldata.eu"
47
47
  __license__ = "GPL-3.0-or-later"
@@ -1039,6 +1039,81 @@ class ASTGeneratorAPI:
1039
1039
  extract_from_node(ast_dict)
1040
1040
  return all_variables, variables_by_table
1041
1041
 
1042
+ def _extract_time_shifts_by_table(self, expression: str) -> Dict[str, str]:
1043
+ """
1044
+ Extract time shift information for each table in the expression.
1045
+
1046
+ Uses the AST to properly parse the expression and find TimeShiftOp nodes
1047
+ to determine the ref_period for each table reference.
1048
+
1049
+ Args:
1050
+ expression: DPM-XL expression
1051
+
1052
+ Returns:
1053
+ Dict mapping table codes to ref_period values (e.g., {"C_01.00": "T-1Q"})
1054
+ Tables without time shifts default to "T".
1055
+ """
1056
+ from py_dpm.dpm_xl.ast.template import ASTTemplate
1057
+
1058
+ time_shifts = {}
1059
+ current_period = ["t"] # Use list to allow mutation in nested function
1060
+
1061
+ class TimeShiftExtractor(ASTTemplate):
1062
+ """Lightweight AST visitor that extracts time shifts for each table."""
1063
+
1064
+ def visit_TimeShiftOp(self, node):
1065
+ # Save current time period and compute new one
1066
+ previous_period = current_period[0]
1067
+
1068
+ period_indicator = node.period_indicator
1069
+ shift_number = node.shift_number
1070
+
1071
+ # Compute time period (same logic as ModuleDependencies)
1072
+ if "-" in str(shift_number):
1073
+ current_period[0] = f"t+{period_indicator}{shift_number}"
1074
+ else:
1075
+ current_period[0] = f"t-{period_indicator}{shift_number}"
1076
+
1077
+ # Visit operand (which contains the VarID)
1078
+ self.visit(node.operand)
1079
+
1080
+ # Restore previous time period
1081
+ current_period[0] = previous_period
1082
+
1083
+ def visit_VarID(self, node):
1084
+ if node.table and current_period[0] != "t":
1085
+ time_shifts[node.table] = current_period[0]
1086
+
1087
+ def convert_to_ref_period(internal_period: str) -> str:
1088
+ """Convert internal time period format to ref_period format.
1089
+
1090
+ Internal format: "t+Q-1" or "t-Q1"
1091
+ Output format: "T-1Q" for one quarter back
1092
+ """
1093
+ if internal_period.startswith("t+"):
1094
+ # e.g., "t+Q-1" -> "T-1Q"
1095
+ indicator = internal_period[2]
1096
+ number = internal_period[3:]
1097
+ if number.startswith("-"):
1098
+ return f"T{number}{indicator}"
1099
+ return f"T+{number}{indicator}"
1100
+ elif internal_period.startswith("t-"):
1101
+ # e.g., "t-Q1" -> "T-1Q"
1102
+ indicator = internal_period[2]
1103
+ number = internal_period[3:]
1104
+ return f"T-{number}{indicator}"
1105
+ return "T"
1106
+
1107
+ try:
1108
+ ast = self.syntax_api.parse_expression(expression)
1109
+ extractor = TimeShiftExtractor()
1110
+ extractor.visit(ast)
1111
+
1112
+ return {table: convert_to_ref_period(period) for table, period in time_shifts.items()}
1113
+
1114
+ except Exception:
1115
+ return {}
1116
+
1042
1117
  def _detect_cross_module_dependencies(
1043
1118
  self,
1044
1119
  expression: str,
@@ -1075,7 +1150,7 @@ class ASTGeneratorAPI:
1075
1150
  )
1076
1151
 
1077
1152
  try:
1078
- # Get tables with module info
1153
+ # Get tables with module info (includes module_version)
1079
1154
  tables_with_modules = scopes_api.get_tables_with_metadata_from_expression(
1080
1155
  expression=expression,
1081
1156
  release_id=release_id
@@ -1091,10 +1166,35 @@ class ASTGeneratorAPI:
1091
1166
  if scope_result.has_error or not scope_result.is_cross_module:
1092
1167
  return {}, []
1093
1168
 
1169
+ # Extract time shifts for each table from expression
1170
+ time_shifts_by_table = self._extract_time_shifts_by_table(expression)
1171
+
1094
1172
  # Determine primary module from first table if not provided
1095
1173
  if primary_module_vid is None and tables_with_modules:
1096
1174
  primary_module_vid = tables_with_modules[0].get("module_vid")
1097
1175
 
1176
+ # Helper to normalize table code (remove 't' prefix if present)
1177
+ def normalize_table_code(code: str) -> str:
1178
+ return code[1:] if code and code.startswith('t') else code
1179
+
1180
+ # Helper to lookup ref_period for a table
1181
+ def get_ref_period(table_code: str) -> str:
1182
+ if not table_code:
1183
+ return "T"
1184
+ ref = time_shifts_by_table.get(table_code)
1185
+ if not ref:
1186
+ ref = time_shifts_by_table.get(normalize_table_code(table_code))
1187
+ return ref or "T"
1188
+
1189
+ # Helper to lookup variables for a table
1190
+ def get_table_variables(table_code: str) -> dict:
1191
+ if not table_code:
1192
+ return {}
1193
+ variables = variables_by_table.get(table_code)
1194
+ if not variables:
1195
+ variables = variables_by_table.get(f"t{table_code}", {})
1196
+ return variables or {}
1197
+
1098
1198
  # Group external tables by module
1099
1199
  external_modules = {}
1100
1200
  for table_info in tables_with_modules:
@@ -1106,36 +1206,38 @@ class ASTGeneratorAPI:
1106
1206
  if not module_code:
1107
1207
  continue
1108
1208
 
1109
- # Get module URI using existing ExplorerQuery
1209
+ # Get module URI
1110
1210
  try:
1111
1211
  module_uri = ExplorerQuery.get_module_url(
1112
1212
  scopes_api.session,
1113
1213
  module_code=module_code,
1114
1214
  release_id=release_id,
1115
1215
  )
1116
- # Remove .json suffix if present (for consistency with expected format)
1117
1216
  if module_uri.endswith(".json"):
1118
1217
  module_uri = module_uri[:-5]
1119
1218
  except Exception:
1120
1219
  continue
1121
1220
 
1221
+ table_code = table_info.get("code")
1222
+ ref_period = get_ref_period(table_code)
1223
+
1122
1224
  if module_uri not in external_modules:
1123
1225
  external_modules[module_uri] = {
1124
1226
  "module_vid": module_vid,
1227
+ "module_version": table_info.get("module_version"), # Already in table_info
1228
+ "ref_period": ref_period,
1125
1229
  "tables": {},
1126
1230
  "variables": {},
1127
1231
  "from_date": None,
1128
1232
  "to_date": None
1129
1233
  }
1234
+ elif ref_period != "T":
1235
+ # Keep most specific ref_period (non-T takes precedence)
1236
+ external_modules[module_uri]["ref_period"] = ref_period
1130
1237
 
1131
- # Add table - get variables from variables_by_table
1132
- table_code = table_info.get("code")
1238
+ # Add table and variables
1133
1239
  if table_code:
1134
- # Look up variables in variables_by_table (handles t prefix)
1135
- table_variables = variables_by_table.get(table_code, {})
1136
- if not table_variables:
1137
- # Try with t prefix
1138
- table_variables = variables_by_table.get(f"t{table_code}", {})
1240
+ table_variables = get_table_variables(table_code)
1139
1241
  external_modules[module_uri]["tables"][table_code] = {
1140
1242
  "variables": table_variables,
1141
1243
  "open_keys": {}
@@ -1169,11 +1271,16 @@ class ASTGeneratorAPI:
1169
1271
  # cross_instance_dependencies entry (one per external module)
1170
1272
  from_date = data["from_date"]
1171
1273
  to_date = data["to_date"]
1274
+ module_entry = {
1275
+ "URI": uri,
1276
+ "ref_period": data["ref_period"]
1277
+ }
1278
+ # Add module_version if available
1279
+ if data["module_version"]:
1280
+ module_entry["module_version"] = data["module_version"]
1281
+
1172
1282
  cross_instance_dependencies.append({
1173
- "modules": [{
1174
- "URI": uri,
1175
- "ref_period": "T"
1176
- }],
1283
+ "modules": [module_entry],
1177
1284
  "affected_operations": [operation_code],
1178
1285
  "from_reference_date": str(from_date) if from_date else "",
1179
1286
  "to_reference_date": str(to_date) if to_date else ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydpm_xl
3
- Version: 0.2.5rc1
3
+ Version: 0.2.5rc2
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
@@ -1,4 +1,4 @@
1
- py_dpm/__init__.py,sha256=GmaGEIVu3IEt3i3UkrQJSpoLu_ENSpiSt_4rBceOjpc,1861
1
+ py_dpm/__init__.py,sha256=u6lHTsVyMlEyPdrczeNUNlN8D7Dge-Cr0MPlZs8iIqo,1861
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
@@ -7,7 +7,7 @@ py_dpm/api/dpm/hierarchical_queries.py,sha256=X4AbpsWy3iItOTVIdVbtaTmRgOHPf0Y64I
7
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=_ir-LP3hgT8diUFt9sEcXsh3cQhA_YiSSFe4a03zjuA,51050
10
+ py_dpm/api/dpm_xl/ast_generator.py,sha256=w7aeRDGJ-ggJoRXnZ-Tn1rEeb1zoSF21co9pSV_KU6A,55327
11
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
@@ -76,9 +76,9 @@ py_dpm/exceptions/exceptions.py,sha256=6S3p-_i5O1oStvSMixt_JQG0xwTeSfBcdzrwL8yBy
76
76
  py_dpm/exceptions/messages.py,sha256=UwY6QIK8c-POcDCc9HYbZFGArCIYAanUGNh2LNKPx3U,7534
77
77
  py_dpm/instance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
78
  py_dpm/instance/instance.py,sha256=OPSEPgSYAxhgqhKuxbMpMPTfBnaFNzURTrUUT4kvGKc,10820
79
- pydpm_xl-0.2.5rc1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
80
- pydpm_xl-0.2.5rc1.dist-info/METADATA,sha256=X7pPwj8tY9-p6ApsGXKW533zyekcZHHqRf9w1Ld9Ar4,9305
81
- pydpm_xl-0.2.5rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
82
- pydpm_xl-0.2.5rc1.dist-info/entry_points.txt,sha256=6DDmBfw-AjtgvMHgq_I730i_LAAs_7-N3C95HD_bRr4,47
83
- pydpm_xl-0.2.5rc1.dist-info/top_level.txt,sha256=495PvWZRoKl2NvbQU25W7dqWIBHqY-mFMPt83uxPpcM,7
84
- pydpm_xl-0.2.5rc1.dist-info/RECORD,,
79
+ pydpm_xl-0.2.5rc2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
80
+ pydpm_xl-0.2.5rc2.dist-info/METADATA,sha256=wtWYaml5W9vzWfDQTiUI3x_EByIduVBZV6bo6fLu31A,9305
81
+ pydpm_xl-0.2.5rc2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
82
+ pydpm_xl-0.2.5rc2.dist-info/entry_points.txt,sha256=6DDmBfw-AjtgvMHgq_I730i_LAAs_7-N3C95HD_bRr4,47
83
+ pydpm_xl-0.2.5rc2.dist-info/top_level.txt,sha256=495PvWZRoKl2NvbQU25W7dqWIBHqY-mFMPt83uxPpcM,7
84
+ pydpm_xl-0.2.5rc2.dist-info/RECORD,,