pydpm_xl 0.2.4__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 +1 -1
- py_dpm/api/dpm_xl/ast_generator.py +296 -13
- py_dpm/api/dpm_xl/complete_ast.py +10 -0
- py_dpm/dpm/models.py +257 -7
- py_dpm/dpm_xl/utils/scopes_calculator.py +86 -30
- {pydpm_xl-0.2.4.dist-info → pydpm_xl-0.2.5.dist-info}/METADATA +54 -1
- {pydpm_xl-0.2.4.dist-info → pydpm_xl-0.2.5.dist-info}/RECORD +11 -11
- {pydpm_xl-0.2.4.dist-info → pydpm_xl-0.2.5.dist-info}/WHEEL +0 -0
- {pydpm_xl-0.2.4.dist-info → pydpm_xl-0.2.5.dist-info}/entry_points.txt +0 -0
- {pydpm_xl-0.2.4.dist-info → pydpm_xl-0.2.5.dist-info}/licenses/LICENSE +0 -0
- {pydpm_xl-0.2.4.dist-info → pydpm_xl-0.2.5.dist-info}/top_level.txt +0 -0
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.
|
|
44
|
+
__version__ = "0.2.5"
|
|
45
45
|
__author__ = "MeaningfulData S.L."
|
|
46
46
|
__email__ = "info@meaningfuldata.eu"
|
|
47
47
|
__license__ = "GPL-3.0-or-later"
|
|
@@ -369,6 +369,7 @@ class ASTGeneratorAPI:
|
|
|
369
369
|
precondition: Optional[str] = None,
|
|
370
370
|
release_id: Optional[int] = None,
|
|
371
371
|
output_path: Optional[Union[str, Path]] = None,
|
|
372
|
+
primary_module_vid: Optional[int] = None,
|
|
372
373
|
) -> Dict[str, Any]:
|
|
373
374
|
"""
|
|
374
375
|
Generate enriched, engine-ready AST with framework structure (Level 3).
|
|
@@ -381,13 +382,14 @@ class ASTGeneratorAPI:
|
|
|
381
382
|
- Everything from generate_complete_ast() PLUS:
|
|
382
383
|
- Framework structure: operations, variables, tables, preconditions
|
|
383
384
|
- Module metadata: version, release info, dates
|
|
384
|
-
- Dependency information
|
|
385
|
+
- Dependency information (including cross-module dependencies)
|
|
385
386
|
- Coordinates (x/y/z) added to data entries
|
|
386
387
|
|
|
387
388
|
**Typical use case:**
|
|
388
389
|
- Feeding AST to business rule execution engines
|
|
389
390
|
- Validation framework integration
|
|
390
391
|
- Production rule processing
|
|
392
|
+
- Module exports with cross-module dependency tracking
|
|
391
393
|
|
|
392
394
|
Args:
|
|
393
395
|
expression: DPM-XL expression string
|
|
@@ -399,6 +401,11 @@ class ASTGeneratorAPI:
|
|
|
399
401
|
If None, uses all available data (release-agnostic).
|
|
400
402
|
output_path: Optional path (string or Path) to save the enriched_ast as JSON file.
|
|
401
403
|
If provided, the enriched_ast will be automatically saved to this location.
|
|
404
|
+
primary_module_vid: Optional module version ID of the module being exported.
|
|
405
|
+
When provided, enables detection of cross-module dependencies - tables from
|
|
406
|
+
other modules will be identified and added to dependency_modules and
|
|
407
|
+
cross_instance_dependencies fields. If None, cross-module detection uses
|
|
408
|
+
the first table's module as the primary module.
|
|
402
409
|
|
|
403
410
|
Returns:
|
|
404
411
|
dict: {
|
|
@@ -416,14 +423,17 @@ class ASTGeneratorAPI:
|
|
|
416
423
|
... )
|
|
417
424
|
>>> # result['enriched_ast'] contains framework structure ready for engines
|
|
418
425
|
>>>
|
|
419
|
-
>>> #
|
|
426
|
+
>>> # For module exports with cross-module dependency tracking:
|
|
420
427
|
>>> result = generator.generate_enriched_ast(
|
|
421
|
-
... "{
|
|
428
|
+
... "{tC_26.00, r030, c010} * {tC_01.00, r0015, c0010}",
|
|
422
429
|
... dpm_version="4.2",
|
|
423
|
-
... operation_code="
|
|
424
|
-
...
|
|
430
|
+
... operation_code="v2814_m",
|
|
431
|
+
... primary_module_vid=123, # Module being exported
|
|
432
|
+
... release_id=42
|
|
425
433
|
... )
|
|
426
|
-
>>> #
|
|
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
|
|
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
|
|
724
|
-
FromReferenceDate, ToReferenceDate,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
861
|
-
FromReferenceDate, ToReferenceDate,
|
|
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
|
-
|
|
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,
|
|
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
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
#
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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,
|
|
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,
|
|
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):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydpm_xl
|
|
3
|
-
Version: 0.2.
|
|
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,4 +1,4 @@
|
|
|
1
|
-
py_dpm/__init__.py,sha256=
|
|
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
|
|
@@ -7,8 +7,8 @@ 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=
|
|
11
|
-
py_dpm/api/dpm_xl/complete_ast.py,sha256=
|
|
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=
|
|
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,7 +68,7 @@ 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=
|
|
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
|
|
@@ -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.
|
|
80
|
-
pydpm_xl-0.2.
|
|
81
|
-
pydpm_xl-0.2.
|
|
82
|
-
pydpm_xl-0.2.
|
|
83
|
-
pydpm_xl-0.2.
|
|
84
|
-
pydpm_xl-0.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|