Skip to content

Commit c7192b2

Browse files
refactor: improve security and modernize API patterns
- Use hmac.compare_digest() for timing-safe API key validation - Replace get_collections() with collection_exists() in Qdrant store - Add FastAPI lifespan for structured startup/shutdown logging - Add project author to pyproject.toml metadata Made-with: Cursor
1 parent c0520a0 commit c7192b2

5 files changed

Lines changed: 23 additions & 13 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = "0.1.0"
44
description = "Lightweight hybrid RAG engine for multilingual document search"
55
readme = "README.md"
66
license = { text = "MIT" }
7+
authors = [{ name = "Hennadii Pynko" }]
78
requires-python = ">=3.11"
89
dependencies = [
910
"fastapi>=0.115",

src/rag_engine/api/app.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
"""FastAPI application setup."""
22

3+
from collections.abc import AsyncIterator
4+
from contextlib import asynccontextmanager
5+
6+
import structlog
37
from fastapi import FastAPI
48
from fastapi.middleware.cors import CORSMiddleware
59
from slowapi import _rate_limit_exceeded_handler
@@ -11,16 +15,25 @@
1115
from rag_engine.utils.logging import setup_logging
1216

1317
settings = Settings() # type: ignore[call-arg]
18+
logger = structlog.get_logger()
1419

1520

16-
def create_app() -> FastAPI:
17-
"""Create and configure the FastAPI application."""
21+
@asynccontextmanager
22+
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
23+
"""Manage application startup and shutdown lifecycle."""
1824
setup_logging(log_level=settings.log_level)
25+
logger.info("app_started", version=settings.app_version)
26+
yield
27+
logger.info("app_shutdown")
1928

29+
30+
def create_app() -> FastAPI:
31+
"""Create and configure the FastAPI application."""
2032
app = FastAPI(
2133
title="rag-engine",
2234
description="Lightweight hybrid RAG engine for multilingual document search",
2335
version=settings.app_version,
36+
lifespan=lifespan,
2437
)
2538

2639
# Rate limiting

src/rag_engine/api/dependencies.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Shared FastAPI dependencies for authentication and service wiring."""
22

3+
import hmac
34
import re
45
from functools import lru_cache
56

@@ -24,7 +25,7 @@ def get_gdpr_service() -> GDPRService:
2425
qdrant_store: QdrantStore | None = None
2526
try:
2627
qdrant_store = QdrantStore(url=_settings.qdrant_url)
27-
qdrant_store._client.get_collections() # verify connectivity
28+
qdrant_store._client.collection_exists("_ping") # verify connectivity
2829
except Exception:
2930
_logger.warning("qdrant_unavailable_for_gdpr", url=_settings.qdrant_url)
3031
qdrant_store = None
@@ -43,7 +44,7 @@ async def verify_api_key(x_api_key: str = Header(...)) -> str:
4344
Raises:
4445
HTTPException: 401 if key is missing or invalid.
4546
"""
46-
if x_api_key != _settings.api_key:
47+
if not hmac.compare_digest(x_api_key, _settings.api_key):
4748
raise HTTPException(status_code=401, detail="Invalid API key")
4849
return x_api_key
4950

src/rag_engine/api/routes/documents.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
router = APIRouter(tags=["documents"], dependencies=[Depends(verify_api_key)])
1919

20-
# Shared store instances (will be replaced with dependency injection later)
2120
_bm25_store = BM25Store()
2221
_graph_store = KnowledgeGraphStore()
2322
_retriever = HybridRetriever(_bm25_store, _graph_store)

src/rag_engine/storage/qdrant_store.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ def ensure_collection(self, tenant_id: str, vector_size: int) -> None:
5353
"""
5454
collection_name = self._collection_name(tenant_id)
5555

56-
existing = [c.name for c in self._client.get_collections().collections]
57-
if collection_name in existing:
56+
if self._client.collection_exists(collection_name):
5857
return
5958

6059
self._client.create_collection(
@@ -138,8 +137,7 @@ def search(
138137
"""
139138
collection_name = self._collection_name(tenant_id)
140139

141-
existing = [c.name for c in self._client.get_collections().collections]
142-
if collection_name not in existing:
140+
if not self._client.collection_exists(collection_name):
143141
return []
144142

145143
hits = self._client.query_points(
@@ -174,8 +172,7 @@ def remove_document(self, tenant_id: str, document_id: str) -> int:
174172
"""
175173
collection_name = self._collection_name(tenant_id)
176174

177-
existing = [c.name for c in self._client.get_collections().collections]
178-
if collection_name not in existing:
175+
if not self._client.collection_exists(collection_name):
179176
return 0
180177

181178
# Count points before deletion
@@ -217,8 +214,7 @@ def clear_tenant(self, tenant_id: str) -> int:
217214
"""
218215
collection_name = self._collection_name(tenant_id)
219216

220-
existing = [c.name for c in self._client.get_collections().collections]
221-
if collection_name not in existing:
217+
if not self._client.collection_exists(collection_name):
222218
return 0
223219

224220
total = self._client.count(collection_name=collection_name).count

0 commit comments

Comments
 (0)