"""Ticker Registry service: add, delete, and list tracked tickers.""" import re from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.exceptions import DuplicateError, NotFoundError, ValidationError from app.models.ticker import Ticker async def add_ticker(db: AsyncSession, symbol: str) -> Ticker: """Add a new ticker after validation. Validates: non-empty, uppercase alphanumeric. Auto-uppercases input. Raises DuplicateError if symbol already tracked. """ stripped = symbol.strip() if not stripped: raise ValidationError("Ticker symbol must not be empty or whitespace-only") normalised = stripped.upper() if not re.fullmatch(r"[A-Z0-9]+", normalised): raise ValidationError( f"Ticker symbol must be alphanumeric: {normalised}" ) result = await db.execute(select(Ticker).where(Ticker.symbol == normalised)) if result.scalar_one_or_none() is not None: raise DuplicateError(f"Ticker already exists: {normalised}") ticker = Ticker(symbol=normalised) db.add(ticker) await db.commit() await db.refresh(ticker) return ticker async def delete_ticker(db: AsyncSession, symbol: str) -> None: """Delete a ticker and cascade all associated data. Raises NotFoundError if the symbol is not tracked. """ normalised = symbol.strip().upper() result = await db.execute(select(Ticker).where(Ticker.symbol == normalised)) ticker = result.scalar_one_or_none() if ticker is None: raise NotFoundError(f"Ticker not found: {normalised}") await db.delete(ticker) await db.commit() async def list_tickers(db: AsyncSession) -> list[Ticker]: """Return all tracked tickers sorted alphabetically by symbol.""" result = await db.execute(select(Ticker).order_by(Ticker.symbol.asc())) return list(result.scalars().all())