Add test coverage

This commit is contained in:
2026-02-20 05:23:43 +00:00
parent b86dfe7373
commit e03747f526
3 changed files with 61 additions and 151 deletions

View File

@@ -34,19 +34,50 @@ build-backend = "uv_build"
dev = [ dev = [
"inline-snapshot>=0.32.1", "inline-snapshot>=0.32.1",
"pytest>=9.0.2", "pytest>=9.0.2",
"pytest-asyncio>=1.3.0",
"pytest-cov>=7.0.0", "pytest-cov>=7.0.0",
"pytest-env>=1.5.0",
"pytest-recording>=0.13.4", "pytest-recording>=0.13.4",
"ruff>=0.15.1", "ruff>=0.15.1",
"ty>=0.0.17", "ty>=0.0.17",
] ]
[tool.ruff]
exclude = ["tests"]
[tool.ruff.lint] [tool.ruff.lint]
select = ['ALL'] select = ['ALL']
ignore = ['D203', 'D213'] ignore = ['D203', 'D213']
[tool.ty.src]
include = ["src", "packages"]
exclude = ["tests"]
[tool.uv.sources] [tool.uv.sources]
rag-client = { workspace = true } rag-client = { workspace = true }
[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
testpaths = ["tests"]
addopts = [
"--cov=capa_de_integracion",
"--cov-report=term-missing",
"--cov-report=html",
"--cov-branch",
]
env = [
"FIRESTORE_EMULATOR_HOST=[::1]:8911",
"GCP_PROJECT_ID=test-project",
"GCP_LOCATION=us-central1",
"GCP_FIRESTORE_DATABASE_ID=(default)",
"RAG_ENDPOINT_URL=http://localhost:8000/rag",
"REDIS_HOST=localhost",
"REDIS_PORT=6379",
"DLP_TEMPLATE_COMPLETE_FLOW=projects/test/dlpJobTriggers/test",
]
[tool.uv.workspace] [tool.uv.workspace]
members = [ members = [
"packages/rag-client", "packages/rag-client",

View File

@@ -1,151 +0,0 @@
"""RAG service for calling RAG endpoints with high concurrency."""
import logging
from types import TracebackType
from typing import Self
import httpx
from pydantic import BaseModel, Field
from capa_de_integracion.config import Settings
logger = logging.getLogger(__name__)
class Message(BaseModel):
"""OpenAI-style message format."""
role: str = Field(..., description="Role: system, user, or assistant")
content: str = Field(..., description="Message content")
class RAGRequest(BaseModel):
"""Request model for RAG endpoint."""
messages: list[Message] = Field(..., description="Conversation history")
class RAGResponse(BaseModel):
"""Response model from RAG endpoint."""
response: str = Field(..., description="Generated response from RAG")
class RAGService:
"""Highly concurrent HTTP client for calling RAG endpoints.
Uses httpx AsyncClient with connection pooling for optimal performance
when handling multiple concurrent requests.
"""
def __init__(
self,
settings: Settings,
max_connections: int = 100,
max_keepalive_connections: int = 20,
timeout: float = 30.0,
) -> None:
"""Initialize RAG service with connection pooling.
Args:
settings: Application settings
max_connections: Maximum number of concurrent connections
max_keepalive_connections: Maximum number of idle connections to keep alive
timeout: Request timeout in seconds
"""
self.settings = settings
self.rag_endpoint_url = settings.rag_endpoint_url
self.timeout = timeout
# Configure connection limits for high concurrency
limits = httpx.Limits(
max_connections=max_connections,
max_keepalive_connections=max_keepalive_connections,
)
# Create async client with connection pooling
self._client = httpx.AsyncClient(
limits=limits,
timeout=httpx.Timeout(timeout),
http2=True, # Enable HTTP/2 for better performance
)
logger.info(
"RAGService initialized with endpoint: %s, "
"max_connections: %s, timeout: %ss",
self.rag_endpoint_url,
max_connections,
timeout,
)
async def query(self, messages: list[dict[str, str]]) -> str:
"""Send conversation history to RAG endpoint and get response.
Args:
messages: OpenAI-style conversation history
e.g., [{"role": "user", "content": "Hello"}, ...]
Returns:
Response string from RAG endpoint
Raises:
httpx.HTTPError: If HTTP request fails
ValueError: If response format is invalid
"""
try:
# Validate and construct request
message_objects = [Message(**msg) for msg in messages]
request = RAGRequest(messages=message_objects)
# Make async HTTP POST request
logger.debug("Sending RAG request with %s messages", len(messages))
response = await self._client.post(
self.rag_endpoint_url,
json=request.model_dump(),
headers={"Content-Type": "application/json"},
)
# Raise exception for HTTP errors
response.raise_for_status()
# Parse response
response_data = response.json()
rag_response = RAGResponse(**response_data)
logger.debug("RAG response received: %s chars", len(rag_response.response))
except httpx.HTTPStatusError as e:
logger.exception(
"HTTP error calling RAG endpoint: %s - %s",
e.response.status_code,
e.response.text,
)
raise
except httpx.RequestError:
logger.exception("Request error calling RAG endpoint:")
raise
except Exception:
logger.exception("Unexpected error calling RAG endpoint")
raise
else:
return rag_response.response
async def close(self) -> None:
"""Close the HTTP client and release connections."""
await self._client.aclose()
logger.info("RAGService client closed")
async def __aenter__(self) -> Self:
"""Async context manager entry."""
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
"""Async context manager exit."""
await self.close()

30
uv.lock generated
View File

@@ -193,7 +193,9 @@ dependencies = [
dev = [ dev = [
{ name = "inline-snapshot" }, { name = "inline-snapshot" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "pytest-env" },
{ name = "pytest-recording" }, { name = "pytest-recording" },
{ name = "ruff" }, { name = "ruff" },
{ name = "ty" }, { name = "ty" },
@@ -220,7 +222,9 @@ requires-dist = [
dev = [ dev = [
{ name = "inline-snapshot", specifier = ">=0.32.1" }, { name = "inline-snapshot", specifier = ">=0.32.1" },
{ name = "pytest", specifier = ">=9.0.2" }, { name = "pytest", specifier = ">=9.0.2" },
{ name = "pytest-asyncio", specifier = ">=1.3.0" },
{ name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "pytest-env", specifier = ">=1.5.0" },
{ name = "pytest-recording", specifier = ">=0.13.4" }, { name = "pytest-recording", specifier = ">=0.13.4" },
{ name = "ruff", specifier = ">=0.15.1" }, { name = "ruff", specifier = ">=0.15.1" },
{ name = "ty", specifier = ">=0.0.17" }, { name = "ty", specifier = ">=0.0.17" },
@@ -1605,6 +1609,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
] ]
[[package]]
name = "pytest-asyncio"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
]
[[package]] [[package]]
name = "pytest-cov" name = "pytest-cov"
version = "7.0.0" version = "7.0.0"
@@ -1619,6 +1636,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
] ]
[[package]]
name = "pytest-env"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/56/a931c6f6194917ff44be41b8586e2ffd13a18fa70fb28d9800a4695befa5/pytest_env-1.5.0.tar.gz", hash = "sha256:db8994b9ce170f135a37acc09ac753a6fc697d15e691b576ed8d8ca261c40246", size = 15271, upload-time = "2026-02-17T18:31:39.095Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/af/99b52a8524983bfece35e51e65a0b517b22920c023e57855c95e744e19e4/pytest_env-1.5.0-py3-none-any.whl", hash = "sha256:89a15686ac837c9cd009a8a2d52bd55865e2f23c82094247915dae4540c87161", size = 10122, upload-time = "2026-02-17T18:31:37.496Z" },
]
[[package]] [[package]]
name = "pytest-recording" name = "pytest-recording"
version = "0.13.4" version = "0.13.4"