diff --git a/python/ebook_search/api/dependencies.py b/python/ebook_search/api/dependencies.py index 508dfd7..77b5227 100644 --- a/python/ebook_search/api/dependencies.py +++ b/python/ebook_search/api/dependencies.py @@ -6,7 +6,6 @@ from typing import Annotated import httpx from fastapi import Depends, Request -from sqlalchemy.ext.asyncio import AsyncEngine from python.ebook_search.config import EbookSearchConfig @@ -16,16 +15,10 @@ def get_config(request: Request) -> EbookSearchConfig: return request.app.state.config -def get_engine(request: Request) -> AsyncEngine: - """Get the database engine from app state.""" - return request.app.state.engine - - def get_http_client(request: Request) -> httpx.AsyncClient: """Get the shared LLM HTTP client from app state.""" return request.app.state.http_client AppConfig = Annotated[EbookSearchConfig, Depends(get_config)] -AppEngine = Annotated[AsyncEngine, Depends(get_engine)] AppHttpClient = Annotated[httpx.AsyncClient, Depends(get_http_client)] diff --git a/python/ebook_search/api/routes/admin.py b/python/ebook_search/api/routes/admin.py index 09a4dca..efb6b01 100644 --- a/python/ebook_search/api/routes/admin.py +++ b/python/ebook_search/api/routes/admin.py @@ -10,7 +10,6 @@ from fastapi.responses import HTMLResponse from python.ebook_search.api.bm25_tasks import schedule_bm25_refresh from python.ebook_search.api.dependencies import ( # noqa: TC001 FastAPI resolves these annotated dependencies at runtime AppConfig, - AppEngine, AppHttpClient, ) from python.ebook_search.api.web import error_response, templates @@ -19,7 +18,10 @@ from python.ebook_search.ingest import ingest_configured_paths from python.ebook_search.protected_phrases.generate_ngrams import generate_candidate_phrases_for_books from python.ebook_search.protected_phrases.judge_ngrams import judge_candidate_phrases_for_books from python.ebook_search.protected_phrases.store import book_ids_pending_first_judgment, corpus_phrase_stats -from python.fastapi_tools import AsyncDbSession # noqa: TC001 FastAPI resolves this annotated dependency at runtime +from python.fastapi_tools import ( # noqa: TC001 FastAPI resolves these annotated dependencies at runtime + AppAsyncEngine, + AsyncDbSession, +) logger = logging.getLogger(__name__) @@ -59,7 +61,7 @@ async def scan_library(request: Request, config: AppConfig, session: AsyncDbSess @router.post("/phrases/generate-all", response_class=HTMLResponse) -async def generate_all_phrases(request: Request, config: AppConfig, engine: AppEngine) -> HTMLResponse: +async def generate_all_phrases(request: Request, config: AppConfig, engine: AppAsyncEngine) -> HTMLResponse: """Regenerate candidate phrases for every indexed book without LLM judging.""" try: result = await generate_candidate_phrases_for_books(engine, config) @@ -83,7 +85,7 @@ async def generate_all_phrases(request: Request, config: AppConfig, engine: AppE @router.post("/phrases/judge-all", response_class=HTMLResponse) -async def judge_all_phrases(request: Request, engine: AppEngine, config: AppConfig) -> HTMLResponse: +async def judge_all_phrases(request: Request, engine: AppAsyncEngine, config: AppConfig) -> HTMLResponse: """Judge unjudged candidate phrases across every indexed book.""" return await run_phrase_judgment(request, engine, config, source_ids=None) @@ -91,7 +93,7 @@ async def judge_all_phrases(request: Request, engine: AppEngine, config: AppConf @router.post("/phrases/judge-missing", response_class=HTMLResponse) async def judge_missing_phrases( request: Request, - engine: AppEngine, + engine: AppAsyncEngine, config: AppConfig, session: AsyncDbSession, ) -> HTMLResponse: @@ -108,7 +110,7 @@ async def judge_missing_phrases( async def run_phrase_judgment( request: Request, - engine: AppEngine, + engine: AppAsyncEngine, config: AppConfig, *, source_ids: list[int] | None, @@ -117,7 +119,7 @@ async def run_phrase_judgment( Args: request (Request): Current request, for template rendering. - engine (AppEngine): Engine used to open per-book judging sessions. + engine (AppAsyncEngine): Engine used to open per-book judging sessions. config (AppConfig): Runtime phrase-tuning settings. source_ids (list[int] | None): Books to judge; ``None`` judges every indexed book. diff --git a/python/ebook_search/api/routes/search.py b/python/ebook_search/api/routes/search.py index 20fb3b9..d707aa6 100644 --- a/python/ebook_search/api/routes/search.py +++ b/python/ebook_search/api/routes/search.py @@ -13,7 +13,6 @@ from fastapi.responses import HTMLResponse from python.ebook_search.answer import answer_query from python.ebook_search.api.dependencies import ( # noqa: TC001 FastAPI resolves these annotated dependencies at runtime AppConfig, - AppEngine, AppHttpClient, ) from python.ebook_search.api.web import error_response, templates @@ -25,6 +24,7 @@ from python.ebook_search.guardrails import ( ) from python.ebook_search.search import SearchResponse, search_ebooks from python.ebook_search.timing import runtime_step_from_start +from python.fastapi_tools import AppAsyncEngine # noqa: TC001 FastAPI resolves this annotated dependency at runtime if TYPE_CHECKING: import httpx @@ -76,7 +76,7 @@ async def build_answer( async def search( request: Request, config: AppConfig, - engine: AppEngine, + engine: AppAsyncEngine, client: AppHttpClient, query: Annotated[str, Form()], *, diff --git a/python/fastapi_tools/__init__.py b/python/fastapi_tools/__init__.py index 8490ccb..5a32670 100644 --- a/python/fastapi_tools/__init__.py +++ b/python/fastapi_tools/__init__.py @@ -1,6 +1,25 @@ """Reusable FastAPI tools.""" -from python.fastapi_tools.db import AsyncDbSession, DbSession, get_async_db, get_db +from python.fastapi_tools.db import ( + AppAsyncEngine, + AppEngine, + AsyncDbSession, + DbSession, + get_async_db, + get_async_engine, + get_db, + get_engine, +) from python.fastapi_tools.zstd_middleware import ZstdMiddleware -__all__ = ["AsyncDbSession", "DbSession", "ZstdMiddleware", "get_async_db", "get_db"] +__all__ = [ + "AppAsyncEngine", + "AppEngine", + "AsyncDbSession", + "DbSession", + "ZstdMiddleware", + "get_async_db", + "get_async_engine", + "get_db", + "get_engine", +] diff --git a/python/fastapi_tools/db.py b/python/fastapi_tools/db.py index 6dad25a..6a369f2 100644 --- a/python/fastapi_tools/db.py +++ b/python/fastapi_tools/db.py @@ -5,13 +5,24 @@ from __future__ import annotations from typing import TYPE_CHECKING, Annotated from fastapi import Depends, Request -from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.engine import Engine +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession from sqlalchemy.orm import Session if TYPE_CHECKING: from collections.abc import AsyncIterator, Iterator +def get_engine(request: Request) -> Engine: + """Get a synchronous database engine from app state.""" + return request.app.state.engine + + +def get_async_engine(request: Request) -> AsyncEngine: + """Get an asynchronous database engine from app state.""" + return request.app.state.engine + + def get_db(request: Request) -> Iterator[Session]: """Get database session from app state.""" with Session(request.app.state.engine) as session: @@ -28,5 +39,7 @@ async def get_async_db(request: Request) -> AsyncIterator[AsyncSession]: yield session +AppEngine = Annotated[Engine, Depends(get_engine)] +AppAsyncEngine = Annotated[AsyncEngine, Depends(get_async_engine)] DbSession = Annotated[Session, Depends(get_db)] AsyncDbSession = Annotated[AsyncSession, Depends(get_async_db)]