Coverage for /builds/svesch/cikit/src/cikit/data/client.py: 100%
67 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-10-09 14:17 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-10-09 14:17 +0000
1# SPDX-License-Identifier: GPL-3.0-or-later
2"""Low-level HTTP client for the cikit data API.
4This module provides the `CikitDataClient` class for direct access to the
5remote cikit data API endpoints. It supports creating pipeline entries,
6updating run data, querying timeline data, and fetching latest values.
8Author:
9 MS CoPilot / OpenAI
10Date:
11 2026-02-12
12"""
14from enum import StrEnum
16import requests
17from requests.adapters import HTTPAdapter
18from urllib3.util.retry import Retry
20from cikit.errors import CiToolsError, CikitSyncError
23REQUESTS_CONNECT_TIMEOUT = 5
24REQUESTS_READ_TIMEOUT = 30
25REQUESTS_TIMEOUT = (REQUESTS_CONNECT_TIMEOUT, REQUESTS_READ_TIMEOUT)
26REQUESTS_RETRY_COUNT = 3
27REQUESTS_RETRY_BACKOFF = 1.0
28REQUESTS_RETRY_STATUS_CODES = [500, 502, 503, 504]
31def create_session() -> requests.Session:
32 """Create a configured HTTP session.
34 The session retries temporary server-side failures and connection issues.
35 Client-side errors are not retried because they usually indicate invalid
36 configuration or invalid request data.
38 Returns:
39 Configured requests session.
40 """
41 retry = Retry(total=REQUESTS_RETRY_COUNT, connect=REQUESTS_RETRY_COUNT,
42 read=REQUESTS_RETRY_COUNT, backoff_factor=REQUESTS_RETRY_BACKOFF,
43 status_forcelist=REQUESTS_RETRY_STATUS_CODES, allowed_methods=["GET", "POST"])
45 adapter = HTTPAdapter(max_retries=retry)
46 session = requests.Session()
47 session.mount("http://", adapter)
48 session.mount("https://", adapter)
50 return session
53class RefKey(StrEnum):
54 """Supported reference keys for timeline queries."""
55 ID = "id"
56 INDEX_DESC = "index_desc"
57 TIMESTAMP = "timestamp"
58 COMMIT_HASH = "commit_hash"
59 SHORT_COMMIT_HASH = "short_commit_hash"
60 PIPELINE_ID = "pipeline_id"
63class CikitDataClient:
64 """Low-level client for the remote cikit data API.
66 The client wraps the available HTTP endpoints and returns parsed JSON
67 responses as Python dictionaries.
68 """
69 def __init__(self, base_url: str, token: str = None):
70 """Initialize the CikitDataClient.
72 Args:
73 base_url: Base URL of the cikit data API. Must not be empty.
74 token: Optional authentication token. If set, an ``X-Auth-Token``
75 header is added to all requests.
77 Raises:
78 CiToolsError: If ``base_url`` is empty or None.
79 """
80 if not base_url:
81 raise CiToolsError("base_url must be provided explicitly")
83 self.base_url = base_url.rstrip("/")
84 self.token = token
85 self.session = create_session()
87 self.headers = {"Content-Type": "application/json"}
88 if self.token:
89 self.headers["X-Auth-Token"] = self.token
91 def _safe_request(self, method: str, url: str, **kwargs) -> dict:
92 """Perform an HTTP request and return the parsed JSON response.
94 All request-related errors are converted to ``CikitSyncError``.
96 Args:
97 method: HTTP method, for example ``"GET"`` or ``"POST"``.
98 url: Fully qualified API endpoint URL.
99 **kwargs: Additional keyword arguments forwarded to
100 ``requests.Session.request()``, such as ``json``, ``params``,
101 ``headers``, or ``timeout``.
103 Returns:
104 Parsed JSON response from the API.
106 Raises:
107 CikitSyncError: If the request fails, the response status is not
108 successful, or the response body is not valid JSON.
109 """
110 try:
111 timeout = kwargs.pop("timeout", REQUESTS_TIMEOUT)
112 response = self.session.request(method, url, timeout=timeout, **kwargs)
113 response.raise_for_status()
114 except requests.RequestException as exp:
115 raise CikitSyncError(f"API request failed: {exp}") from exp
117 try:
118 data = response.json()
119 except ValueError as exp:
120 raise CikitSyncError(f"Error while parsing API response: {exp}") from exp
122 return data
124 def create_pipeline(self, project: str, pipeline_id: str, ci_provider: str,
125 commit_hash: str = None) -> dict:
126 """Create a new pipeline entry.
128 Args:
129 project: Project name.
130 pipeline_id: External pipeline identifier.
131 ci_provider: CI provider name.
132 commit_hash: Optional commit hash.
134 Returns:
135 Parsed JSON response from the API.
136 """
137 url = f"{self.base_url}/cikit_data_create.php"
138 payload = {"project": project, "pipeline_id": pipeline_id, "ci_provider": ci_provider}
139 if commit_hash:
140 payload["commit_hash"] = commit_hash
141 return self._safe_request("POST", url, json=payload, headers=self.headers)
143 def update_run(self, project: str, pipeline_id: str, run: str, data: dict) -> dict:
144 """Update run-level data for a pipeline.
146 Args:
147 project: Project name.
148 pipeline_id: Pipeline identifier.
149 run: Cikit run name.
150 data: Metric key/value pairs for this run.
152 Returns:
153 Parsed JSON response from the API.
154 """
155 url = f"{self.base_url}/cikit_data_update.php"
156 payload = {"project": project, "pipeline_id": pipeline_id, "run": run, "data": data}
157 return self._safe_request("POST", url, json=payload, headers=self.headers)
159 def get_timeline(self, project: str, keys: list[str],
160 ref_key: RefKey = RefKey.TIMESTAMP, limit: int = None) -> dict:
161 """Retrieve timeline data for one or more cikit data keys.
163 Args:
164 project: Project name.
165 keys: Fully qualified cikit data keys, for example
166 ``"runs.build.duration"``.
167 ref_key: Reference key used for the X-axis.
168 limit: Optional maximum number of entries.
170 Returns:
171 Parsed JSON response containing timeline references and series data.
172 """
173 url = f"{self.base_url}/cikit_data_timeline.php"
174 params = {"project": project, "ref_key": ref_key, "keys": ",".join(keys)}
176 if limit is not None:
177 params["limit"] = str(limit)
179 return self._safe_request("GET", url, params=params, headers=self.headers)
181 def get_latest(self, project: str, key: str) -> dict:
182 """Retrieve the latest value for a given project key.
184 Args:
185 project: Project name.
186 key: Fully qualified cikit data key, for example
187 ``"runs.build.duration"``.
189 Returns:
190 Parsed JSON response containing timestamp, pipeline_id,
191 ci_provider, commit_hash, and value.
192 """
193 url = f"{self.base_url}/cikit_data_latest.php"
194 params = {"project": project, "key": key}
195 return self._safe_request("GET", url, params=params, headers=self.headers)