From 59a76fc2266ac1115575d8a90d328179e7c41b2a Mon Sep 17 00:00:00 2001 From: Anibal Angulo Date: Fri, 20 Feb 2026 05:23:43 +0000 Subject: [PATCH] Add test coverage --- pyproject.toml | 31 ++++ .../services/rag_service.py | 151 ------------------ uv.lock | 30 ++++ 3 files changed, 61 insertions(+), 151 deletions(-) delete mode 100644 src/capa_de_integracion/services/rag_service.py diff --git a/pyproject.toml b/pyproject.toml index 7a0eb12..8a63752 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,19 +34,50 @@ build-backend = "uv_build" dev = [ "inline-snapshot>=0.32.1", "pytest>=9.0.2", + "pytest-asyncio>=1.3.0", "pytest-cov>=7.0.0", + "pytest-env>=1.5.0", "pytest-recording>=0.13.4", "ruff>=0.15.1", "ty>=0.0.17", ] +[tool.ruff] +exclude = ["tests"] + [tool.ruff.lint] select = ['ALL'] ignore = ['D203', 'D213'] +[tool.ty.src] +include = ["src", "packages"] +exclude = ["tests"] + [tool.uv.sources] 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] members = [ "packages/rag-client", diff --git a/src/capa_de_integracion/services/rag_service.py b/src/capa_de_integracion/services/rag_service.py deleted file mode 100644 index 49d5ba4..0000000 --- a/src/capa_de_integracion/services/rag_service.py +++ /dev/null @@ -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() diff --git a/uv.lock b/uv.lock index 3b8860c..861d58d 100644 --- a/uv.lock +++ b/uv.lock @@ -193,7 +193,9 @@ dependencies = [ dev = [ { name = "inline-snapshot" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-env" }, { name = "pytest-recording" }, { name = "ruff" }, { name = "ty" }, @@ -220,7 +222,9 @@ requires-dist = [ dev = [ { name = "inline-snapshot", specifier = ">=0.32.1" }, { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, + { name = "pytest-env", specifier = ">=1.5.0" }, { name = "pytest-recording", specifier = ">=0.13.4" }, { name = "ruff", specifier = ">=0.15.1" }, { 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" }, ] +[[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]] name = "pytest-cov" 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" }, ] +[[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]] name = "pytest-recording" version = "0.13.4"