DenseFeed / src /densefeed /web /auth.py
j14i's picture
fix frontend auth status bootstrap
20b40d8 verified
Raw
History Blame Contribute Delete
7.73 kB
"""Web auth helpers — token resolution, username lookup, DB hydration."""
from __future__ import annotations
import logging
import threading
import gradio as gr
from densefeed.auth import hf_token, install_token
from densefeed.config import DenseFeedConfig, load_config
from densefeed.storage.duckdb_backend import DuckDBBackend
from densefeed.storage import sync as storage_sync
logger = logging.getLogger(__name__)
# Shared mutable state — initialized once in create_ui(), accessed via get_config/get_backend
_backend: DuckDBBackend | None = None
_config: DenseFeedConfig | None = None
_config_lock = threading.Lock()
_db_loaded_for_repo: str | None = None
def default_user_repos(username: str) -> tuple[str, str]:
"""Return default private config and public audio dataset repo IDs for a user."""
import os
app_name = (os.environ.get("SPACE_ID", "DenseFeed").split("/")[-1] or "DenseFeed")
slug = app_name.lower().replace("_", "-")
return f"{username}/{slug}-data", f"{username}/{slug}-audio"
def set_storage_repos(hf_dataset_repo: str, hf_audio_repo: str) -> None:
"""Update shared storage config and force the next backend access to hydrate it."""
global _backend, _db_loaded_for_repo
cfg = get_config()
changed = (
cfg.storage.hf_dataset_repo != hf_dataset_repo
or cfg.storage.hf_audio_repo != hf_audio_repo
)
cfg.storage.hf_dataset_repo = hf_dataset_repo
cfg.storage.hf_audio_repo = hf_audio_repo
if changed:
if _backend is not None:
_backend.close()
_backend = None
_db_loaded_for_repo = None
def space_default_config(cfg: DenseFeedConfig) -> DenseFeedConfig:
"""Apply production defaults when running on Hugging Face Spaces."""
import os
if os.environ.get("SPACE_ID"):
cfg.storage.backend = "duckdb"
try:
import spaces # noqa: F401
cfg.llm.backend = "zerogpu"
logger.info("[config] ZeroGPU detected — backend=zerogpu")
except ImportError:
logger.info(
"[config] SPACE_ID but no ZeroGPU — backend=%s",
cfg.llm.backend,
)
return cfg
def get_config() -> DenseFeedConfig:
"""Return the module-level config, loading fresh only if not yet initialized."""
global _config
with _config_lock:
if _config is None:
_config = space_default_config(load_config())
return _config
def get_config_copy() -> DenseFeedConfig:
"""Return a deep copy of the module-level config for per-request mutation."""
return get_config().model_copy(deep=True)
def get_backend() -> DuckDBBackend:
"""Lazily initialize and return the DuckDB backend."""
global _backend, _config, _db_loaded_for_repo
with _config_lock:
if _backend is None:
cfg = space_default_config(_config or load_config())
_config = cfg
if (
cfg.storage.backend == "duckdb"
and cfg.storage.hf_dataset_repo
and hf_token.get()
and _db_loaded_for_repo != cfg.storage.hf_dataset_repo
):
storage_sync.download_db(
cfg.storage.hf_dataset_repo, cfg.storage.duckdb_path
)
_backend = DuckDBBackend(
cfg.storage.duckdb_path, hf_audio_repo=cfg.storage.hf_audio_repo
)
if cfg.storage.hf_dataset_repo:
_db_loaded_for_repo = cfg.storage.hf_dataset_repo
return _backend
def set_initial_state(config: DenseFeedConfig, backend: DuckDBBackend | None) -> None:
"""Set the module-level state during app startup."""
global _backend, _config
_config = config
_backend = backend
def init_backend() -> DuckDBBackend:
"""Create a backend from current config without hydration (for startup)."""
cfg = get_config()
return DuckDBBackend(
cfg.storage.duckdb_path, hf_audio_repo=cfg.storage.hf_audio_repo
)
def request_bearer_token(request: gr.Request | None) -> str | None:
"""Return an HF token from API Authorization headers, if present."""
if request is None:
return None
auth = request.headers.get("authorization") or request.headers.get("Authorization")
if not auth:
return None
scheme, _, value = auth.partition(" ")
if scheme.lower() == "bearer" and value:
return value
return None
def active_token(
token: gr.OAuthToken | None = None, request: gr.Request | None = None
) -> str | None:
"""Return the current HF token.
Browser users authenticate via Space OAuth. API/debug callers can provide an
explicit Authorization bearer token, which must take precedence because
Gradio may still inject a Space OAuth token with narrower scopes.
"""
return request_bearer_token(request) or (token.token if token else None)
def install_active_token(token_str: str) -> None:
"""Install the session token for all code paths that need HF auth.
Delegates to auth.install_token for the ContextVar + env write,
then triggers DuckDB hydration so the backend is ready for use.
"""
install_token(token_str)
ensure_remote_db_loaded()
def ensure_remote_db_loaded() -> None:
"""Download the persisted DuckDB once auth is available, even after startup."""
global _backend, _db_loaded_for_repo
cfg = get_config()
if cfg.storage.backend != "duckdb" or not cfg.storage.hf_dataset_repo:
return
token = hf_token.get()
if not token:
logger.warning("[sync] authenticated DB load skipped: no token")
return
if _db_loaded_for_repo == cfg.storage.hf_dataset_repo:
return
with _config_lock:
if _db_loaded_for_repo == cfg.storage.hf_dataset_repo:
return
downloaded = storage_sync.download_db(
cfg.storage.hf_dataset_repo, cfg.storage.duckdb_path
)
if _backend is not None:
_backend.close()
_backend = DuckDBBackend(
cfg.storage.duckdb_path, hf_audio_repo=cfg.storage.hf_audio_repo
)
_db_loaded_for_repo = cfg.storage.hf_dataset_repo
logger.info(
"[sync] DB hydration complete repo=%s downloaded=%s path=%s",
cfg.storage.hf_dataset_repo,
downloaded,
cfg.storage.duckdb_path,
)
def get_username(
token: gr.OAuthToken | None = None, request: gr.Request | None = None
) -> str:
"""Safely retrieve HF username from OAuthToken or API bearer token."""
if token and hasattr(token, "username") and token.username:
return token.username
tok = active_token(token, request)
if not tok:
return ""
try:
from huggingface_hub import HfApi
api = HfApi(token=tok)
return api.whoami().get("name", "")
except Exception:
return ""
def merge_prefs(cfg: DenseFeedConfig, prefs: dict) -> None:
"""Merge user preferences into pipeline config."""
import json
cfg.pipeline.min_relevance = int(
prefs.get("min_relevance", cfg.pipeline.min_relevance)
)
cfg.pipeline.top_n_items = int(prefs.get("top_n_items", cfg.pipeline.top_n_items))
cfg.storage.hf_dataset_repo = prefs.get(
"hf_dataset_repo", cfg.storage.hf_dataset_repo
)
cfg.storage.hf_audio_repo = prefs.get("hf_audio_repo", cfg.storage.hf_audio_repo)
excluded = prefs.get("excluded_sources", [])
if isinstance(excluded, str):
excluded = json.loads(excluded)
for src in excluded:
attr = getattr(cfg.sources, src, None)
if attr and hasattr(attr, "enabled"):
attr.enabled = False