scholarr/build/lib/app/services/domains/settings/application.py
2026-02-26 12:54:19 +01:00

159 lines
4.9 KiB
Python

from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models import UserSetting
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
from app.settings import settings as app_settings
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