58 lines
1.9 KiB
Python
58 lines
1.9 KiB
Python
"""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())
|