from __future__ import annotations from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import UserSetting from app.settings import settings as app_settings class UserSettingsServiceError(ValueError): """Raised for expected settings-validation failures.""" NAV_PAGE_DASHBOARD = "dashboard" NAV_PAGE_SCHOLARS = "scholars" NAV_PAGE_PUBLICATIONS = "publications" NAV_PAGE_SETTINGS = "settings" NAV_PAGE_STYLE_GUIDE = "style-guide" NAV_PAGE_RUNS = "runs" NAV_PAGE_USERS = "users" ALLOWED_NAV_PAGES = ( NAV_PAGE_DASHBOARD, NAV_PAGE_SCHOLARS, NAV_PAGE_PUBLICATIONS, NAV_PAGE_SETTINGS, NAV_PAGE_STYLE_GUIDE, NAV_PAGE_RUNS, NAV_PAGE_USERS, ) REQUIRED_NAV_PAGES = ( NAV_PAGE_DASHBOARD, NAV_PAGE_SCHOLARS, NAV_PAGE_SETTINGS, ) DEFAULT_NAV_VISIBLE_PAGES = list(ALLOWED_NAV_PAGES) HARD_MIN_RUN_INTERVAL_MINUTES = 15 HARD_MIN_REQUEST_DELAY_SECONDS = 2 def resolve_run_interval_minimum(configured_minimum: int | None) -> int: try: parsed = int(configured_minimum) if configured_minimum is not None else HARD_MIN_RUN_INTERVAL_MINUTES except (TypeError, ValueError): parsed = HARD_MIN_RUN_INTERVAL_MINUTES return max(HARD_MIN_RUN_INTERVAL_MINUTES, parsed) def resolve_request_delay_minimum(configured_minimum: int | None) -> int: try: parsed = int(configured_minimum) if configured_minimum is not None else HARD_MIN_REQUEST_DELAY_SECONDS except (TypeError, ValueError): parsed = HARD_MIN_REQUEST_DELAY_SECONDS return max(HARD_MIN_REQUEST_DELAY_SECONDS, parsed) def parse_run_interval_minutes(value: str, *, minimum: int = HARD_MIN_RUN_INTERVAL_MINUTES) -> int: try: parsed = int(value) except ValueError as exc: raise UserSettingsServiceError("Check interval must be a whole number.") from exc effective_minimum = resolve_run_interval_minimum(minimum) if parsed < effective_minimum: raise UserSettingsServiceError(f"Check interval must be at least {effective_minimum} minutes.") return parsed def parse_request_delay_seconds(value: str, *, minimum: int = HARD_MIN_REQUEST_DELAY_SECONDS) -> int: try: parsed = int(value) except ValueError as exc: raise UserSettingsServiceError("Request delay must be a whole number.") from exc effective_minimum = resolve_request_delay_minimum(minimum) if parsed < effective_minimum: raise UserSettingsServiceError(f"Request delay must be at least {effective_minimum} seconds.") return parsed def parse_nav_visible_pages(value: object) -> list[str]: if not isinstance(value, list): raise UserSettingsServiceError("Navigation visibility must be a list of page ids.") deduped: list[str] = [] seen: set[str] = set() for raw_page in value: if not isinstance(raw_page, str): raise UserSettingsServiceError("Navigation visibility entries must be strings.") page_id = raw_page.strip() if page_id not in ALLOWED_NAV_PAGES: raise UserSettingsServiceError(f"Unsupported navigation page id: {page_id}") if page_id in seen: continue seen.add(page_id) deduped.append(page_id) missing_required = [page for page in REQUIRED_NAV_PAGES if page not in seen] if missing_required: raise UserSettingsServiceError("Dashboard, Scholars, and Settings must remain visible.") return deduped async def get_or_create_settings( db_session: AsyncSession, *, user_id: int, ) -> UserSetting: result = await db_session.execute(select(UserSetting).where(UserSetting.user_id == user_id)) settings = result.scalar_one_or_none() if settings is not None: return settings settings = UserSetting( user_id=user_id, openalex_api_key=app_settings.openalex_api_key, crossref_api_token=app_settings.crossref_api_token, crossref_api_mailto=app_settings.crossref_api_mailto, ) db_session.add(settings) await db_session.commit() await db_session.refresh(settings) return settings async def update_settings( db_session: AsyncSession, *, settings: UserSetting, auto_run_enabled: bool, run_interval_minutes: int, request_delay_seconds: int, nav_visible_pages: list[str], openalex_api_key: str | None, crossref_api_token: str | None, crossref_api_mailto: str | None, ) -> UserSetting: settings.auto_run_enabled = auto_run_enabled settings.run_interval_minutes = run_interval_minutes settings.request_delay_seconds = request_delay_seconds settings.nav_visible_pages = nav_visible_pages settings.openalex_api_key = openalex_api_key settings.crossref_api_token = crossref_api_token settings.crossref_api_mailto = crossref_api_mailto await db_session.commit() await db_session.refresh(settings) return settings