Coverage for packages / ingest_client / client.py: 29%
52 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-08 08:14 +1200
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-08 08:14 +1200
1"""Client for tab-api-ingest service.
3Provides async HTTP client for consuming racing data from the tab-api-ingest
4REST API instead of calling the TAB Affiliates API directly.
5"""
7from __future__ import annotations
9from typing import Any
11import httpx
13from packages.core.common.logging import get_logger
14from packages.core.common.settings import get_settings
16logger = get_logger(__name__)
19class IngestServiceClient:
20 """Async HTTP client for tab-api-ingest REST API.
22 Fetches meetings, races, and runner data from the tab-api-ingest service
23 which sources data from the TAB/HRNZ APIs and stores it in its own DB.
25 Example:
26 >>> async with IngestServiceClient() as client:
27 >>> meetings = await client.get_meetings(date="2024-01-01")
28 >>> for meeting in meetings:
29 >>> races = await client.get_races(meeting_id=meeting["meeting"])
30 """
32 def __init__(self, base_url: str | None = None):
33 """Initialize the ingest service client.
35 Args:
36 base_url: tab-api-ingest service base URL. Defaults to the
37 INGEST_SERVICE_URL setting or http://localhost:9090.
38 """
39 settings = get_settings()
40 self.base_url = (
41 base_url
42 or getattr(settings, "ingest_service", None)
43 and settings.ingest_service.url
44 or "http://localhost:9090"
45 )
46 self._client: httpx.AsyncClient | None = None
48 async def __aenter__(self):
49 """Async context manager entry — creates the HTTP client."""
50 self._client = httpx.AsyncClient(timeout=30.0)
51 return self
53 async def __aexit__(self, exc_type, exc_val, exc_tb):
54 """Async context manager exit — closes the HTTP client."""
55 if self._client is not None:
56 await self._client.aclose()
57 self._client = None
59 async def _request(
60 self,
61 method: str,
62 url: str,
63 params: dict[str, Any] | None = None,
64 ) -> Any:
65 """Make an HTTP request with the internal client.
67 Args:
68 method: HTTP method
69 url: Full URL
70 params: Query parameters
72 Returns:
73 Response JSON data
75 Raises:
76 httpx.HTTPError: On request failure
77 """
78 if self._client is None:
79 self._client = httpx.AsyncClient(timeout=30.0)
80 response = await self._client.request(method, url, params=params)
81 response.raise_for_status()
82 return response.json()
84 async def get_meetings(
85 self,
86 date: str | None = None,
87 country: str | None = None,
88 category: str | None = None,
89 ) -> list[dict[str, Any]]:
90 """Fetch meetings from the ingest service.
92 Args:
93 date: Optional ISO date filter (YYYY-MM-DD)
94 country: Optional country code filter (e.g. NZ, AUS)
95 category: Optional racing category (T, H, G)
97 Returns:
98 List of meeting data dictionaries
99 """
100 params: dict[str, Any] = {}
101 if date:
102 params["date"] = date
103 if country:
104 params["country"] = country
105 if category:
106 params["category"] = category
108 logger.info(
109 "Fetching meetings from ingest service",
110 extra={"params": params},
111 )
113 data = await self._request("GET", f"{self.base_url}/api/meetings", params)
114 logger.info(f"Found {len(data)} meetings from ingest service")
115 return data
117 async def get_race(self, race_id: str) -> dict[str, Any]:
118 """Fetch a single race from the ingest service.
120 Args:
121 race_id: Race/event ID (string)
123 Returns:
124 Race data dictionary
125 """
126 logger.info(f"Fetching race {race_id} from ingest service")
127 return await self._request("GET", f"{self.base_url}/api/races/{race_id}")
129 async def get_runners(self, race_id: str) -> list[dict[str, Any]]:
130 """Fetch runners for a race from the ingest service.
132 Args:
133 race_id: Race/event ID (string)
135 Returns:
136 List of runner data dictionaries
137 """
138 logger.info(f"Fetching runners for race {race_id} from ingest service")
139 return await self._request(
140 "GET",
141 f"{self.base_url}/api/races/{race_id}/runners",
142 )
144 async def get_races(
145 self,
146 date: str | None = None,
147 meeting_id: str | None = None,
148 status: str | None = None,
149 limit: int = 100,
150 offset: int = 0,
151 ) -> list[dict[str, Any]]:
152 """Fetch races from the ingest service.
154 Args:
155 date: Optional ISO date filter (YYYY-MM-DD)
156 meeting_id: Optional meeting ID filter
157 status: Optional status filter
158 limit: Maximum results (default 100)
159 offset: Pagination offset (default 0)
161 Returns:
162 List of race data dictionaries
163 """
164 params: dict[str, Any] = {"limit": limit, "offset": offset}
165 if date:
166 params["date"] = date
167 if meeting_id:
168 params["meetingId"] = meeting_id
169 if status:
170 params["status"] = status
172 logger.info(
173 "Fetching races from ingest service",
174 extra={"params": params},
175 )
177 return await self._request("GET", f"{self.base_url}/api/races", params)