pydpm_xl 0.2.5__tar.gz → 0.2.5rc1__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.5 → pydpm_xl-0.2.5rc1}/PKG-INFO +1 -1
  2. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/__init__.py +1 -1
  3. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm_xl/ast_generator.py +17 -122
  4. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm/models.py +7 -257
  5. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/utils/scopes_calculator.py +30 -86
  6. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/pydpm_xl.egg-info/PKG-INFO +1 -1
  7. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/pyproject.toml +2 -2
  8. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/LICENSE +0 -0
  9. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/README.md +0 -0
  10. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/__init__.py +0 -0
  11. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm/__init__.py +0 -0
  12. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm/data_dictionary.py +0 -0
  13. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm/explorer.py +0 -0
  14. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm/hierarchical_queries.py +0 -0
  15. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm/instance.py +0 -0
  16. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm/migration.py +0 -0
  17. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm_xl/__init__.py +0 -0
  18. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm_xl/complete_ast.py +0 -0
  19. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm_xl/operation_scopes.py +0 -0
  20. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm_xl/semantic.py +0 -0
  21. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/api/dpm_xl/syntax.py +0 -0
  22. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/cli/__init__.py +0 -0
  23. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/cli/commands/__init__.py +0 -0
  24. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/cli/main.py +0 -0
  25. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm/__init__.py +0 -0
  26. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm/migration.py +0 -0
  27. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm/queries/base.py +0 -0
  28. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm/queries/basic_objects.py +0 -0
  29. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm/queries/explorer_queries.py +0 -0
  30. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm/queries/filters.py +0 -0
  31. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm/queries/glossary.py +0 -0
  32. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm/queries/hierarchical_queries.py +0 -0
  33. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm/queries/tables.py +0 -0
  34. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm/utils.py +0 -0
  35. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/__init__.py +0 -0
  36. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/ast/__init__.py +0 -0
  37. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/ast/constructor.py +0 -0
  38. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/ast/ml_generation.py +0 -0
  39. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/ast/module_analyzer.py +0 -0
  40. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/ast/module_dependencies.py +0 -0
  41. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/ast/nodes.py +0 -0
  42. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/ast/operands.py +0 -0
  43. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/ast/template.py +0 -0
  44. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/ast/visitor.py +0 -0
  45. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/ast/where_clause.py +0 -0
  46. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/grammar/__init__.py +0 -0
  47. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/grammar/generated/__init__.py +0 -0
  48. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.interp +0 -0
  49. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.py +0 -0
  50. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/grammar/generated/dpm_xlLexer.tokens +0 -0
  51. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.interp +0 -0
  52. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.py +0 -0
  53. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/grammar/generated/dpm_xlParser.tokens +0 -0
  54. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/grammar/generated/dpm_xlParserListener.py +0 -0
  55. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/grammar/generated/dpm_xlParserVisitor.py +0 -0
  56. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/grammar/generated/listeners.py +0 -0
  57. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/operators/__init__.py +0 -0
  58. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/operators/aggregate.py +0 -0
  59. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/operators/arithmetic.py +0 -0
  60. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/operators/base.py +0 -0
  61. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/operators/boolean.py +0 -0
  62. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/operators/clause.py +0 -0
  63. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/operators/comparison.py +0 -0
  64. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/operators/conditional.py +0 -0
  65. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/operators/string.py +0 -0
  66. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/operators/time.py +0 -0
  67. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/semantic_analyzer.py +0 -0
  68. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/symbols.py +0 -0
  69. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/types/__init__.py +0 -0
  70. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/types/promotion.py +0 -0
  71. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/types/scalar.py +0 -0
  72. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/types/time.py +0 -0
  73. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/utils/__init__.py +0 -0
  74. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/utils/data_handlers.py +0 -0
  75. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/utils/operands_mapping.py +0 -0
  76. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/utils/operator_mapping.py +0 -0
  77. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/utils/serialization.py +0 -0
  78. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/dpm_xl/utils/tokens.py +0 -0
  79. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/exceptions/__init__.py +0 -0
  80. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/exceptions/exceptions.py +0 -0
  81. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/exceptions/messages.py +0 -0
  82. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/instance/__init__.py +0 -0
  83. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/py_dpm/instance/instance.py +0 -0
  84. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/pydpm_xl.egg-info/SOURCES.txt +0 -0
  85. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/pydpm_xl.egg-info/dependency_links.txt +0 -0
  86. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/pydpm_xl.egg-info/entry_points.txt +0 -0
  87. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/pydpm_xl.egg-info/requires.txt +0 -0
  88. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/pydpm_xl.egg-info/top_level.txt +0 -0
  89. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/setup.cfg +0 -0
  90. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/tests/test_cli_semantic.py +0 -0
  91. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/tests/test_data_dictionary_releases.py +0 -0
  92. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/tests/test_db_connection_handling.py +0 -0
  93. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/tests/test_get_table_details.py +0 -0
  94. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/tests/test_get_tables_date_filter.py +0 -0
  95. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/tests/test_get_tables_release_code.py +0 -0
  96. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/tests/test_hierarchical_query.py +0 -0
  97. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/tests/test_query_refactor.py +0 -0
  98. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/tests/test_release_filters_semantic.py +0 -0
  99. {pydpm_xl-0.2.5 → pydpm_xl-0.2.5rc1}/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.5
