Skip to content

Commit 53883b8

Browse files
committed
Support GCS feature snapshot artifacts
1 parent 1b8a173 commit 53883b8

2 files changed

Lines changed: 211 additions & 1 deletion

File tree

application/feature_snapshot_service.py

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import hashlib
66
import json
7+
import tempfile
78
from dataclasses import dataclass
89
from datetime import timezone
910
from pathlib import Path
@@ -15,6 +16,7 @@
1516
DEFAULT_SNAPSHOT_DATE_COLUMNS = ("as_of", "snapshot_date")
1617
DEFAULT_MAX_SNAPSHOT_MONTH_LAG = 1
1718
DEFAULT_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+
102154
def _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

tests/test_feature_snapshot_service.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import unittest
66
from pathlib import Path
77
from tempfile import TemporaryDirectory
8+
from unittest.mock import patch
89

910

1011
def _sha256_file(path: Path) -> str:
@@ -145,6 +146,87 @@ def test_load_feature_snapshot_guarded_validates_manifest_checksums(self):
145146
self.assertEqual(result.metadata["snapshot_manifest_strategy_profile"], "tech_pullback_cash_buffer")
146147
self.assertEqual(result.metadata["snapshot_manifest_config_name"], "tech_pullback_cash_buffer")
147148

149+
def test_load_feature_snapshot_downloads_gcs_csv(self):
150+
_skip_if_missing_pandas()
151+
from application.feature_snapshot_service import load_feature_snapshot
152+
153+
with TemporaryDirectory() as tmp_dir:
154+
source_uri = "gs://unit-test-bucket/snapshots/tech_pullback.csv"
155+
156+
def fake_download(uri: str, destination: Path) -> None:
157+
self.assertEqual(uri, source_uri)
158+
destination.parent.mkdir(parents=True, exist_ok=True)
159+
destination.write_text("symbol,sector,mom_6_1\nAAA,tech,0.1\n", encoding="utf-8")
160+
161+
with patch("application.feature_snapshot_service._download_gcs_object", side_effect=fake_download):
162+
frame = load_feature_snapshot(source_uri)
163+
164+
self.assertEqual(frame.to_dict(orient="records"), [{"symbol": "AAA", "sector": "tech", "mom_6_1": 0.1}])
165+
166+
def test_load_feature_snapshot_guarded_downloads_gcs_snapshot_and_manifest(self):
167+
_skip_if_missing_pandas()
168+
from application.feature_snapshot_service import load_feature_snapshot_guarded
169+
170+
with TemporaryDirectory() as tmp_dir:
171+
config_path = Path(tmp_dir) / "config.json"
172+
config_path.write_text(json.dumps({"name": "tech_pullback_cash_buffer"}), encoding="utf-8")
173+
174+
snapshot_uri = "gs://unit-test-bucket/snapshots/tech_pullback_cash_buffer_feature_snapshot_latest.csv"
175+
manifest_uri = f"{snapshot_uri}.manifest.json"
176+
downloaded_snapshot_path: Path | None = None
177+
178+
def fake_download(uri: str, destination: Path) -> None:
179+
nonlocal downloaded_snapshot_path
180+
destination.parent.mkdir(parents=True, exist_ok=True)
181+
if uri == snapshot_uri:
182+
downloaded_snapshot_path = destination
183+
destination.write_text(
184+
"as_of,symbol,sector,mom_6_1\n2026-03-31,AAA,Information Technology,0.1\n",
185+
encoding="utf-8",
186+
)
187+
return
188+
if uri == manifest_uri:
189+
self.assertIsNotNone(downloaded_snapshot_path)
190+
destination.write_text(
191+
json.dumps(
192+
{
193+
"contract_version": "tech_pullback_cash_buffer.feature_snapshot.v1",
194+
"strategy_profile": "tech_pullback_cash_buffer",
195+
"config_name": "tech_pullback_cash_buffer",
196+
"config_path": str(config_path),
197+
"config_sha256": _sha256_file(config_path),
198+
"snapshot_path": snapshot_uri,
199+
"snapshot_sha256": _sha256_file(downloaded_snapshot_path),
200+
"snapshot_as_of": "2026-03-31",
201+
}
202+
),
203+
encoding="utf-8",
204+
)
205+
return
206+
raise AssertionError(f"unexpected uri: {uri}")
207+
208+
with patch("application.feature_snapshot_service._download_gcs_object", side_effect=fake_download):
209+
result = load_feature_snapshot_guarded(
210+
snapshot_uri,
211+
run_as_of="2026-04-01",
212+
required_columns=("as_of", "symbol", "sector", "mom_6_1"),
213+
require_manifest=True,
214+
expected_strategy_profile="tech_pullback_cash_buffer",
215+
expected_config_name="tech_pullback_cash_buffer",
216+
expected_config_path=str(config_path),
217+
expected_contract_version="tech_pullback_cash_buffer.feature_snapshot.v1",
218+
)
219+
220+
self.assertIsNotNone(result.frame)
221+
self.assertEqual(result.metadata["snapshot_guard_decision"], "proceed")
222+
self.assertEqual(
223+
result.metadata["feature_snapshot_path"],
224+
snapshot_uri,
225+
)
226+
self.assertEqual(result.metadata["snapshot_manifest_path"], manifest_uri)
227+
self.assertEqual(result.metadata["snapshot_source_uri"], snapshot_uri)
228+
self.assertEqual(result.metadata["snapshot_manifest_source_uri"], manifest_uri)
229+
148230

149231
if __name__ == "__main__":
150232
unittest.main()

0 commit comments

Comments
 (0)