"""Versioned strategy catalog APIs.""" from __future__ import annotations import json from fastapi import APIRouter, Depends, HTTPException, Response, status from app.core.auth import AuthContext, require_admin, require_read from app.core.config import Settings, get_settings from app.domain.models import StrategySet from app.repositories.factory import get_portfolio_repository_from_settings from app.schemas.portfolio import CreateStrategySetRequest, UpdateStrategySetRequest from app.schemas.v1 import StrategyCreateRequest, StrategyResponse, StrategyUpdateRequest from app.services.strategy_set_service import ( StrategyConflictError, StrategyInactiveError, StrategyInUseError, StrategyNotFoundError, StrategySetService, ) router = APIRouter(tags=["v1-strategies"]) def _get_service(settings: Settings) -> tuple[StrategySetService, object]: repository = get_portfolio_repository_from_settings(settings) return StrategySetService(repository), repository def _close_repository(repository: object) -> None: if callable(getattr(type(repository), "close", None)): repository.close() def _canonical_config(config: dict[str, object]) -> str: return json.dumps(config, sort_keys=True, ensure_ascii=True, separators=(",", ":")) def _response(strategy: StrategySet) -> StrategyResponse: try: parsed = json.loads(strategy.config) if strategy.config else {} except (TypeError, ValueError, json.JSONDecodeError): parsed = {} return StrategyResponse( strategy_id=strategy.id, name=strategy.name, config=parsed if isinstance(parsed, dict) else {}, status=strategy.status, revision=strategy.revision, created_at=strategy.created_at, updated_at=strategy.updated_at, ) def _raise_strategy_error(exc: ValueError) -> None: if isinstance(exc, StrategyNotFoundError): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc if isinstance(exc, (StrategyInactiveError, StrategyInUseError, StrategyConflictError)): raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc raise exc @router.get("/v1/strategies", response_model=list[StrategyResponse]) def list_active_strategies( _auth: AuthContext = Depends(require_read), settings: Settings = Depends(get_settings), ) -> list[StrategyResponse]: service, repository = _get_service(settings) try: return [_response(item) for item in service.list(include_inactive=False)] finally: _close_repository(repository) @router.get("/v1/strategies/{strategy_id}", response_model=StrategyResponse) def get_active_strategy( strategy_id: str, _auth: AuthContext = Depends(require_read), settings: Settings = Depends(get_settings), ) -> StrategyResponse: service, repository = _get_service(settings) try: try: return _response(service.get(strategy_id, active_only=True)) except ValueError as exc: _raise_strategy_error(exc) finally: _close_repository(repository) @router.get("/v1/admin/strategies", response_model=list[StrategyResponse]) def list_admin_strategies( _auth: AuthContext = Depends(require_admin), settings: Settings = Depends(get_settings), ) -> list[StrategyResponse]: service, repository = _get_service(settings) try: return [_response(item) for item in service.list(include_inactive=True)] finally: _close_repository(repository) @router.get("/v1/admin/strategies/{strategy_id}", response_model=StrategyResponse) def get_admin_strategy( strategy_id: str, _auth: AuthContext = Depends(require_admin), settings: Settings = Depends(get_settings), ) -> StrategyResponse: service, repository = _get_service(settings) try: try: return _response(service.get(strategy_id)) except ValueError as exc: _raise_strategy_error(exc) finally: _close_repository(repository) @router.post( "/v1/admin/strategies", response_model=StrategyResponse, status_code=status.HTTP_201_CREATED, ) def create_admin_strategy( body: StrategyCreateRequest, _auth: AuthContext = Depends(require_admin), settings: Settings = Depends(get_settings), ) -> StrategyResponse: service, repository = _get_service(settings) try: strategy = service.create( CreateStrategySetRequest( name=body.name, config=_canonical_config(body.config), status=body.status, ) ) return _response(strategy) finally: _close_repository(repository) @router.patch("/v1/admin/strategies/{strategy_id}", response_model=StrategyResponse) def update_admin_strategy( strategy_id: str, body: StrategyUpdateRequest, _auth: AuthContext = Depends(require_admin), settings: Settings = Depends(get_settings), ) -> StrategyResponse: service, repository = _get_service(settings) try: try: strategy = service.update( strategy_id, UpdateStrategySetRequest( name=body.name, config=_canonical_config(body.config) if body.config is not None else None, status=body.status, ), ) return _response(strategy) except ValueError as exc: _raise_strategy_error(exc) finally: _close_repository(repository) @router.delete( "/v1/admin/strategies/{strategy_id}", status_code=status.HTTP_204_NO_CONTENT, ) def delete_admin_strategy( strategy_id: str, _auth: AuthContext = Depends(require_admin), settings: Settings = Depends(get_settings), ) -> Response: service, repository = _get_service(settings) try: try: service.delete(strategy_id) except ValueError as exc: _raise_strategy_error(exc) finally: _close_repository(repository) return Response(status_code=status.HTTP_204_NO_CONTENT)