3
+ Version: 0.2.5rc1
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
@@ -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.5"
44
+ __version__ = "0.2.5rc1"
45
45
  __author__ = "MeaningfulData S.L."
46
46
  __email__ = "info@meaningfuldata.eu"
47
47
  __license__ = "GPL-3.0-or-later"
@@ -811,7 +811,7 @@ class ASTGeneratorAPI:
811
811
  preconditions = {}
812
812
  precondition_variables = {}
813
813
 
814
- if precondition:
814
+ if precondition or (context and "table" in context):
815
815
  preconditions, precondition_variables = self._build_preconditions(
816
816
  precondition=precondition,
817
817
  context=context,
@@ -974,6 +974,8 @@ class ASTGeneratorAPI:
974
974
  match = re.match(r"\{v_([^}]+)\}", precondition)
975
975
  if match:
976
976
  table_code = match.group(1)
977
+ elif context and "table" in context:
978
+ table_code = context["table"]
977
979
 
978
980
  if table_code:
979
981
  # Query database for actual variable ID and version
@@ -1037,81 +1039,6 @@ class ASTGeneratorAPI:
1037
1039
  extract_from_node(ast_dict)
1038
1040
  return all_variables, variables_by_table
1039
1041
 
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
1042
  def _detect_cross_module_dependencies(
1116
1043
  self,
1117
1044
  expression: str,
@@ -1148,7 +1075,7 @@ class ASTGeneratorAPI:
1148
1075
  )
1149
1076
 
1150
1077
  try:
1151
- # Get tables with module info (includes module_version)
1078
+ # Get tables with module info
1152
1079
  tables_with_modules = scopes_api.get_tables_with_metadata_from_expression(
1153
1080
  expression=expression,
1154
1081
  release_id=release_id
@@ -1164,35 +1091,10 @@ class ASTGeneratorAPI:
1164
1091
  if scope_result.has_error or not scope_result.is_cross_module:
1165
1092
  return {}, []
1166
1093
 
1167
- # Extract time shifts for each table from expression
1168
- time_shifts_by_table = self._extract_time_shifts_by_table(expression)
1169
-
1170
1094
  # Determine primary module from first table if not provided
1171
1095
  if primary_module_vid is None and tables_with_modules:
1172
1096
  primary_module_vid = tables_with_modules[0].get("module_vid")
1173
1097
 
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
1098
  # Group external tables by module
1197
1099
  external_modules = {}
1198
1100
  for table_info in tables_with_modules:
@@ -1204,38 +1106,36 @@ class ASTGeneratorAPI:
1204
1106
  if not module_code:
1205
1107
  continue
1206
1108
 
1207
- # Get module URI
1109
+ # Get module URI using existing ExplorerQuery
1208
1110
  try:
1209
1111
  module_uri = ExplorerQuery.get_module_url(
1210
1112
  scopes_api.session,
1211
1113
  module_code=module_code,
1212
1114
  release_id=release_id,
1213
1115
  )
1116
+ # Remove .json suffix if present (for consistency with expected format)
1214
1117
  if module_uri.endswith(".json"):
1215
1118
  module_uri = module_uri[:-5]
1216
1119
  except Exception:
1217
1120
  continue
1218
1121
 
1219
- table_code = table_info.get("code")
1220
- ref_period = get_ref_period(table_code)
1221
-
1222
1122
  if module_uri not in external_modules:
1223
1123
  external_modules[module_uri] = {
1224
1124
  "module_vid": module_vid,
1225
- "module_version": table_info.get("module_version"), # Already in table_info
1226
- "ref_period": ref_period,
1227
1125
  "tables": {},
1228
1126
  "variables": {},
1229
1127
  "from_date": None,
1230
1128
  "to_date": None
1231
1129
  }
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
1130
 
1236
- # Add table and variables
1131
+ # Add table - get variables from variables_by_table
1132
+ table_code = table_info.get("code")
1237
1133
  if table_code:
1238
- table_variables = get_table_variables(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}", {})
1239
1139
  external_modules[module_uri]["tables"][table_code] = {
1240
1140
  "variables": table_variables,
1241
1141
  "open_keys": {}
@@ -1269,16 +1169,11 @@ class ASTGeneratorAPI:
1269
1169
  # cross_instance_dependencies entry (one per external module)
1270
1170
  from_date = data["from_date"]
1271
1171
  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
1172
  cross_instance_dependencies.append({
1281
- "modules": [module_entry],
1173
+ "modules": [{
1174
+ "URI": uri,
1175
+ "ref_period": "T"
1176
+ }],
1282
1177
  "affected_operations": [operation_code],
1283
1178
  "from_reference_date": str(from_date) if from_date else "",
1284
1179
  "to_reference_date": str(to_date) if to_date else ""
@@ -720,20 +720,16 @@ 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
- ModuleCode, VersionNumber, FromReferenceDate, ToReferenceDate,
725
- StartReleaseID, EndReleaseID
723
+ pandas DataFrame with columns: ModuleVID, TableVID (as VARIABLE_VID),
724
+ FromReferenceDate, ToReferenceDate, EndReleaseID
726
725
  """
727
726
  if not tables_vids:
728
727
  return pd.DataFrame(
729
728
  columns=[
730
729
  "ModuleVID",
731
730
  "variable_vid",
732
- "ModuleCode",
733
- "VersionNumber",
734
731
  "FromReferenceDate",
735
732
  "ToReferenceDate",
736
- "StartReleaseID",
737
733
  "EndReleaseID",
738
734
  ]
739
735
  )
@@ -742,11 +738,8 @@ class ModuleVersion(Base):
742
738
  session.query(
743
739
  cls.modulevid.label("ModuleVID"),
744
740
  ModuleVersionComposition.tablevid.label("variable_vid"),
745
- cls.code.label("ModuleCode"),
746
- cls.versionnumber.label("VersionNumber"),
747
741
  cls.fromreferencedate.label("FromReferenceDate"),
748
742
  cls.toreferencedate.label("ToReferenceDate"),
749
- cls.startreleaseid.label("StartReleaseID"),
750
743
  cls.endreleaseid.label("EndReleaseID"),
751
744
  )
752
745
  .join(
@@ -766,20 +759,16 @@ class ModuleVersion(Base):
766
759
  )
767
760
 
768
761
  results = query.all()
769
- df = pd.DataFrame(
762
+ return pd.DataFrame(
770
763
  results,
771
764
  columns=[
772
765
  "ModuleVID",
773
766
  "variable_vid",
774
- "ModuleCode",
775
- "VersionNumber",
776
767
  "FromReferenceDate",
777
768
  "ToReferenceDate",
778
- "StartReleaseID",
779
769
  "EndReleaseID",
780
770
  ],
781
771
  )
782
- return cls._apply_fallback_for_equal_dates(session, df)
783
772
 
784
773
  @classmethod
785
774
  def get_from_table_codes(cls, session, table_codes, release_id=None):
@@ -801,8 +790,6 @@ class ModuleVersion(Base):
801
790
  columns=[
802
791
  "ModuleVID",
803
792
  "variable_vid",
804
- "ModuleCode",
805
- "VersionNumber",
806
793
  "FromReferenceDate",
807
794
  "ToReferenceDate",
808
795
  "StartReleaseID",
@@ -817,8 +804,6 @@ class ModuleVersion(Base):
817
804
  session.query(
818
805
  cls.modulevid.label("ModuleVID"),
819
806
  ModuleVersionComposition.tablevid.label("variable_vid"),
820
- cls.code.label("ModuleCode"),
821
- cls.versionnumber.label("VersionNumber"),
822
807
  cls.fromreferencedate.label("FromReferenceDate"),
823
808
  cls.toreferencedate.label("ToReferenceDate"),
824
809
  cls.startreleaseid.label("StartReleaseID"),
@@ -846,13 +831,11 @@ class ModuleVersion(Base):
846
831
  )
847
832
 
848
833
  results = query.all()
849
- df = pd.DataFrame(
834
+ return pd.DataFrame(
850
835
  results,
851
836
  columns=[
852
837
  "ModuleVID",
853
838
  "variable_vid",
854
- "ModuleCode",
855
- "VersionNumber",
856
839
  "FromReferenceDate",
857
840
  "ToReferenceDate",
858
841
  "StartReleaseID",
@@ -860,7 +843,6 @@ class ModuleVersion(Base):
860
843
  "TableCode",
861
844
  ],
862
845
  )
863
- return cls._apply_fallback_for_equal_dates(session, df)
864
846
 
865
847
  @classmethod
866
848
  def get_precondition_module_versions(
@@ -875,21 +857,16 @@ class ModuleVersion(Base):
875
857
  release_id: Optional release ID to filter modules by release range
876
858
 
877
859
  Returns:
878
- pandas DataFrame with columns: ModuleVID, variable_vid (VariableVID),
879
- ModuleCode, VersionNumber, FromReferenceDate, ToReferenceDate,
880
- StartReleaseID, EndReleaseID, Code
860
+ pandas DataFrame with columns: ModuleVID, VariableVID (as VARIABLE_VID),
861
+ FromReferenceDate, ToReferenceDate, Code
881
862
  """
882
863
  if not precondition_items:
883
864
  return pd.DataFrame(
884
865
  columns=[
885
866
  "ModuleVID",
886
867
  "variable_vid",
887
- "ModuleCode",
888
- "VersionNumber",
889
868
  "FromReferenceDate",
890
869
  "ToReferenceDate",
891
- "StartReleaseID",
892
- "EndReleaseID",
893
870
  "Code",
894
871
  ]
895
872
  )
@@ -898,12 +875,8 @@ class ModuleVersion(Base):
898
875
  session.query(
899
876
  cls.modulevid.label("ModuleVID"),
900
877
  VariableVersion.variablevid.label("variable_vid"),
901
- cls.code.label("ModuleCode"),
902
- cls.versionnumber.label("VersionNumber"),
903
878
  cls.fromreferencedate.label("FromReferenceDate"),
904
879
  cls.toreferencedate.label("ToReferenceDate"),
905
- cls.startreleaseid.label("StartReleaseID"),
906
- cls.endreleaseid.label("EndReleaseID"),
907
880
  VariableVersion.code.label("Code"),
908
881
  )
909
882
  .join(ModuleParameters, cls.modulevid == ModuleParameters.modulevid)
@@ -926,21 +899,16 @@ class ModuleVersion(Base):
926
899
  )
927
900
 
928
901
  results = query.all()
929
- df = pd.DataFrame(
902
+ return pd.DataFrame(
930
903
  results,
931
904
  columns=[
932
905
  "ModuleVID",
933
906
  "variable_vid",
934
- "ModuleCode",
935
- "VersionNumber",
936
907
  "FromReferenceDate",
937
908
  "ToReferenceDate",
938
- "StartReleaseID",
939
- "EndReleaseID",
940
909
  "Code",
941
910
  ],
942
911
  )
943
- return cls._apply_fallback_for_equal_dates(session, df)
944
912
 
945
913
  @classmethod
946
914
  def get_module_version_by_vid(cls, session, vid):
@@ -978,170 +946,6 @@ class ModuleVersion(Base):
978
946
  ],
979
947
  )
980
948
 
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
-
1145
949
  @classmethod
1146
950
  def get_last_release(cls, session):
1147
951
  """
@@ -1315,60 +1119,6 @@ class OperationScope(Base):
1315
1119
  "OperationScopeComposition", back_populates="operation_scope"
1316
1120
  )
1317
1121
 
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
-
1372
1122
 
1373
1123
  class OperationScopeComposition(Base):
1374
1124
  __tablename__ = "OperationScopeComposition"
@@ -71,19 +71,9 @@ 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]
77
74
  operation_scope = self.create_operation_scope(from_date)
78
75
  self.create_operation_scope_composition(
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
- },
76
+ operation_scope=operation_scope, module_vid=module_vid
87
77
  )
88
78
  else:
89
79
  intra_modules = []
@@ -93,10 +83,11 @@ class OperationScopeService:
93
83
  if table_codes:
94
84
  unique_operands_number = len(table_codes) + len(precondition_items)
95
85
 
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]
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)
100
91
 
101
92
  for module_vid, group_df in modules_info_dataframe.groupby(MODULE_VID):
102
93
  table_codes_in_module = (
@@ -114,55 +105,31 @@ class OperationScopeService:
114
105
  end_release = group_df["EndReleaseID"].values[0]
115
106
 
116
107
  # Determine if this is a "new" module starting in this release
108
+ # or an "old" module ending in this release
117
109
  is_starting = start_release == release_id
110
+ is_ending = end_release == release_id or end_release == float(
111
+ release_id
112
+ )
118
113
 
119
114
  if len(table_codes_in_module) == unique_operands_number:
120
115
  # Intra-module: include ALL modules active in the release
116
+ # (don't filter by lifecycle - that's only for cross-module)
121
117
  intra_modules.append(module_vid)
122
118
  else:
123
- # Track modules by table code and lifecycle
119
+ # For cross-module, group by table code AND lifecycle stage
120
+ target_dict = (
121
+ starting_modules if is_starting else ending_modules
122
+ )
124
123
  for table_code in table_codes_in_module:
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
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
166
133
  else:
167
134
  # Original logic for table VIDs
168
135
  unique_operands_number = len(tables_vids) + len(precondition_items)
@@ -301,21 +268,12 @@ class OperationScopeService:
301
268
  :param modules_vids: list with module version ids
302
269
  """
303
270
  for module_vid in modules_vids:
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"]
271
+ from_date = modules_info[modules_info["ModuleVID"] == module_vid][
272
+ "FromReferenceDate"
273
+ ].values[0]
309
274
  operation_scope = self.create_operation_scope(from_date)
310
275
  self.create_operation_scope_composition(
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
- },
276
+ operation_scope=operation_scope, module_vid=module_vid
319
277
  )
320
278
 
321
279
  def process_cross_module(self, cross_modules, modules_dataframe):
@@ -355,18 +313,8 @@ class OperationScopeService:
355
313
  operation_scope = self.create_operation_scope(from_submission_date)
356
314
  combination = set(combination)
357
315
  for module in combination:
358
- module_row = modules_dataframe[
359
- modules_dataframe[MODULE_VID] == module
360
- ].iloc[0]
361
316
  self.create_operation_scope_composition(
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
- },
317
+ operation_scope=operation_scope, module_vid=module
370
318
  )
371
319
 
372
320
  def create_operation_scope(self, submission_date):
@@ -392,21 +340,17 @@ class OperationScopeService:
392
340
  self.session.add(operation_scope)
393
341
  return operation_scope
394
342
 
395
- def create_operation_scope_composition(self, operation_scope, module_vid, module_info=None):
343
+ def create_operation_scope_composition(self, operation_scope, module_vid):
396
344
  """
397
345
  Method to populate OperationScopeComposition table
398
346
  :param operation_scope: Operation scope data
399
347
  :param module_vid: Module version id
400
- :param module_info: Optional dict with module info (code, from_reference_date, to_reference_date)
401
348
  """
402
349
  operation_scope_composition = OperationScopeComposition(
403
350
  operation_scope=operation_scope,
404
351
  modulevid=module_vid,
405
352
  rowguid=str(uuid.uuid4()),
406
353
  )
407
- # Store module info as transient attribute for to_dict() access
408
- if module_info:
409
- operation_scope_composition._module_info = module_info
410
354
  self.session.add(operation_scope_composition)
411
355
 
412
356
  def get_scopes_with_status(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydpm_xl
3
- Version: 0.2.5
3
+ Version: 0.2.5rc1
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "pydpm_xl"
3
- version = "0.2.5"
3
+ version = "0.2.5rc1"
4
4
  description = "Python library for DPM-XL data processing and analysis"
5
5
  authors = [
6
6
  {name = "MeaningfulData S.L.", email = "info@meaningfuldata.eu"}
@@ -52,7 +52,7 @@ exclude = []
52
52
 
53
53
  [tool.poetry]
54
54
  name = "pydpm_xl"
55
- version = "0.2.5"
55
+ version = "0.2.5rc1"
56
56
  description = "Python library for DPM-XL data processing and analysis"
57
57
  authors = ["MeaningfulData S.L. <info@meaningfuldata.eu>"]
58
58
  readme = "README.md"
File without changes
File without changes
File without changes
File without changes