From ca58dd94dbab1d81c7dc62b999a781e10f89eab4 Mon Sep 17 00:00:00 2001 From: agatha Date: Sat, 14 Mar 2026 16:02:47 -0400 Subject: [PATCH] feat: add proxy source crud and wire into app --- src/proxy_pool/app.py | 6 +- src/proxy_pool/proxy/router.py | 126 +++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 src/proxy_pool/proxy/router.py diff --git a/src/proxy_pool/app.py b/src/proxy_pool/app.py index 0cb3233..c6cc9d2 100644 --- a/src/proxy_pool/app.py +++ b/src/proxy_pool/app.py @@ -4,10 +4,12 @@ from contextlib import asynccontextmanager from fastapi import FastAPI from redis.asyncio import from_url as redis_from_url +from proxy_pool.common.router import router as health_router from proxy_pool.config import get_settings from proxy_pool.db.session import create_session_factory from proxy_pool.plugins.discovery import discover_plugins from proxy_pool.plugins.registry import PluginRegistry +from proxy_pool.proxy.router import router as proxy_router logger = logging.getLogger(__name__) @@ -57,7 +59,7 @@ def create_app() -> FastAPI: lifespan=lifespan, ) - # Register routers here as we build them - # app.include_router(...) + app.include_router(health_router) + app.include_router(proxy_router) return app diff --git a/src/proxy_pool/proxy/router.py b/src/proxy_pool/proxy/router.py new file mode 100644 index 0000000..f74b97e --- /dev/null +++ b/src/proxy_pool/proxy/router.py @@ -0,0 +1,126 @@ +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from proxy_pool.common.dependencies import get_db, get_registry +from proxy_pool.plugins.registry import PluginRegistry +from proxy_pool.proxy.models import ProxySource +from proxy_pool.proxy.schemas import ( + ProxySourceCreate, + ProxySourceResponse, + ProxySourceUpdate, +) + +router = APIRouter(prefix="/sources", tags=["sources"]) + + +@router.get("", response_model=list[ProxySourceResponse]) +async def list_sources( + is_active: bool | None = None, + db: AsyncSession = Depends(get_db), +) -> list[ProxySourceResponse]: + query = select(ProxySource) + if is_active is not None: + query = query.where(ProxySource.is_active == is_active) + query = query.order_by(ProxySource.created_at.desc()) + + result = await db.execute(query) + sources = result.scalars().all() + return [ProxySourceResponse.model_validate(s) for s in sources] + + +@router.post( + "", + response_model=ProxySourceResponse, + status_code=status.HTTP_201_CREATED, +) +async def create_source( + body: ProxySourceCreate, + db: AsyncSession = Depends(get_db), + registry: PluginRegistry = Depends(get_registry), +) -> ProxySourceResponse: + # Validate parser exists + try: + registry.get_parser(body.parser_name) + except Exception: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"No parser registered with name '{body.parser_name}'", + ) from None + + source = ProxySource( + url=body.url, + parser_name=body.parser_name, + cron_schedule=body.cron_schedule, + default_protocol=body.default_protocol, + ) + db.add(source) + await db.commit() + await db.refresh(source) + + return ProxySourceResponse.model_validate(source) + + +@router.get("/{source_id}", response_model=ProxySourceResponse) +async def get_source( + source_id: UUID, + db: AsyncSession = Depends(get_db), +) -> ProxySourceResponse: + source = await db.get(ProxySource, source_id) + if source is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Source not found", + ) + return ProxySourceResponse.model_validate(source) + + +@router.patch("/{source_id}", response_model=ProxySourceResponse) +async def update_source( + source_id: UUID, + body: ProxySourceUpdate, + db: AsyncSession = Depends(get_db), + registry: PluginRegistry = Depends(get_registry), +) -> ProxySourceResponse: + source = await db.get(ProxySource, source_id) + if source is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Source not found", + ) + + update_data = body.model_dump(exclude_unset=True) + + if "parser_name" in update_data: + try: + registry.get_parser(update_data["parser_name"]) + except Exception: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"No parser registered with name '{update_data['parser_name']}'", + ) from None + + for field, value in update_data.items(): + setattr(source, field, value) + + await db.commit() + await db.refresh(source) + + return ProxySourceResponse.model_validate(source) + + +@router.delete("/{source_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_source( + source_id: UUID, + db: AsyncSession = Depends(get_db), +) -> None: + source = await db.get(ProxySource, source_id) + if source is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Source not found", + ) + await db.delete(source) + await db.commit()