"""Shared pytest fixtures for integration tests. Provides HTTP clients (httpx.AsyncClient) for each service, base URL fixtures driven by environment variables, and a seed_ids dict that re-exports every deterministic UUID from seed_sandbox.py. When the ``profiler`` fixture is active (provided by conftest_profiling.py), each HTTP client is wrapped in :class:`ProfiledAsyncClient` so that every request is automatically timed and recorded. """ from __future__ import annotations import os from typing import Any import httpx import pytest from tests.integration.profiler import EndpointProfiler from tests.integration.seed_sandbox import ( SEED_AGENT_IDS, SEED_BROKER_ACCOUNT_ID, SEED_COMPANY_IDS, SEED_DOCUMENT_IDS, SEED_GLOBAL_EVENT_IDS, SEED_ORDER_IDS, SEED_PORTFOLIO_SNAPSHOT_ID, SEED_POSITION_IDS, SEED_RECOMMENDATION_IDS, SEED_RISK_CONFIG_ID, SEED_TRADING_DECISION_ID, SEED_TREND_IDS, SEED_VARIANT_IDS, ) # Profiling plugin loaded via root conftest.py (pytest_plugins must be top-level) # --------------------------------------------------------------------------- # ProfiledAsyncClient — transparent timing wrapper # --------------------------------------------------------------------------- class ProfiledAsyncClient: """Wraps :class:`httpx.AsyncClient` to record per-request timing. Every HTTP method call (get, post, put, patch, delete, head, options) is automatically timed via :meth:`EndpointProfiler.track` using the pattern ``"METHOD /path"``. Attribute access for anything not explicitly wrapped is forwarded to the underlying client so tests can still use ``client.base_url``, ``client.headers``, etc. """ def __init__(self, client: httpx.AsyncClient, profiler: EndpointProfiler) -> None: self._client = client self._profiler = profiler # -- Proxied HTTP methods ------------------------------------------------ async def get(self, url: str, **kwargs: Any) -> httpx.Response: async with self._profiler.track(f"GET {url}"): return await self._client.get(url, **kwargs) async def post(self, url: str, **kwargs: Any) -> httpx.Response: async with self._profiler.track(f"POST {url}"): return await self._client.post(url, **kwargs) async def put(self, url: str, **kwargs: Any) -> httpx.Response: async with self._profiler.track(f"PUT {url}"): return await self._client.put(url, **kwargs) async def patch(self, url: str, **kwargs: Any) -> httpx.Response: async with self._profiler.track(f"PATCH {url}"): return await self._client.patch(url, **kwargs) async def delete(self, url: str, **kwargs: Any) -> httpx.Response: async with self._profiler.track(f"DELETE {url}"): return await self._client.delete(url, **kwargs) async def head(self, url: str, **kwargs: Any) -> httpx.Response: async with self._profiler.track(f"HEAD {url}"): return await self._client.head(url, **kwargs) async def options(self, url: str, **kwargs: Any) -> httpx.Response: async with self._profiler.track(f"OPTIONS {url}"): return await self._client.options(url, **kwargs) # -- Transparent attribute forwarding ------------------------------------ def __getattr__(self, name: str) -> Any: return getattr(self._client, name) # --------------------------------------------------------------------------- # URL fixtures — read from env vars set by the runner Job (runner.yaml) # --------------------------------------------------------------------------- @pytest.fixture def query_api_url() -> str: """Base URL for the Query API service.""" return os.environ.get("QUERY_API_URL", "http://localhost:8000") @pytest.fixture def registry_api_url() -> str: """Base URL for the Symbol Registry service.""" return os.environ.get("REGISTRY_API_URL", "http://localhost:8001") @pytest.fixture def risk_api_url() -> str: """Base URL for the Risk Engine service.""" return os.environ.get("RISK_API_URL", "http://localhost:8002") @pytest.fixture def trading_api_url() -> str: """Base URL for the Trading Engine service.""" return os.environ.get("TRADING_API_URL", "http://localhost:8003") # --------------------------------------------------------------------------- # Async HTTP client fixtures — one per service, 30 s timeout # Wrapped with ProfiledAsyncClient for automatic timing collection. # --------------------------------------------------------------------------- @pytest.fixture async def query_client( query_api_url: str, profiler: EndpointProfiler, ) -> ProfiledAsyncClient: """Profiled async HTTP client pointed at the Query API.""" async with httpx.AsyncClient(base_url=query_api_url, timeout=30.0) as client: yield ProfiledAsyncClient(client, profiler) @pytest.fixture async def registry_client( registry_api_url: str, profiler: EndpointProfiler, ) -> ProfiledAsyncClient: """Profiled async HTTP client pointed at the Symbol Registry.""" async with httpx.AsyncClient(base_url=registry_api_url, timeout=30.0) as client: yield ProfiledAsyncClient(client, profiler) @pytest.fixture async def risk_client( risk_api_url: str, profiler: EndpointProfiler, ) -> ProfiledAsyncClient: """Profiled async HTTP client pointed at the Risk Engine.""" async with httpx.AsyncClient(base_url=risk_api_url, timeout=30.0) as client: yield ProfiledAsyncClient(client, profiler) @pytest.fixture async def trading_client( trading_api_url: str, profiler: EndpointProfiler, ) -> ProfiledAsyncClient: """Profiled async HTTP client pointed at the Trading Engine.""" async with httpx.AsyncClient(base_url=trading_api_url, timeout=30.0) as client: yield ProfiledAsyncClient(client, profiler) # --------------------------------------------------------------------------- # Seed ID lookup — single dict with all deterministic IDs from seed_sandbox # --------------------------------------------------------------------------- @pytest.fixture def seed_ids() -> dict: """All deterministic seed IDs for assertion in integration tests.""" return { "companies": SEED_COMPANY_IDS, "documents": SEED_DOCUMENT_IDS, "trends": SEED_TREND_IDS, "recommendations": SEED_RECOMMENDATION_IDS, "orders": SEED_ORDER_IDS, "positions": SEED_POSITION_IDS, "global_events": SEED_GLOBAL_EVENT_IDS, "agents": SEED_AGENT_IDS, "variants": SEED_VARIANT_IDS, "broker_account_id": SEED_BROKER_ACCOUNT_ID, "trading_decision_id": SEED_TRADING_DECISION_ID, "portfolio_snapshot_id": SEED_PORTFOLIO_SNAPSHOT_ID, "risk_config_id": SEED_RISK_CONFIG_ID, }