|
| 1 | +"""Canonical point-in-time universe snapshots for historical research inputs. |
| 2 | +
|
| 3 | +The artifact binds a constituent set to its source content digest and the time |
| 4 | +at which that set became available. It is deliberately a local pure contract: |
| 5 | +it does not fetch a provider, select securities, write storage, or enable a |
| 6 | +strategy/runtime lane. |
| 7 | +""" |
| 8 | + |
| 9 | +from __future__ import annotations |
| 10 | + |
| 11 | +import hashlib |
| 12 | +import json |
| 13 | +import re |
| 14 | +from collections.abc import Iterable, Mapping |
| 15 | +from datetime import UTC, date, datetime |
| 16 | +from typing import Any |
| 17 | + |
| 18 | +POINT_IN_TIME_UNIVERSE_SCHEMA = "qsl.us-equity-point-in-time-universe.v1" |
| 19 | +_DIGEST = re.compile(r"^[0-9a-f]{64}$") |
| 20 | +_IDENTIFIER = re.compile(r"^[a-z0-9][a-z0-9_.-]{0,127}$") |
| 21 | +_SYMBOL = re.compile(r"^[A-Z][A-Z0-9.-]{0,14}$") |
| 22 | +_TIMESTAMP = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$") |
| 23 | +_ROOT_FIELDS = frozenset( |
| 24 | + { |
| 25 | + "schema_version", |
| 26 | + "universe_id", |
| 27 | + "effective_date", |
| 28 | + "available_at", |
| 29 | + "source", |
| 30 | + "constituents", |
| 31 | + "snapshot_sha256", |
| 32 | + } |
| 33 | +) |
| 34 | +_SOURCE_FIELDS = frozenset({"source_id", "raw_artifact_sha256", "license_scope"}) |
| 35 | + |
| 36 | + |
| 37 | +class PointInTimeUniverseError(ValueError): |
| 38 | + """Raised when a universe snapshot cannot prove no-look-ahead eligibility.""" |
| 39 | + |
| 40 | + |
| 41 | +def _fail(message: str) -> None: |
| 42 | + raise PointInTimeUniverseError(message) |
| 43 | + |
| 44 | + |
| 45 | +def _identifier(value: object, label: str) -> str: |
| 46 | + if not isinstance(value, str) or not _IDENTIFIER.fullmatch(value): |
| 47 | + _fail(f"invalid {label}") |
| 48 | + return value |
| 49 | + |
| 50 | + |
| 51 | +def _digest(value: object, label: str) -> str: |
| 52 | + if not isinstance(value, str) or not _DIGEST.fullmatch(value): |
| 53 | + _fail(f"invalid {label}") |
| 54 | + return value |
| 55 | + |
| 56 | + |
| 57 | +def _date(value: object, label: str) -> str: |
| 58 | + if not isinstance(value, str): |
| 59 | + _fail(f"invalid {label}") |
| 60 | + try: |
| 61 | + return date.fromisoformat(value).isoformat() |
| 62 | + except ValueError as exc: |
| 63 | + raise PointInTimeUniverseError(f"invalid {label}") from exc |
| 64 | + |
| 65 | + |
| 66 | +def _timestamp(value: object, label: str) -> datetime: |
| 67 | + if not isinstance(value, str) or not _TIMESTAMP.fullmatch(value): |
| 68 | + _fail(f"invalid {label}") |
| 69 | + try: |
| 70 | + return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC) |
| 71 | + except ValueError as exc: |
| 72 | + raise PointInTimeUniverseError(f"invalid {label}") from exc |
| 73 | + |
| 74 | + |
| 75 | +def _constituents(value: object) -> tuple[str, ...]: |
| 76 | + if not isinstance(value, Iterable) or isinstance(value, (str, bytes, Mapping)): |
| 77 | + _fail("invalid constituents") |
| 78 | + items = tuple(value) |
| 79 | + if not items: |
| 80 | + _fail("empty constituents") |
| 81 | + normalized: list[str] = [] |
| 82 | + for symbol in items: |
| 83 | + if not isinstance(symbol, str) or not _SYMBOL.fullmatch(symbol): |
| 84 | + _fail("invalid constituent symbol") |
| 85 | + normalized.append(symbol) |
| 86 | + if len(set(normalized)) != len(normalized): |
| 87 | + _fail("duplicate constituent symbol") |
| 88 | + return tuple(sorted(normalized)) |
| 89 | + |
| 90 | + |
| 91 | +def _source(value: object) -> dict[str, str]: |
| 92 | + if not isinstance(value, Mapping) or set(value) != _SOURCE_FIELDS: |
| 93 | + _fail("invalid universe source") |
| 94 | + license_scope = value["license_scope"] |
| 95 | + if not isinstance(license_scope, str) or not license_scope or license_scope != license_scope.strip(): |
| 96 | + _fail("invalid universe license scope") |
| 97 | + return { |
| 98 | + "source_id": _identifier(value["source_id"], "universe source id"), |
| 99 | + "raw_artifact_sha256": _digest(value["raw_artifact_sha256"], "universe raw artifact digest"), |
| 100 | + "license_scope": license_scope, |
| 101 | + } |
| 102 | + |
| 103 | + |
| 104 | +def _canonical_json(value: Mapping[str, Any], *, without_digest: bool) -> bytes: |
| 105 | + material = dict(value) |
| 106 | + if without_digest: |
| 107 | + material.pop("snapshot_sha256", None) |
| 108 | + try: |
| 109 | + return json.dumps( |
| 110 | + material, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False |
| 111 | + ).encode("utf-8") |
| 112 | + except (TypeError, ValueError) as exc: |
| 113 | + raise PointInTimeUniverseError("invalid universe snapshot") from exc |
| 114 | + |
| 115 | + |
| 116 | +def calculate_point_in_time_universe_sha256(value: Mapping[str, Any]) -> str: |
| 117 | + """Return the self-digest for one exact point-in-time universe snapshot.""" |
| 118 | + return hashlib.sha256(_canonical_json(value, without_digest=True)).hexdigest() |
| 119 | + |
| 120 | + |
| 121 | +def build_point_in_time_universe_snapshot( |
| 122 | + *, |
| 123 | + universe_id: object, |
| 124 | + effective_date: object, |
| 125 | + available_at: object, |
| 126 | + source_id: object, |
| 127 | + raw_artifact_sha256: object, |
| 128 | + license_scope: object, |
| 129 | + constituents: object, |
| 130 | +) -> dict[str, object]: |
| 131 | + """Build a canonical local snapshot without making any provider request.""" |
| 132 | + result: dict[str, object] = { |
| 133 | + "schema_version": POINT_IN_TIME_UNIVERSE_SCHEMA, |
| 134 | + "universe_id": _identifier(universe_id, "universe id"), |
| 135 | + "effective_date": _date(effective_date, "effective date"), |
| 136 | + "available_at": _timestamp(available_at, "available timestamp").strftime( |
| 137 | + "%Y-%m-%dT%H:%M:%SZ" |
| 138 | + ), |
| 139 | + "source": _source( |
| 140 | + { |
| 141 | + "source_id": source_id, |
| 142 | + "raw_artifact_sha256": raw_artifact_sha256, |
| 143 | + "license_scope": license_scope, |
| 144 | + } |
| 145 | + ), |
| 146 | + "constituents": list(_constituents(constituents)), |
| 147 | + "snapshot_sha256": "", |
| 148 | + } |
| 149 | + result["snapshot_sha256"] = calculate_point_in_time_universe_sha256(result) |
| 150 | + return validate_point_in_time_universe_snapshot(result) |
| 151 | + |
| 152 | + |
| 153 | +def validate_point_in_time_universe_snapshot(value: object) -> dict[str, object]: |
| 154 | + """Validate one exact artifact and its constituent/source binding.""" |
| 155 | + if not isinstance(value, Mapping) or set(value) != _ROOT_FIELDS: |
| 156 | + _fail("invalid universe snapshot") |
| 157 | + if value["schema_version"] != POINT_IN_TIME_UNIVERSE_SCHEMA: |
| 158 | + _fail("invalid universe snapshot schema") |
| 159 | + normalized: dict[str, object] = { |
| 160 | + "schema_version": POINT_IN_TIME_UNIVERSE_SCHEMA, |
| 161 | + "universe_id": _identifier(value["universe_id"], "universe id"), |
| 162 | + "effective_date": _date(value["effective_date"], "effective date"), |
| 163 | + "available_at": _timestamp(value["available_at"], "available timestamp").strftime( |
| 164 | + "%Y-%m-%dT%H:%M:%SZ" |
| 165 | + ), |
| 166 | + "source": _source(value["source"]), |
| 167 | + "constituents": list(_constituents(value["constituents"])), |
| 168 | + "snapshot_sha256": _digest(value["snapshot_sha256"], "universe snapshot digest"), |
| 169 | + } |
| 170 | + if normalized["snapshot_sha256"] != calculate_point_in_time_universe_sha256(normalized): |
| 171 | + _fail("universe snapshot digest mismatch") |
| 172 | + return normalized |
| 173 | + |
| 174 | + |
| 175 | +def validate_universe_snapshot_for_decision( |
| 176 | + value: object, *, decision_at: object |
| 177 | +) -> dict[str, object]: |
| 178 | + """Require that a snapshot was available no later than the decision time.""" |
| 179 | + snapshot = validate_point_in_time_universe_snapshot(value) |
| 180 | + available_at = _timestamp(snapshot["available_at"], "available timestamp") |
| 181 | + decision = _timestamp(decision_at, "decision timestamp") |
| 182 | + if available_at > decision: |
| 183 | + _fail("universe snapshot was unavailable at decision time") |
| 184 | + return snapshot |
| 185 | + |
| 186 | + |
| 187 | +__all__ = [ |
| 188 | + "POINT_IN_TIME_UNIVERSE_SCHEMA", |
| 189 | + "PointInTimeUniverseError", |
| 190 | + "build_point_in_time_universe_snapshot", |
| 191 | + "calculate_point_in_time_universe_sha256", |
| 192 | + "validate_point_in_time_universe_snapshot", |
| 193 | + "validate_universe_snapshot_for_decision", |
| 194 | +] |
0 commit comments