| """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__) |
|
|
| |
| _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 |
|
|
| 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 |
|
|