44
55import hashlib
66import json
7+ import tempfile
78from dataclasses import dataclass
89from datetime import timezone
910from pathlib import Path
1516DEFAULT_SNAPSHOT_DATE_COLUMNS = ("as_of" , "snapshot_date" )
1617DEFAULT_MAX_SNAPSHOT_MONTH_LAG = 1
1718DEFAULT_SNAPSHOT_MANIFEST_SUFFIX = ".manifest.json"
19+ DEFAULT_ARTIFACT_CACHE_DIR = Path (tempfile .gettempdir ()) / "quant_strategy_artifacts"
1820
1921
2022@dataclass (frozen = True )
@@ -99,6 +101,56 @@ def _resolve_manifest_path(snapshot_path: Path, manifest_path: str | None) -> Pa
99101 return Path (f"{ snapshot_path } { DEFAULT_SNAPSHOT_MANIFEST_SUFFIX } " )
100102
101103
104+ def _is_gcs_uri (reference : str | None ) -> bool :
105+ return str (reference or "" ).strip ().startswith ("gs://" )
106+
107+
108+ def _resolve_manifest_reference (snapshot_reference : str , manifest_path : str | None ) -> str :
109+ raw_manifest = str (manifest_path or "" ).strip ()
110+ if raw_manifest :
111+ return raw_manifest
112+ return f"{ str (snapshot_reference ).strip ()} { DEFAULT_SNAPSHOT_MANIFEST_SUFFIX } "
113+
114+
115+ def _parse_gcs_uri (uri : str ) -> tuple [str , str ]:
116+ raw_uri = str (uri or "" ).strip ()
117+ if not raw_uri .startswith ("gs://" ):
118+ raise ValueError (f"Unsupported GCS URI: { raw_uri } " )
119+ bucket_name , _ , object_name = raw_uri [5 :].partition ("/" )
120+ if not bucket_name or not object_name :
121+ raise ValueError (f"Invalid GCS URI: { raw_uri } " )
122+ return bucket_name , object_name
123+
124+
125+ def _download_gcs_object (uri : str , destination : Path ) -> None :
126+ from google .cloud import storage
127+
128+ bucket_name , object_name = _parse_gcs_uri (uri )
129+ destination .parent .mkdir (parents = True , exist_ok = True )
130+ client = storage .Client ()
131+ client .bucket (bucket_name ).blob (object_name ).download_to_filename (str (destination ))
132+
133+
134+ def _cache_path_for_remote_artifact (reference : str ) -> Path :
135+ raw_reference = str (reference or "" ).strip ()
136+ digest = hashlib .sha256 (raw_reference .encode ("utf-8" )).hexdigest ()[:16 ]
137+ leaf_name = Path (raw_reference ).name or "artifact"
138+ return DEFAULT_ARTIFACT_CACHE_DIR / digest / leaf_name
139+
140+
141+ def _materialize_artifact_path (reference : str ) -> tuple [Path , dict [str , object ]]:
142+ raw_reference = str (reference or "" ).strip ()
143+ if not raw_reference :
144+ raise ValueError ("artifact reference is required" )
145+
146+ if not _is_gcs_uri (raw_reference ):
147+ return Path (raw_reference ), {"source_uri" : None , "local_path" : raw_reference }
148+
149+ local_path = _cache_path_for_remote_artifact (raw_reference )
150+ _download_gcs_object (raw_reference , local_path )
151+ return local_path , {"source_uri" : raw_reference , "local_path" : str (local_path )}
152+
153+
102154def _normalize_manifest_payload (payload : dict [str , object ]) -> dict [str , object ]:
103155 normalized = dict (payload )
104156 if "snapshot_as_of" in normalized :
@@ -117,7 +169,12 @@ def load_feature_snapshot(path: str) -> pd.DataFrame:
117169 raw_path = str (path or "" ).strip ()
118170 if not raw_path :
119171 raise EnvironmentError ("Feature snapshot path is required" )
120- snapshot_path = Path (raw_path )
172+ try :
173+ snapshot_path , _ = _materialize_artifact_path (raw_path )
174+ except Exception as exc :
175+ raise FileNotFoundError (
176+ f"Feature snapshot unavailable: { raw_path } ({ type (exc ).__name__ } : { exc } )"
177+ ) from exc
121178 if not snapshot_path .exists ():
122179 raise FileNotFoundError (f"Feature snapshot not found: { snapshot_path } " )
123180 return _load_snapshot_frame (snapshot_path )
@@ -150,6 +207,77 @@ def load_feature_snapshot_guarded(
150207 ),
151208 )
152209
210+ manifest_reference = _resolve_manifest_reference (raw_path , manifest_path )
211+ if _is_gcs_uri (raw_path ) or _is_gcs_uri (manifest_reference ):
212+ try :
213+ local_snapshot_path , snapshot_artifact_metadata = _materialize_artifact_path (raw_path )
214+ except Exception as exc :
215+ return FeatureSnapshotGuardResult (
216+ frame = None ,
217+ metadata = _build_guard_metadata (
218+ snapshot_path = raw_path ,
219+ decision = "fail_closed" ,
220+ snapshot_exists = False ,
221+ snapshot_source_uri = raw_path if _is_gcs_uri (raw_path ) else None ,
222+ fail_reason = f"feature_snapshot_download_failed:{ type (exc ).__name__ } :{ exc } " ,
223+ ),
224+ )
225+
226+ local_manifest_path = None
227+ manifest_artifact_metadata = {
228+ "source_uri" : manifest_reference if _is_gcs_uri (manifest_reference ) else None ,
229+ "local_path" : manifest_reference ,
230+ }
231+ manifest_download_error = None
232+ try :
233+ local_manifest_path , manifest_artifact_metadata = _materialize_artifact_path (
234+ manifest_reference
235+ )
236+ except Exception as exc :
237+ manifest_download_error = f"{ type (exc ).__name__ } :{ exc } "
238+ if require_manifest :
239+ return FeatureSnapshotGuardResult (
240+ frame = None ,
241+ metadata = _build_guard_metadata (
242+ snapshot_path = raw_path ,
243+ decision = "fail_closed" ,
244+ snapshot_exists = True ,
245+ snapshot_source_uri = snapshot_artifact_metadata .get ("source_uri" ),
246+ snapshot_local_path = snapshot_artifact_metadata .get ("local_path" ),
247+ snapshot_manifest_path = manifest_reference ,
248+ snapshot_manifest_exists = False ,
249+ snapshot_manifest_source_uri = manifest_artifact_metadata .get ("source_uri" ),
250+ snapshot_manifest_download_error = manifest_download_error ,
251+ fail_reason = f"feature_snapshot_manifest_download_failed:{ manifest_download_error } " ,
252+ ),
253+ )
254+
255+ result = load_feature_snapshot_guarded (
256+ str (local_snapshot_path ),
257+ run_as_of = run_as_of ,
258+ required_columns = required_columns ,
259+ snapshot_date_columns = snapshot_date_columns ,
260+ max_snapshot_month_lag = max_snapshot_month_lag ,
261+ manifest_path = str (local_manifest_path ) if local_manifest_path is not None else None ,
262+ require_manifest = require_manifest ,
263+ expected_strategy_profile = expected_strategy_profile ,
264+ expected_config_name = expected_config_name ,
265+ expected_config_path = expected_config_path ,
266+ expected_contract_version = expected_contract_version ,
267+ )
268+ metadata = dict (result .metadata )
269+ metadata ["feature_snapshot_path" ] = raw_path
270+ metadata ["snapshot_path" ] = raw_path
271+ metadata ["snapshot_source_uri" ] = snapshot_artifact_metadata .get ("source_uri" )
272+ metadata ["snapshot_local_path" ] = snapshot_artifact_metadata .get ("local_path" )
273+ metadata ["snapshot_manifest_path" ] = manifest_reference
274+ metadata ["snapshot_manifest_source_uri" ] = manifest_artifact_metadata .get ("source_uri" )
275+ metadata ["snapshot_manifest_local_path" ] = manifest_artifact_metadata .get ("local_path" )
276+ if manifest_download_error is not None :
277+ metadata ["snapshot_manifest_download_error" ] = manifest_download_error
278+ metadata ["snapshot_manifest_exists" ] = False
279+ return FeatureSnapshotGuardResult (frame = result .frame , metadata = metadata )
280+
153281 snapshot_path = Path (raw_path )
154282 manifest_file = _resolve_manifest_path (snapshot_path , manifest_path )
155283 file_timestamp = None
0 commit comments