pydpm_xl 0.2.5rc2__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.
@@ -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
- expression: str,
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
- dpm_version: Optional[str] = None,
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
- expression: DPM-XL expression string
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
- dpm_version: DPM version code (e.g., "4.0", "4.1", "4.2")
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
- expression=expression,
154
- dpm_version=dpm_version,
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
- dpm_version: Optional[str] = None,
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
- dpm_version: DPM version code (e.g., "4.2")
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
- dpm_version=dpm_version,
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
- "--dpm-version",
64
+ "--release-code",
65
65
  type=str,
66
- help="DPM Version (e.g. 4.2) to use for validation",
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, dpm_version: str):
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 dpm_version: Version code of the release used.
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 dpm_version is not None:
81
- raise click.UsageError("Cannot provide both --release-id and --dpm-version")
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 dpm_version:
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 == dpm_version)
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: DPM version '{dpm_version}' not found.", style="bold red"
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 VARIABLE_VID),
724
- FromReferenceDate, ToReferenceDate, EndReleaseID
723
+ pandas DataFrame with columns: ModuleVID, TableVID (as variable_vid),
724
+ ModuleCode, VersionNumber, FromReferenceDate, ToReferenceDate,
725
+ StartReleaseID, EndReleaseID
725
726
  """
726
727
  if not tables_vids:
727
728
  return pd.DataFrame(
728
729
  columns=[
729
730
  "ModuleVID",
730
731
  "variable_vid",
732
+ "ModuleCode",
733
+ "VersionNumber",
731
734
  "FromReferenceDate",
732
735
  "ToReferenceDate",
736
+ "StartReleaseID",
733
737
  "EndReleaseID",
734
738
  ]
735
739
  )
@@ -738,8 +742,11 @@ class ModuleVersion(Base):
738
742
  session.query(
739
743
  cls.modulevid.label("ModuleVID"),
740
744
  ModuleVersionComposition.tablevid.label("variable_vid"),
745
+ cls.code.label("ModuleCode"),
746
+ cls.versionnumber.label("VersionNumber"),
741
747
  cls.fromreferencedate.label("FromReferenceDate"),
742
748
  cls.toreferencedate.label("ToReferenceDate"),
749
+ cls.startreleaseid.label("StartReleaseID"),
743
750
  cls.endreleaseid.label("EndReleaseID"),
744
751
  )
745
752
  .join(
@@ -759,16 +766,20 @@ class ModuleVersion(Base):
759
766
  )
760
767
 
761
768
  results = query.all()
762
- return pd.DataFrame(
769
+ df = pd.DataFrame(
763
770
  results,
764
771
  columns=[
765
772
  "ModuleVID",
766
773
  "variable_vid",
774
+ "ModuleCode",
775
+ "VersionNumber",
767
776
  "FromReferenceDate",
768
777
  "ToReferenceDate",
778
+ "StartReleaseID",
769
779
  "EndReleaseID",
770
780
  ],
771
781
  )
782
+ return cls._apply_fallback_for_equal_dates(session, df)
772
783
 
773
784
  @classmethod
774
785
  def get_from_table_codes(cls, session, table_codes, release_id=None):
@@ -790,6 +801,8 @@ class ModuleVersion(Base):
790
801
  columns=[
791
802
  "ModuleVID",
792
803
  "variable_vid",
804
+ "ModuleCode",
805
+ "VersionNumber",
793
806
  "FromReferenceDate",
794
807
  "ToReferenceDate",
795
808
  "StartReleaseID",
@@ -804,6 +817,8 @@ class ModuleVersion(Base):
804
817
  session.query(
805
818
  cls.modulevid.label("ModuleVID"),
806
819
  ModuleVersionComposition.tablevid.label("variable_vid"),
820
+ cls.code.label("ModuleCode"),
821
+ cls.versionnumber.label("VersionNumber"),
807
822
  cls.fromreferencedate.label("FromReferenceDate"),
808
823
  cls.toreferencedate.label("ToReferenceDate"),
809
824
  cls.startreleaseid.label("StartReleaseID"),
@@ -831,11 +846,13 @@ class ModuleVersion(Base):
831
846
  )
832
847
 
833
848
  results = query.all()
834
- return pd.DataFrame(
849
+ df = pd.DataFrame(
835
850
  results,
836
851
  columns=[
837
852
  "ModuleVID",
838
853
  "variable_vid",
854
+ "ModuleCode",
855
+ "VersionNumber",
839
856
  "FromReferenceDate",
840
857
  "ToReferenceDate",
841
858
  "StartReleaseID",
@@ -843,6 +860,7 @@ class ModuleVersion(Base):
843
860
  "TableCode",
844
861
  ],
845
862
  )
863
+ return cls._apply_fallback_for_equal_dates(session, df)
846
864
 
847
865
  @classmethod
848
866
  def get_precondition_module_versions(
@@ -857,16 +875,21 @@ class ModuleVersion(Base):
857
875
  release_id: Optional release ID to filter modules by release range
858
876
 
859
877
  Returns:
860
- pandas DataFrame with columns: ModuleVID, VariableVID (as VARIABLE_VID),
861
- FromReferenceDate, ToReferenceDate, Code
878
+ pandas DataFrame with columns: ModuleVID, variable_vid (VariableVID),
879
+ ModuleCode, VersionNumber, FromReferenceDate, ToReferenceDate,
880
+ StartReleaseID, EndReleaseID, Code
862
881
  """
863
882
  if not precondition_items:
864
883
  return pd.DataFrame(
865
884
  columns=[
866
885
  "ModuleVID",
867
886
  "variable_vid",
887
+ "ModuleCode",
888
+ "VersionNumber",
868
889
  "FromReferenceDate",
869
890
  "ToReferenceDate",
891
+ "StartReleaseID",
892
+ "EndReleaseID",
870
893
  "Code",
871
894
  ]
872
895
  )
@@ -875,8 +898,12 @@ class ModuleVersion(Base):
875
898
  session.query(
876
899
  cls.modulevid.label("ModuleVID"),
877
900
  VariableVersion.variablevid.label("variable_vid"),
901
+ cls.code.label("ModuleCode"),
902
+ cls.versionnumber.label("VersionNumber"),
878
903
  cls.fromreferencedate.label("FromReferenceDate"),
879
904
  cls.toreferencedate.label("ToReferenceDate"),
905
+ cls.startreleaseid.label("StartReleaseID"),
906
+ cls.endreleaseid.label("EndReleaseID"),
880
907
  VariableVersion.code.label("Code"),
881
908
  )
882
909
  .join(ModuleParameters, cls.modulevid == ModuleParameters.modulevid)
@@ -899,16 +926,21 @@ class ModuleVersion(Base):
899
926
  )
900
927
 
901
928
  results = query.all()
902
- return pd.DataFrame(
929
+ df = pd.DataFrame(
903
930
  results,
904
931
  columns=[
905
932
  "ModuleVID",
906
933
  "variable_vid",
934
+ "ModuleCode",
935
+ "VersionNumber",
907
936
  "FromReferenceDate",
908
937
  "ToReferenceDate",
938
+ "StartReleaseID",
939
+ "EndReleaseID",
909
940
  "Code",
910
941
  ],
911
942
  )
943
+ return cls._apply_fallback_for_equal_dates(session, df)
912
944
 
913
945
  @classmethod
914
946
  def get_module_version_by_vid(cls, session, vid):
@@ -946,6 +978,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"