Files
kite/backend/services/cloud_analysis_settings.py
T
curo1305 1df61040c6 feat(14-02): add version-key, settings helpers and Phase 14 cache service functions
- cloud_analysis_versioning.py: compute_version_key with version>etag>fingerprint
  precedence (D-19); compute_fingerprint helper; content_hash is optional post-
  hydration only (D-20); raises ValueError only, no HTTPException
- cloud_analysis_settings.py: get_or_create_user_analysis_settings (upsert on
  first access), update_user_analysis_settings with enum validation and tier-capped
  cache limit, build_default_settings for API responses; raises ValueError only
- cloud_cache.py: Phase 14 durable cache functions added alongside existing Phase 12
  in-memory TTL cache — create_cache_entry, list_cache_entries (owner-scoped),
  evict_lru_entries (LRU, skips pin_count>0 and active_job_count>0), increment/
  decrement_quota_for_cache (atomic UPDATE pattern); re-exports compute_version_key
  from cloud_analysis_versioning so tests have one import site
2026-06-23 15:14:19 +02:00

164 lines
6.2 KiB
Python

"""
Cloud analysis user settings helpers — Phase 14.
Provides get_or_create_user_analysis_settings and update_user_analysis_settings:
the single authority for reading and mutating per-user analysis preferences.
Rules:
- Service raises ValueError only — never HTTPException (CLAUDE.md).
- Default values are defined here; the router layer never hard-codes them.
- Tier/admin cache limit maximums are enforced as a seam: pass the admin
maximum via max_cache_limit_bytes to update_user_analysis_settings.
- Invalid enum values are rejected before touching the database.
"""
from __future__ import annotations
import uuid
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from db.models import UserAnalysisSettings
# ── Valid enum values ─────────────────────────────────────────────────────────
VALID_PROGRESS_DETAIL = {"simple", "detailed"}
VALID_FAILURE_BEHAVIOR = {"pause_batch", "continue_item"}
# Absolute floor for cache limit — prevents pathologically small values
MIN_CACHE_LIMIT_BYTES = 1 * 1024 * 1024 # 1 MB
# Default tier maximum — overridable by caller (admin/tier seam)
DEFAULT_MAX_CACHE_LIMIT_BYTES = 5 * 1024 * 1024 * 1024 # 5 GB
# Default per-user cache limit when row is created
DEFAULT_CACHE_LIMIT_BYTES = 512 * 1024 * 1024 # 512 MB
# Default enum values (mirrors migration server_defaults)
DEFAULT_PROGRESS_DETAIL = "simple"
DEFAULT_FAILURE_BEHAVIOR = "pause_batch"
# ── Public API ─────────────────────────────────────────────────────────────────
async def get_or_create_user_analysis_settings(
session: AsyncSession,
*,
user_id: uuid.UUID,
) -> UserAnalysisSettings:
"""Return the user's analysis settings row, creating defaults if absent.
Never raises — creates the row on first access so callers always receive
a valid settings object.
Args:
session: Active async SQLAlchemy session.
user_id: Authenticated user UUID.
Returns:
UserAnalysisSettings with current preferences or newly created defaults.
"""
uid = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id))
result = await session.execute(
select(UserAnalysisSettings).where(UserAnalysisSettings.user_id == uid)
)
existing = result.scalars().first()
if existing is not None:
return existing
# Create defaults on first access
settings_row = UserAnalysisSettings(
user_id=uid,
analysis_progress_detail=DEFAULT_PROGRESS_DETAIL,
analysis_failure_behavior=DEFAULT_FAILURE_BEHAVIOR,
cloud_cache_limit_bytes=DEFAULT_CACHE_LIMIT_BYTES,
)
session.add(settings_row)
await session.flush()
return settings_row
async def update_user_analysis_settings(
session: AsyncSession,
*,
user_id: uuid.UUID,
analysis_progress_detail: Optional[str] = None,
analysis_failure_behavior: Optional[str] = None,
cloud_cache_limit_bytes: Optional[int] = None,
max_cache_limit_bytes: int = DEFAULT_MAX_CACHE_LIMIT_BYTES,
) -> UserAnalysisSettings:
"""Update analysis preferences for the user.
Only provided (non-None) fields are updated. Enum values and cache limit
bounds are validated before the database is touched.
Args:
session: Active async SQLAlchemy session.
user_id: Authenticated user UUID.
analysis_progress_detail: "simple" | "detailed" (or None to leave unchanged).
analysis_failure_behavior: "pause_batch" | "continue_item" (or None).
cloud_cache_limit_bytes: User-preferred cache byte limit (or None).
max_cache_limit_bytes: Tier/admin maximum (default 5 GB). Caller
supplies this so the seam is explicit.
Returns:
Updated UserAnalysisSettings row.
Raises:
ValueError: Invalid enum value or cache limit out of bounds.
"""
if analysis_progress_detail is not None:
if analysis_progress_detail not in VALID_PROGRESS_DETAIL:
raise ValueError(
f"Invalid analysis_progress_detail {analysis_progress_detail!r}. "
f"Valid values: {VALID_PROGRESS_DETAIL}"
)
if analysis_failure_behavior is not None:
if analysis_failure_behavior not in VALID_FAILURE_BEHAVIOR:
raise ValueError(
f"Invalid analysis_failure_behavior {analysis_failure_behavior!r}. "
f"Valid values: {VALID_FAILURE_BEHAVIOR}"
)
if cloud_cache_limit_bytes is not None:
if cloud_cache_limit_bytes < MIN_CACHE_LIMIT_BYTES:
raise ValueError(
f"cloud_cache_limit_bytes must be at least {MIN_CACHE_LIMIT_BYTES} bytes, "
f"got {cloud_cache_limit_bytes}"
)
if cloud_cache_limit_bytes > max_cache_limit_bytes:
raise ValueError(
f"cloud_cache_limit_bytes {cloud_cache_limit_bytes} exceeds tier maximum "
f"{max_cache_limit_bytes}"
)
# Fetch or create the row (never fails)
row = await get_or_create_user_analysis_settings(session, user_id=user_id)
if analysis_progress_detail is not None:
row.analysis_progress_detail = analysis_progress_detail
if analysis_failure_behavior is not None:
row.analysis_failure_behavior = analysis_failure_behavior
if cloud_cache_limit_bytes is not None:
row.cloud_cache_limit_bytes = cloud_cache_limit_bytes
await session.flush()
return row
def build_default_settings() -> dict:
"""Return a dict of default settings values for use in API responses.
Used when no row exists yet and the caller needs a response without
committing a new row (e.g. GET /cache before any settings are saved).
"""
return {
"analysis_progress_detail": DEFAULT_PROGRESS_DETAIL,
"analysis_failure_behavior": DEFAULT_FAILURE_BEHAVIOR,
"cloud_cache_limit_bytes": DEFAULT_CACHE_LIMIT_BYTES,
}