Skip to content

Commit 04ea4ac

Browse files
authored
Apply audit remediation
Apply audit remediation from the 2026-06-10 review.
1 parent 023641c commit 04ea4ac

5 files changed

Lines changed: 130 additions & 27 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@ on:
55
branches: [ main ]
66
pull_request:
77

8+
permissions:
9+
contents: read
10+
811
jobs:
912
test:
1013
runs-on: ubuntu-latest
14+
timeout-minutes: 20
1115
steps:
1216
- name: Checkout
1317
uses: actions/checkout@v6

.github/workflows/dependabot_auto_merge.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ jobs:
99
auto-merge:
1010
if: github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_branch, 'dependabot/')
1111
runs-on: ubuntu-latest
12+
timeout-minutes: 10
1213
permissions:
1314
contents: write
1415
pull-requests: write
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""Artifact path helpers for strategy plugin signal files."""
2+
3+
from __future__ import annotations
4+
5+
import hashlib
6+
from pathlib import Path
7+
from typing import Any
8+
9+
10+
def materialize_local_or_gcs_artifact(
11+
reference: str,
12+
*,
13+
cache_dir: Path,
14+
client_factory: Any = None,
15+
) -> tuple[Path, dict[str, str | None]]:
16+
raw_reference = _required_string(reference, field_name="reference")
17+
if not raw_reference.startswith("gs://"):
18+
return Path(raw_reference).expanduser(), {"source_uri": None, "local_path": raw_reference}
19+
20+
local_path = cache_path_for_remote_artifact(raw_reference, cache_dir=cache_dir)
21+
download_gcs_object(raw_reference, local_path, client_factory=client_factory)
22+
return local_path, {"source_uri": raw_reference, "local_path": str(local_path)}
23+
24+
25+
def download_gcs_object(uri: str, destination: Path, *, client_factory: Any = None) -> None:
26+
if client_factory is None:
27+
try:
28+
from google.cloud import storage # type: ignore
29+
except ImportError as exc:
30+
raise RuntimeError("google-cloud-storage is required for GCS strategy plugin artifacts") from exc
31+
client_factory = storage.Client
32+
bucket_name, object_name = parse_gcs_uri(uri)
33+
destination.parent.mkdir(parents=True, exist_ok=True)
34+
client = client_factory()
35+
client.bucket(bucket_name).blob(object_name).download_to_filename(str(destination))
36+
37+
38+
def parse_gcs_uri(uri: str) -> tuple[str, str]:
39+
raw_uri = str(uri or "").strip()
40+
if not raw_uri.startswith("gs://"):
41+
raise ValueError(f"Unsupported GCS URI: {raw_uri}")
42+
bucket_name, _, object_name = raw_uri[5:].partition("/")
43+
if not bucket_name or not object_name:
44+
raise ValueError(f"Invalid GCS URI: {raw_uri}")
45+
return bucket_name, object_name
46+
47+
48+
def cache_path_for_remote_artifact(reference: str, *, cache_dir: Path) -> Path:
49+
digest = hashlib.sha256(reference.encode("utf-8")).hexdigest()[:16]
50+
leaf_name = Path(reference).name or "latest_signal.json"
51+
return cache_dir / digest / leaf_name
52+
53+
54+
def _required_string(value: Any, *, field_name: str) -> str:
55+
text = str(value or "").strip()
56+
if not text:
57+
raise ValueError(f"{field_name} must be a non-empty string")
58+
return text

src/quant_platform_kit/common/strategy_plugins.py

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
from pathlib import Path
1212
from typing import Any, Callable
1313

14+
from quant_platform_kit.common.strategy_plugin_artifacts import (
15+
cache_path_for_remote_artifact,
16+
download_gcs_object,
17+
materialize_local_or_gcs_artifact,
18+
parse_gcs_uri,
19+
)
20+
1421
PLUGIN_CRISIS_RESPONSE_SHADOW = "crisis_response_shadow"
1522
PLUGIN_MARKET_REGIME_CONTROL = "market_regime_control"
1623
PLUGIN_MACRO_RISK_GOVERNOR = "macro_risk_governor"
@@ -1237,42 +1244,23 @@ def _sanitize_key_part(value: Any) -> str:
12371244

12381245

12391246
def _materialize_artifact_path(reference: str, *, client_factory: Any = None) -> tuple[Path, dict[str, str | None]]:
1240-
raw_reference = _required_string(reference, field_name="reference")
1241-
if not raw_reference.startswith("gs://"):
1242-
return Path(raw_reference).expanduser(), {"source_uri": None, "local_path": raw_reference}
1243-
1244-
local_path = _cache_path_for_remote_artifact(raw_reference)
1245-
_download_gcs_object(raw_reference, local_path, client_factory=client_factory)
1246-
return local_path, {"source_uri": raw_reference, "local_path": str(local_path)}
1247+
return materialize_local_or_gcs_artifact(
1248+
reference,
1249+
cache_dir=DEFAULT_PLUGIN_ARTIFACT_CACHE_DIR,
1250+
client_factory=client_factory,
1251+
)
12471252

12481253

12491254
def _download_gcs_object(uri: str, destination: Path, *, client_factory: Any = None) -> None:
1250-
if client_factory is None:
1251-
try:
1252-
from google.cloud import storage # type: ignore
1253-
except ImportError as exc:
1254-
raise RuntimeError("google-cloud-storage is required for GCS strategy plugin artifacts") from exc
1255-
client_factory = storage.Client
1256-
bucket_name, object_name = _parse_gcs_uri(uri)
1257-
destination.parent.mkdir(parents=True, exist_ok=True)
1258-
client = client_factory()
1259-
client.bucket(bucket_name).blob(object_name).download_to_filename(str(destination))
1255+
download_gcs_object(uri, destination, client_factory=client_factory)
12601256

12611257

12621258
def _parse_gcs_uri(uri: str) -> tuple[str, str]:
1263-
raw_uri = str(uri or "").strip()
1264-
if not raw_uri.startswith("gs://"):
1265-
raise ValueError(f"Unsupported GCS URI: {raw_uri}")
1266-
bucket_name, _, object_name = raw_uri[5:].partition("/")
1267-
if not bucket_name or not object_name:
1268-
raise ValueError(f"Invalid GCS URI: {raw_uri}")
1269-
return bucket_name, object_name
1259+
return parse_gcs_uri(uri)
12701260

12711261

12721262
def _cache_path_for_remote_artifact(reference: str) -> Path:
1273-
digest = hashlib.sha256(reference.encode("utf-8")).hexdigest()[:16]
1274-
leaf_name = Path(reference).name or "latest_signal.json"
1275-
return DEFAULT_PLUGIN_ARTIFACT_CACHE_DIR / digest / leaf_name
1263+
return cache_path_for_remote_artifact(reference, cache_dir=DEFAULT_PLUGIN_ARTIFACT_CACHE_DIR)
12761264

12771265

12781266
def _as_bool(value: Any, *, default: bool = False) -> bool:
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from quant_platform_kit.common.strategy_plugin_artifacts import (
6+
cache_path_for_remote_artifact,
7+
materialize_local_or_gcs_artifact,
8+
parse_gcs_uri,
9+
)
10+
11+
12+
def test_parse_gcs_uri_requires_bucket_and_object():
13+
assert parse_gcs_uri("gs://bucket/path/latest_signal.json") == (
14+
"bucket",
15+
"path/latest_signal.json",
16+
)
17+
18+
with pytest.raises(ValueError, match="Invalid GCS URI"):
19+
parse_gcs_uri("gs://bucket")
20+
21+
with pytest.raises(ValueError, match="Unsupported GCS URI"):
22+
parse_gcs_uri("https://example.com/latest_signal.json")
23+
24+
25+
def test_cache_path_for_remote_artifact_is_stable_under_cache_dir():
26+
cache_dir = Path("/tmp/cache")
27+
first = cache_path_for_remote_artifact(
28+
"gs://bucket/path/latest_signal.json",
29+
cache_dir=cache_dir,
30+
)
31+
second = cache_path_for_remote_artifact(
32+
"gs://bucket/path/latest_signal.json",
33+
cache_dir=cache_dir,
34+
)
35+
36+
assert first == second
37+
assert first.parent.parent == cache_dir
38+
assert first.name == "latest_signal.json"
39+
40+
41+
def test_materialize_local_artifact_does_not_download():
42+
local_path, metadata = materialize_local_or_gcs_artifact(
43+
"~/signals/latest_signal.json",
44+
cache_dir=Path("/tmp/cache"),
45+
client_factory=lambda: (_ for _ in ()).throw(AssertionError("should not download")),
46+
)
47+
48+
assert local_path == Path("~/signals/latest_signal.json").expanduser()
49+
assert metadata == {
50+
"source_uri": None,
51+
"local_path": "~/signals/latest_signal.json",
52+
}

0 commit comments

Comments
 (0)