Spaces:
Running
Running
File size: 9,340 Bytes
1916d8e 8754770 1916d8e 8754770 1916d8e 8754770 1916d8e 8754770 1916d8e 8754770 1916d8e 8754770 1916d8e 8754770 1916d8e 8754770 1916d8e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | """
khazna_loader.py β Space-side loading of the Khazna index.
The harvest runs in GitHub Actions (nightly, 23:00 Dubai) and publishes
khazna_index.json to an HF Dataset repo. The Space downloads it β it never
harvests, because HF Spaces have an ephemeral filesystem and a 20-minute
harvest would block startup.
Behaviour:
* startup -> download once, load into memory (non-blocking)
* daily 23:30 -> re-download (a few seconds)
* unavailable -> keep the last valid snapshot, or say "not checked" when
none is loaded; a load time is not a source harvest date.
Add to requirements.txt:
huggingface_hub
apscheduler
"""
from __future__ import annotations
import json
import logging
import os
import re
import threading
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from urllib.parse import urlsplit
logger = logging.getLogger("khazna")
HF_DATASET = os.getenv("KHAZNA_DATASET", "nikeshn/researchbee-data")
INDEX_FILE = "khazna_index.json"
LOCAL_PATH = Path("/tmp/khazna_index.json")
_DOI_RE = re.compile(r"^10\.\d{4,9}/\S+$", re.ASCII)
_DEPOSIT_STATES = {"open", "embargoed", "restricted", "closed", "metadata_only", "undetermined"}
def _normalise_doi(doi: str) -> str:
d = doi.strip().lower()
for prefix in ("https://doi.org/", "http://doi.org/", "https://dx.doi.org/", "http://dx.doi.org/", "doi:"):
if d.startswith(prefix):
d = d[len(prefix):]
break
return d.strip().strip("/")
def _validate_snapshot(data: object) -> dict[str, dict]:
"""Validate the existing flat source format before replacing a good index."""
if not isinstance(data, dict) or not data:
raise ValueError("empty_or_invalid_map")
validated: dict[str, dict] = {}
for key, value in data.items():
if not isinstance(key, str) or not isinstance(value, dict):
raise ValueError("invalid_record")
doi = _normalise_doi(key)
if not _DOI_RE.fullmatch(doi) or doi in validated:
raise ValueError("invalid_or_duplicate_doi")
record = dict(value)
for field in ("deposit_state", "embargo_end", "portal_url"):
if record.get(field) is not None and not isinstance(record[field], str):
raise ValueError("invalid_record_field")
state = (record.get("deposit_state") or "undetermined").strip().lower()
record["deposit_state"] = state if state in _DEPOSIT_STATES else "undetermined"
portal = (record.get("portal_url") or "").strip()
if portal:
parsed = urlsplit(portal)
if parsed.scheme not in {"https", "http"} or not parsed.hostname or parsed.username or parsed.password:
raise ValueError("invalid_portal_url")
record["portal_url"] = portal
validated[doi] = record
return validated
class KhaznaIndex:
def __init__(self) -> None:
self._idx: dict[str, dict] = {}
self._loaded_at: Optional[str] = None
self._last_reload_at: Optional[str] = None
self._last_reload_success: Optional[bool] = None
self._last_reload_status = "not_attempted"
self._load_source: Optional[str] = None
self._download_origin = "unknown"
self._reload_lock = threading.Lock()
# ββ loading ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _download(self) -> Optional[Path]:
try:
from huggingface_hub import hf_hub_download
p = hf_hub_download(
repo_id=HF_DATASET,
filename=INDEX_FILE,
repo_type="dataset",
token=os.getenv("HF_TOKEN"), # omit if the dataset is public
)
self._download_origin = "huggingface_download_or_cache"
return Path(p)
except Exception:
# SDK/network exception messages can contain tokens, signed URLs
# or internal paths. Keep logs and public status categorical.
logger.warning("Khazna index download unavailable; trying local fallback")
self._download_origin = "local_fallback"
return LOCAL_PATH if LOCAL_PATH.exists() else None
def reload(self) -> bool:
with self._reload_lock:
self._last_reload_at = datetime.now(timezone.utc).isoformat(timespec="seconds")
self._last_reload_success = False
self._download_origin = "unknown"
try:
path = self._download()
if not path or not path.exists():
self._last_reload_status = "unavailable"
logger.warning("Khazna index unavailable; retaining any previously loaded snapshot")
return False
data = json.loads(path.read_text(encoding="utf-8"))
validated = _validate_snapshot(data)
except (ValueError, UnicodeError):
self._last_reload_status = "invalid_snapshot"
logger.error("Khazna index invalid; retaining any previously loaded snapshot")
return False
except Exception:
self._last_reload_status = "load_error"
logger.error("Khazna index could not be loaded; retaining any previously loaded snapshot")
return False
self._idx = validated
self._loaded_at = datetime.now(timezone.utc).isoformat(timespec="seconds")
self._last_reload_success = True
self._last_reload_status = "loaded"
self._load_source = self._download_origin
logger.info("Khazna index loaded: %d DOIs; source harvest date unknown", len(self._idx))
return True
# ββ lookup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get(self, doi: str) -> Optional[dict]:
if not isinstance(doi, str):
return None
return self._idx.get(_normalise_doi(doi))
@property
def available(self) -> bool:
return bool(self._idx)
@property
def status(self) -> dict:
return {"available": self.available,
"records": len(self._idx),
"loaded_at": self._loaded_at,
"source": HF_DATASET,
"last_reload_at": self._last_reload_at,
"last_reload_success": self._last_reload_success,
"last_reload_status": self._last_reload_status,
"load_source": self._load_source,
"retaining_previous_snapshot": self.available and self._last_reload_success is False,
# The existing DOI map contains no harvest timestamp. Neither
# loading a cache nor downloading establishes source freshness.
"source_harvested_at": None,
"snapshot_freshness": "unknown",
"freshness_note": "Source harvest date is not supplied. loaded_at records loading only; an absent DOI is not proof of current repository absence."}
khazna_index = KhaznaIndex()
# ββ FastAPI wiring (app.py) ββββββββββββββββββββββββββββββββββββββββββββββββ
#
# from khazna_loader import khazna_index
# from apscheduler.schedulers.asyncio import AsyncIOScheduler
# from apscheduler.triggers.cron import CronTrigger
#
# @app.on_event("startup")
# async def _startup():
# # Non-blocking: the Space serves traffic immediately; Khazna lookups
# # report "not checked" for the few seconds before the index lands.
# asyncio.create_task(asyncio.to_thread(khazna_index.reload))
#
# sched = AsyncIOScheduler(timezone="Asia/Dubai")
# # AsyncIOScheduler runs this synchronous job in its executor; do not call
# # asyncio.create_task from a scheduler worker with no running event loop.
# sched.add_job(khazna_index.reload,
# CronTrigger(hour=23, minute=30, timezone="Asia/Dubai"))
# sched.start()
#
# @app.get("/api/health/khazna")
# async def khazna_health():
# return khazna_index.status
#
#
# ββ Using it in the license flow βββββββββββββββββββββββββββββββββββββββββββ
#
# def khazna_status_for(doi: str) -> dict:
# """Three-valued, never guesses absence when the index is missing."""
# if not khazna_index.available:
# return {"checked": False,
# "note": "Khazna could not be checked right now."}
# rec = khazna_index.get(doi)
# if rec is None:
# return {"checked": True, "in_khazna": False}
# return {"checked": True, "in_khazna": True,
# "deposit_state": rec["deposit_state"], # open|embargoed|...|metadata_only
# "embargo_end": rec.get("embargo_end"),
# "portal_url": rec.get("portal_url"),
# "needs_deposit": rec["deposit_state"] in ("metadata_only", "closed")}
|