pydpm_xl 0.2.5rc3__py3-none-any.whl → 0.2.6__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/__init__.py +0 -2
- py_dpm/api/dpm/data_dictionary.py +2 -0
- py_dpm/api/dpm/explorer.py +75 -1
- py_dpm/api/dpm_xl/__init__.py +0 -2
- py_dpm/api/dpm_xl/ast_generator.py +1050 -202
- py_dpm/api/dpm_xl/complete_ast.py +45 -46
- py_dpm/cli/main.py +9 -9
- py_dpm/dpm/models.py +263 -7
- py_dpm/dpm/queries/explorer_queries.py +119 -0
- py_dpm/dpm_xl/utils/scopes_calculator.py +86 -30
- py_dpm/dpm_xl/utils/serialization.py +5 -3
- py_dpm/instance/instance.py +1 -0
- {pydpm_xl-0.2.5rc3.dist-info → pydpm_xl-0.2.6.dist-info}/METADATA +1 -1
- {pydpm_xl-0.2.5rc3.dist-info → pydpm_xl-0.2.6.dist-info}/RECORD +19 -19
- {pydpm_xl-0.2.5rc3.dist-info → pydpm_xl-0.2.6.dist-info}/WHEEL +1 -1
- {pydpm_xl-0.2.5rc3.dist-info → pydpm_xl-0.2.6.dist-info}/entry_points.txt +0 -0
- {pydpm_xl-0.2.5rc3.dist-info → pydpm_xl-0.2.6.dist-info}/licenses/LICENSE +0 -0
- {pydpm_xl-0.2.5rc3.dist-info → pydpm_xl-0.2.6.dist-info}/top_level.txt +0 -0
|
@@ -12,7 +12,7 @@ For new code, prefer using ASTGeneratorAPI directly:
|
|
|
12
12
|
result = generator.generate_complete_ast(expression)
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
-
from typing import Dict, Any, Optional, List
|
|
15
|
+
from typing import Dict, Any, Optional, List, Union, Tuple
|
|
16
16
|
from py_dpm.api.dpm_xl.ast_generator import ASTGeneratorAPI
|
|
17
17
|
|
|
18
18
|
|
|
@@ -46,35 +46,6 @@ def generate_complete_ast(
|
|
|
46
46
|
return generator.generate_complete_ast(expression, release_id=release_id)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def generate_complete_batch(
|
|
50
|
-
expressions: list,
|
|
51
|
-
database_path: str = None,
|
|
52
|
-
connection_url: str = None,
|
|
53
|
-
release_id: Optional[int] = None,
|
|
54
|
-
):
|
|
55
|
-
"""
|
|
56
|
-
Generate complete ASTs for multiple expressions.
|
|
57
|
-
|
|
58
|
-
This function delegates to ASTGeneratorAPI for backwards compatibility.
|
|
59
|
-
|
|
60
|
-
Args:
|
|
61
|
-
expressions: List of DPM-XL expression strings
|
|
62
|
-
database_path: Path to SQLite database file
|
|
63
|
-
connection_url: SQLAlchemy connection URL for PostgreSQL (optional)
|
|
64
|
-
release_id: Optional release ID to filter database lookups by specific release.
|
|
65
|
-
If None, uses all available data (release-agnostic).
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
list: List of result dictionaries
|
|
69
|
-
"""
|
|
70
|
-
generator = ASTGeneratorAPI(
|
|
71
|
-
database_path=database_path,
|
|
72
|
-
connection_url=connection_url,
|
|
73
|
-
enable_semantic_validation=True
|
|
74
|
-
)
|
|
75
|
-
return generator.generate_complete_batch(expressions, release_id=release_id)
|
|
76
|
-
|
|
77
|
-
|
|
78
49
|
# Convenience function with cleaner interface
|
|
79
50
|
def parse_with_data_fields(
|
|
80
51
|
expression: str,
|
|
@@ -109,33 +80,42 @@ def parse_with_data_fields(
|
|
|
109
80
|
|
|
110
81
|
|
|
111
82
|
def generate_enriched_ast(
|
|
112
|
-
|
|
83
|
+
expressions: Union[str, List[Tuple[str, str, Optional[str]]]],
|
|
113
84
|
database_path: Optional[str] = None,
|
|
114
85
|
connection_url: Optional[str] = None,
|
|
115
|
-
|
|
116
|
-
operation_code: Optional[str] = None,
|
|
86
|
+
release_code: Optional[str] = None,
|
|
117
87
|
table_context: Optional[Dict[str, Any]] = None,
|
|
118
|
-
precondition: Optional[str] = None,
|
|
119
88
|
release_id: Optional[int] = None,
|
|
120
89
|
primary_module_vid: Optional[int] = None,
|
|
90
|
+
module_code: Optional[str] = None,
|
|
91
|
+
preferred_module_dependencies: Optional[List[str]] = None,
|
|
121
92
|
) -> Dict[str, Any]:
|
|
122
93
|
"""
|
|
123
|
-
Generate enriched, engine-ready AST from DPM-XL expression.
|
|
94
|
+
Generate enriched, engine-ready AST from DPM-XL expression(s).
|
|
124
95
|
|
|
125
96
|
This function delegates to ASTGeneratorAPI for backwards compatibility.
|
|
126
97
|
|
|
98
|
+
Supports both single expressions (for backward compatibility) and multiple
|
|
99
|
+
expression/operation/precondition tuples for generating scripts with multiple operations.
|
|
100
|
+
|
|
127
101
|
Args:
|
|
128
|
-
|
|
102
|
+
expressions: Either a single DPM-XL expression string (backward compatible),
|
|
103
|
+
or a list of tuples: [(expression, operation_code, precondition), ...].
|
|
104
|
+
Each tuple contains:
|
|
105
|
+
- expression (str): The DPM-XL expression (required)
|
|
106
|
+
- operation_code (str): The operation code (required)
|
|
107
|
+
- precondition (Optional[str]): Optional precondition reference (e.g., {v_F_44_04})
|
|
129
108
|
database_path: Path to SQLite database (or None for PostgreSQL)
|
|
130
109
|
connection_url: PostgreSQL connection URL (takes precedence over database_path)
|
|
131
|
-
|
|
132
|
-
operation_code: Optional operation code (defaults to "default_code")
|
|
110
|
+
release_code: DPM release code (e.g., "4.0", "4.1", "4.2")
|
|
133
111
|
table_context: Optional table context dict with keys: 'table', 'columns', 'rows', 'sheets', 'default', 'interval'
|
|
134
|
-
precondition: Optional precondition variable reference (e.g., {v_F_44_04})
|
|
135
112
|
release_id: Optional release ID to filter database lookups by specific release.
|
|
136
113
|
If None, uses all available data (release-agnostic).
|
|
137
114
|
primary_module_vid: Optional module version ID of the module being exported.
|
|
138
115
|
When provided, enables detection of cross-module dependencies.
|
|
116
|
+
module_code: Optional module code (e.g., "FINREP9") to specify the main module.
|
|
117
|
+
preferred_module_dependencies: Optional list of module codes to prefer when
|
|
118
|
+
multiple dependency scopes are possible.
|
|
139
119
|
|
|
140
120
|
Returns:
|
|
141
121
|
dict: {
|
|
@@ -143,6 +123,25 @@ def generate_enriched_ast(
|
|
|
143
123
|
'enriched_ast': dict, # Engine-ready AST with framework structure
|
|
144
124
|
'error': str # Error message if failed
|
|
145
125
|
}
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
>>> # Single expression (backward compatible)
|
|
129
|
+
>>> result = generate_enriched_ast(
|
|
130
|
+
... "{tF_01.00, r0010, c0010}",
|
|
131
|
+
... database_path="data.db",
|
|
132
|
+
... release_code="4.2",
|
|
133
|
+
... )
|
|
134
|
+
>>>
|
|
135
|
+
>>> # Multiple expressions
|
|
136
|
+
>>> result = generate_enriched_ast(
|
|
137
|
+
... [
|
|
138
|
+
... ("{tF_01.00, r0010, c0010} = 0", "v1234_m", None),
|
|
139
|
+
... ("{tF_01.00, r0020, c0010} > 0", "v1235_m", "{v_F_44_04}"),
|
|
140
|
+
... ],
|
|
141
|
+
... database_path="data.db",
|
|
142
|
+
... release_code="4.2",
|
|
143
|
+
... module_code="FINREP9",
|
|
144
|
+
... )
|
|
146
145
|
"""
|
|
147
146
|
generator = ASTGeneratorAPI(
|
|
148
147
|
database_path=database_path,
|
|
@@ -150,13 +149,13 @@ def generate_enriched_ast(
|
|
|
150
149
|
enable_semantic_validation=True
|
|
151
150
|
)
|
|
152
151
|
return generator.generate_enriched_ast(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
operation_code=operation_code,
|
|
152
|
+
expressions=expressions,
|
|
153
|
+
release_code=release_code,
|
|
156
154
|
table_context=table_context,
|
|
157
|
-
precondition=precondition,
|
|
158
155
|
release_id=release_id,
|
|
159
156
|
primary_module_vid=primary_module_vid,
|
|
157
|
+
module_code=module_code,
|
|
158
|
+
preferred_module_dependencies=preferred_module_dependencies,
|
|
160
159
|
)
|
|
161
160
|
|
|
162
161
|
|
|
@@ -166,7 +165,7 @@ def enrich_ast_with_metadata(
|
|
|
166
165
|
context: Optional[Dict[str, Any]],
|
|
167
166
|
database_path: Optional[str] = None,
|
|
168
167
|
connection_url: Optional[str] = None,
|
|
169
|
-
|
|
168
|
+
release_code: Optional[str] = None,
|
|
170
169
|
operation_code: Optional[str] = None,
|
|
171
170
|
precondition: Optional[str] = None,
|
|
172
171
|
release_id: Optional[int] = None,
|
|
@@ -183,7 +182,7 @@ def enrich_ast_with_metadata(
|
|
|
183
182
|
context: Context dict with table, rows, columns, sheets, default, interval
|
|
184
183
|
database_path: Path to SQLite database
|
|
185
184
|
connection_url: PostgreSQL connection URL (takes precedence)
|
|
186
|
-
|
|
185
|
+
release_code: DPM release code (e.g., "4.2")
|
|
187
186
|
operation_code: Operation code (defaults to "default_code")
|
|
188
187
|
precondition: Precondition variable reference (e.g., {v_F_44_04})
|
|
189
188
|
release_id: Optional release ID to filter database lookups
|
|
@@ -201,7 +200,7 @@ def enrich_ast_with_metadata(
|
|
|
201
200
|
ast_dict=ast_dict,
|
|
202
201
|
expression=expression,
|
|
203
202
|
context=context,
|
|
204
|
-
|
|
203
|
+
release_code=release_code,
|
|
205
204
|
operation_code=operation_code,
|
|
206
205
|
precondition=precondition,
|
|
207
206
|
release_id=release_id,
|
py_dpm/cli/main.py
CHANGED
|
@@ -61,41 +61,41 @@ def migrate_access(access_file: str):
|
|
|
61
61
|
"--release-id", type=int, help="Release ID to use for validation", default=None
|
|
62
62
|
)
|
|
63
63
|
@click.option(
|
|
64
|
-
"--
|
|
64
|
+
"--release-code",
|
|
65
65
|
type=str,
|
|
66
|
-
help="
|
|
66
|
+
help="Release code (e.g. 4.2) to use for validation",
|
|
67
67
|
default=None,
|
|
68
68
|
)
|
|
69
|
-
def semantic(expression: str, release_id: int,
|
|
69
|
+
def semantic(expression: str, release_id: int, release_code: str):
|
|
70
70
|
"""
|
|
71
71
|
Semantically analyses the input expression by applying the syntax validation, the operands checking, the data type
|
|
72
72
|
validation and the structure validation
|
|
73
73
|
:param expression: Expression to be analysed
|
|
74
74
|
:param release_id: ID of the release used. If None, gathers the live release
|
|
75
|
-
:param
|
|
75
|
+
:param release_code: Version code of the release used.
|
|
76
76
|
Used only in DPM-ML generation
|
|
77
77
|
:return if Return_data is False, any Symbol, else data extracted from DB based on operands cell references
|
|
78
78
|
"""
|
|
79
79
|
|
|
80
|
-
if release_id is not None and
|
|
81
|
-
raise click.UsageError("Cannot provide both --release-id and --
|
|
80
|
+
if release_id is not None and release_code is not None:
|
|
81
|
+
raise click.UsageError("Cannot provide both --release-id and --release-code")
|
|
82
82
|
|
|
83
83
|
error_code = ""
|
|
84
84
|
validation_type = STATUS_UNKNOWN
|
|
85
85
|
|
|
86
86
|
semantic_api = SemanticAPI()
|
|
87
87
|
|
|
88
|
-
if
|
|
88
|
+
if release_code:
|
|
89
89
|
from py_dpm.dpm.models import Release
|
|
90
90
|
|
|
91
91
|
release_id = (
|
|
92
92
|
semantic_api.session.query(Release.releaseid)
|
|
93
|
-
.filter(Release.code ==
|
|
93
|
+
.filter(Release.code == release_code)
|
|
94
94
|
.scalar()
|
|
95
95
|
)
|
|
96
96
|
if release_id is None:
|
|
97
97
|
console.print(
|
|
98
|
-
f"Error:
|
|
98
|
+
f"Error: Release code '{release_code}' not found.", style="bold red"
|
|
99
99
|
)
|
|
100
100
|
sys.exit(1)
|
|
101
101
|
|
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,176 @@ 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
|
+
# Convert to native Python int to avoid numpy.int64 issues with PostgreSQL
|
|
1010
|
+
module_vids_needing_fallback = [
|
|
1011
|
+
int(vid) for vid in rows_needing_fallback[module_vid_col].unique()
|
|
1012
|
+
]
|
|
1013
|
+
|
|
1014
|
+
# Batch query: get module info (moduleid, startreleaseid) for affected rows
|
|
1015
|
+
current_modules = (
|
|
1016
|
+
session.query(
|
|
1017
|
+
cls.modulevid,
|
|
1018
|
+
cls.moduleid,
|
|
1019
|
+
cls.startreleaseid,
|
|
1020
|
+
)
|
|
1021
|
+
.filter(cls.modulevid.in_(module_vids_needing_fallback))
|
|
1022
|
+
.all()
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
# Build mapping: current_modulevid -> (moduleid, startreleaseid)
|
|
1026
|
+
current_module_info = {
|
|
1027
|
+
row.modulevid: (row.moduleid, row.startreleaseid) for row in current_modules
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
# Get all potential previous versions for the affected modules
|
|
1031
|
+
unique_module_ids = list(set(info[0] for info in current_module_info.values()))
|
|
1032
|
+
|
|
1033
|
+
previous_versions_query = (
|
|
1034
|
+
session.query(cls)
|
|
1035
|
+
.filter(cls.moduleid.in_(unique_module_ids))
|
|
1036
|
+
.order_by(cls.moduleid, cls.startreleaseid.desc())
|
|
1037
|
+
.all()
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
# Build lookup: moduleid -> list of versions sorted by startreleaseid desc
|
|
1041
|
+
versions_by_moduleid = {}
|
|
1042
|
+
for mv in previous_versions_query:
|
|
1043
|
+
if mv.moduleid not in versions_by_moduleid:
|
|
1044
|
+
versions_by_moduleid[mv.moduleid] = []
|
|
1045
|
+
versions_by_moduleid[mv.moduleid].append(mv)
|
|
1046
|
+
|
|
1047
|
+
# For each current modulevid, find the previous version
|
|
1048
|
+
# Skip versions that also have equal dates (ghost modules)
|
|
1049
|
+
replacement_map = {} # current_modulevid -> previous_moduleversion
|
|
1050
|
+
for current_vid, (moduleid, current_startreleaseid) in current_module_info.items():
|
|
1051
|
+
versions = versions_by_moduleid.get(moduleid, [])
|
|
1052
|
+
for mv in versions:
|
|
1053
|
+
if mv.startreleaseid < current_startreleaseid:
|
|
1054
|
+
# Only use this version if it has different dates
|
|
1055
|
+
# (skip ghost modules where from == to)
|
|
1056
|
+
if mv.fromreferencedate != mv.toreferencedate:
|
|
1057
|
+
replacement_map[current_vid] = mv
|
|
1058
|
+
break # Already sorted desc, so first match is highest
|
|
1059
|
+
|
|
1060
|
+
# Apply replacements to DataFrame
|
|
1061
|
+
if not replacement_map:
|
|
1062
|
+
return df
|
|
1063
|
+
|
|
1064
|
+
# Create a copy to avoid modifying original
|
|
1065
|
+
result_df = df.copy()
|
|
1066
|
+
|
|
1067
|
+
for idx, row in result_df.iterrows():
|
|
1068
|
+
if row["FromReferenceDate"] == row["ToReferenceDate"]:
|
|
1069
|
+
# Convert to native Python int to match replacement_map keys
|
|
1070
|
+
current_vid = int(row[module_vid_col])
|
|
1071
|
+
if current_vid in replacement_map:
|
|
1072
|
+
prev_mv = replacement_map[current_vid]
|
|
1073
|
+
result_df.at[idx, "ModuleVID"] = prev_mv.modulevid
|
|
1074
|
+
result_df.at[idx, "ModuleCode"] = prev_mv.code
|
|
1075
|
+
result_df.at[idx, "VersionNumber"] = prev_mv.versionnumber
|
|
1076
|
+
result_df.at[idx, "FromReferenceDate"] = prev_mv.fromreferencedate
|
|
1077
|
+
result_df.at[idx, "ToReferenceDate"] = prev_mv.toreferencedate
|
|
1078
|
+
if "StartReleaseID" in result_df.columns:
|
|
1079
|
+
result_df.at[idx, "StartReleaseID"] = prev_mv.startreleaseid
|
|
1080
|
+
if "EndReleaseID" in result_df.columns:
|
|
1081
|
+
result_df.at[idx, "EndReleaseID"] = prev_mv.endreleaseid
|
|
1082
|
+
|
|
1083
|
+
return result_df
|
|
1084
|
+
|
|
1085
|
+
@classmethod
|
|
1086
|
+
def get_from_release_id(
|
|
1087
|
+
cls, session, release_id, module_id=None, module_code=None
|
|
1088
|
+
):
|
|
1089
|
+
"""
|
|
1090
|
+
Get the module version applicable to a given release for a specific module.
|
|
1091
|
+
|
|
1092
|
+
If the resulting module version has fromreferencedate == toreferencedate,
|
|
1093
|
+
the previous module version for the same module is returned instead.
|
|
1094
|
+
|
|
1095
|
+
Args:
|
|
1096
|
+
session: SQLAlchemy session
|
|
1097
|
+
release_id: The release ID to filter for
|
|
1098
|
+
module_id: Optional module ID (mutually exclusive with module_code)
|
|
1099
|
+
module_code: Optional module code (mutually exclusive with module_id)
|
|
1100
|
+
|
|
1101
|
+
Returns:
|
|
1102
|
+
ModuleVersion instance or None if not found
|
|
1103
|
+
|
|
1104
|
+
Raises:
|
|
1105
|
+
ValueError: If neither module_id nor module_code is provided,
|
|
1106
|
+
or if both are provided
|
|
1107
|
+
"""
|
|
1108
|
+
if module_id is None and module_code is None:
|
|
1109
|
+
raise ValueError("Either module_id or module_code must be provided.")
|
|
1110
|
+
if module_id is not None and module_code is not None:
|
|
1111
|
+
raise ValueError(
|
|
1112
|
+
"Specify only one of module_id or module_code, not both."
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
# Build the base query with release filtering
|
|
1116
|
+
query = session.query(cls).filter(
|
|
1117
|
+
and_(
|
|
1118
|
+
cls.startreleaseid <= release_id,
|
|
1119
|
+
or_(cls.endreleaseid > release_id, cls.endreleaseid.is_(None)),
|
|
1120
|
+
)
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
# Apply module filter
|
|
1124
|
+
if module_id is not None:
|
|
1125
|
+
query = query.filter(cls.moduleid == module_id)
|
|
1126
|
+
else: # module_code
|
|
1127
|
+
query = query.filter(cls.code == module_code)
|
|
1128
|
+
|
|
1129
|
+
module_version = query.first()
|
|
1130
|
+
|
|
1131
|
+
if module_version is None:
|
|
1132
|
+
return None
|
|
1133
|
+
|
|
1134
|
+
# Check if fromreferencedate == toreferencedate
|
|
1135
|
+
if module_version.fromreferencedate == module_version.toreferencedate:
|
|
1136
|
+
# Get the previous module version for the same module
|
|
1137
|
+
prev_query = (
|
|
1138
|
+
session.query(cls)
|
|
1139
|
+
.filter(
|
|
1140
|
+
cls.moduleid == module_version.moduleid,
|
|
1141
|
+
cls.startreleaseid < module_version.startreleaseid,
|
|
1142
|
+
)
|
|
1143
|
+
.order_by(cls.startreleaseid.desc())
|
|
1144
|
+
)
|
|
1145
|
+
prev_module_version = prev_query.first()
|
|
1146
|
+
if prev_module_version:
|
|
1147
|
+
return prev_module_version
|
|
1148
|
+
|
|
1149
|
+
return module_version
|
|
1150
|
+
|
|
949
1151
|
@classmethod
|
|
950
1152
|
def get_last_release(cls, session):
|
|
951
1153
|
"""
|
|
@@ -1119,6 +1321,60 @@ class OperationScope(Base):
|
|
|
1119
1321
|
"OperationScopeComposition", back_populates="operation_scope"
|
|
1120
1322
|
)
|
|
1121
1323
|
|
|
1324
|
+
def to_dict(self):
|
|
1325
|
+
"""
|
|
1326
|
+
Convert the operation scope to a dictionary representation.
|
|
1327
|
+
|
|
1328
|
+
Returns:
|
|
1329
|
+
dict: A dictionary with module codes as keys and module details as values.
|
|
1330
|
+
Format: {
|
|
1331
|
+
"<module_code>": {
|
|
1332
|
+
"module_version_number": <versionnumber>,
|
|
1333
|
+
"from_reference_date": <fromreferencedate>,
|
|
1334
|
+
"to_reference_date": <toreferencedate>
|
|
1335
|
+
},
|
|
1336
|
+
...
|
|
1337
|
+
}
|
|
1338
|
+
"""
|
|
1339
|
+
from sqlalchemy.orm import object_session
|
|
1340
|
+
|
|
1341
|
+
def format_date(date_value):
|
|
1342
|
+
"""Format date to string (YYYY-MM-DD) or None if NaT/None."""
|
|
1343
|
+
if date_value is None:
|
|
1344
|
+
return None
|
|
1345
|
+
if pd.isna(date_value):
|
|
1346
|
+
return None
|
|
1347
|
+
if hasattr(date_value, "strftime"):
|
|
1348
|
+
return date_value.strftime("%Y-%m-%d")
|
|
1349
|
+
return str(date_value)
|
|
1350
|
+
|
|
1351
|
+
result = {}
|
|
1352
|
+
for composition in self.operation_scope_compositions:
|
|
1353
|
+
# For new/proposed scopes, use transient _module_info attribute
|
|
1354
|
+
if hasattr(composition, "_module_info") and composition._module_info:
|
|
1355
|
+
info = composition._module_info
|
|
1356
|
+
result[info["code"]] = {
|
|
1357
|
+
"module_version_number": info["version_number"],
|
|
1358
|
+
"from_reference_date": format_date(info["from_reference_date"]),
|
|
1359
|
+
"to_reference_date": format_date(info["to_reference_date"]),
|
|
1360
|
+
}
|
|
1361
|
+
else:
|
|
1362
|
+
# For existing scopes from DB, use relationship or query
|
|
1363
|
+
module_version = composition.module_version
|
|
1364
|
+
if module_version is None:
|
|
1365
|
+
session = object_session(self)
|
|
1366
|
+
if session is not None:
|
|
1367
|
+
module_version = session.query(ModuleVersion).filter(
|
|
1368
|
+
ModuleVersion.modulevid == composition.modulevid
|
|
1369
|
+
).first()
|
|
1370
|
+
if module_version is not None:
|
|
1371
|
+
result[module_version.code] = {
|
|
1372
|
+
"module_version_number": module_version.versionnumber,
|
|
1373
|
+
"from_reference_date": format_date(module_version.fromreferencedate),
|
|
1374
|
+
"to_reference_date": format_date(module_version.toreferencedate),
|
|
1375
|
+
}
|
|
1376
|
+
return result
|
|
1377
|
+
|
|
1122
1378
|
|
|
1123
1379
|
class OperationScopeComposition(Base):
|
|
1124
1380
|
__tablename__ = "OperationScopeComposition"
|