userver: en/testsuite/daemons/service_client.py Source File
Loading...
Searching...
No Matches
service_client.py
1import json
2import ssl
3import typing
4import uuid
5
6import aiohttp
7import yarl
8
9from testsuite import types
10from testsuite.utils import http, url_util
11
12DEFAULT_HOST = 'localhost'
13DEFAULT_TIMEOUT = 120.0
14
15TResponse = typing.TypeVar(
16 'TResponse',
17 aiohttp.ClientResponse,
18 http.ClientResponse,
19)
20
21
24 self,
25 base_url: str,
26 *,
27 session: aiohttp.ClientSession,
28 ssl_context: ssl.SSLContext | None = None,
29 span_id_header: str | None = None,
30 headers: dict[str, str] | None = None,
31 timeout: float = DEFAULT_TIMEOUT,
32 ):
33 """
34 :param base_url: Base client url
35 :param session: ``aiohttp.ClientSession`` instance
36 :param headers: default request headers dictionary
37 :param timeout: http client default timeout
38 """
39 self._base_url = url_util.ensure_trailing_separator(base_url)
40 self._headers = headers or {}
41 self._timeout = timeout
42 self._session = session
43 self._ssl_context = ssl_context
44 self._span_id_header = span_id_header
45
46 def url(self, path: str | yarl.URL):
47 if isinstance(path, str):
48 return url_util.join(self._base_url, path)
49 return path
50
51 async def _aiohttp_request(
52 self,
53 http_method: str,
54 path: str | yarl.URL,
55 headers: dict[str, str] | None = None,
56 bearer: str | None = None,
57 x_real_ip: str | None = None,
58 **kwargs,
59 ) -> aiohttp.ClientResponse:
60 url = self.url(path)
61 headers = self._build_headers(
62 headers,
63 bearer=bearer,
64 x_real_ip=x_real_ip,
65 )
66 kwargs['timeout'] = kwargs.get('timeout', self._timeout)
67
68 params = kwargs.get('params', None)
69 if params is not None:
70 kwargs['params'] = _flatten(params)
71 response = await self._session.request(
72 http_method,
73 url,
74 headers=headers,
75 ssl=self._ssl_context, # type: ignore[arg-type]
76 **kwargs,
77 )
78 return response
79
80 def _build_headers(
81 self,
82 user_headers: dict[str, str] | None = None,
83 bearer: str | None = None,
84 x_real_ip: str | None = None,
85 ) -> dict[str, str]:
86 headers = self._headers.copy()
87 if user_headers:
88 headers.update(user_headers)
89 if bearer:
90 headers['Authorization'] = 'Bearer %s' % bearer
91 if x_real_ip:
92 headers['X-Real-IP'] = x_real_ip
93 if self._span_id_header and self._span_id_header not in headers:
94 headers[self._span_id_header] = uuid.uuid4().hex
95
96 headers = {
97 key: '' if value is None else value
98 for key, value in headers.items()
99 }
100 return headers
101
102
103class GenericClient(BaseAiohttpClient, typing.Generic[TResponse]):
104 """Basic asyncio HTTP service client."""
105
106 async def post(
107 self,
108 path: str,
109 json: types.JsonAnyOptional = None,
110 data: typing.Any = None,
111 params: dict[str, str] | None = None,
112 bearer: str | None = None,
113 x_real_ip: str | None = None,
114 headers: dict[str, str] | None = None,
115 **kwargs,
116 ) -> TResponse:
117 """Perform HTTP POST request."""
118 return await self._request(
119 'POST',
120 path,
121 json=json,
122 data=data,
123 params=params,
124 headers=headers,
125 bearer=bearer,
126 x_real_ip=x_real_ip,
127 **kwargs,
128 )
129
130 async def put(
131 self,
132 path,
133 json: types.JsonAnyOptional = None,
134 data: typing.Any = None,
135 params: dict[str, str] | None = None,
136 bearer: str | None = None,
137 x_real_ip: str | None = None,
138 headers: dict[str, str] | None = None,
139 **kwargs,
140 ) -> TResponse:
141 """Perform HTTP PUT request."""
142 return await self._request(
143 'PUT',
144 path,
145 json=json,
146 data=data,
147 params=params,
148 headers=headers,
149 bearer=bearer,
150 x_real_ip=x_real_ip,
151 **kwargs,
152 )
153
154 async def patch(
155 self,
156 path,
157 json: types.JsonAnyOptional = None,
158 data: typing.Any = None,
159 params: dict[str, str] | None = None,
160 bearer: str | None = None,
161 x_real_ip: str | None = None,
162 headers: dict[str, str] | None = None,
163 **kwargs,
164 ) -> TResponse:
165 """Perform HTTP PATCH request."""
166 return await self._request(
167 'PATCH',
168 path,
169 json=json,
170 data=data,
171 params=params,
172 headers=headers,
173 bearer=bearer,
174 x_real_ip=x_real_ip,
175 **kwargs,
176 )
177
178 async def get(
179 self,
180 path: str,
181 headers: dict[str, str] | None = None,
182 bearer: str | None = None,
183 x_real_ip: str | None = None,
184 **kwargs,
185 ) -> TResponse:
186 """Perform HTTP GET request."""
187 return await self._request(
188 'GET',
189 path,
190 headers=headers,
191 bearer=bearer,
192 x_real_ip=x_real_ip,
193 **kwargs,
194 )
195
196 async def delete(
197 self,
198 path: str,
199 headers: dict[str, str] | None = None,
200 bearer: str | None = None,
201 x_real_ip: str | None = None,
202 **kwargs,
203 ) -> TResponse:
204 """Perform HTTP DELETE request."""
205 return await self._request(
206 'DELETE',
207 path,
208 headers=headers,
209 bearer=bearer,
210 x_real_ip=x_real_ip,
211 **kwargs,
212 )
213
214 async def options(
215 self,
216 path: str,
217 headers: dict[str, str] | None = None,
218 bearer: str | None = None,
219 x_real_ip: str | None = None,
220 **kwargs,
221 ) -> TResponse:
222 """Perform HTTP OPTIONS request."""
223 return await self._request(
224 'OPTIONS',
225 path,
226 headers=headers,
227 bearer=bearer,
228 x_real_ip=x_real_ip,
229 **kwargs,
230 )
231
232 async def request(
233 self,
234 http_method: str,
235 path: str,
236 **kwargs,
237 ) -> TResponse:
238 """Perform HTTP ``http_method`` request."""
239 return await self._request(http_method, path, **kwargs)
240
241 async def _request(
242 self,
243 http_method: str,
244 path: str | yarl.URL,
245 headers: dict[str, str] | None = None,
246 bearer: str | None = None,
247 x_real_ip: str | None = None,
248 **kwargs,
249 ) -> TResponse:
250 raise NotImplementedError
251
252
253class AiohttpClient(GenericClient[aiohttp.ClientResponse]):
254 async def _request(
255 self,
256 http_method: str,
257 path: str | yarl.URL,
258 headers: dict[str, str] | None = None,
259 bearer: str | None = None,
260 x_real_ip: str | None = None,
261 **kwargs,
262 ) -> aiohttp.ClientResponse:
263 return await self._aiohttp_request(
264 http_method,
265 path,
266 headers,
267 bearer,
268 x_real_ip,
269 **kwargs,
270 )
271
272
274 async def _request(
275 self,
276 http_method: str,
277 path: str | yarl.URL,
278 headers: dict[str, str] | None = None,
279 bearer: str | None = None,
280 x_real_ip: str | None = None,
281 **kwargs,
283 response = await self._aiohttp_request(
284 http_method,
285 path,
286 headers,
287 bearer,
288 x_real_ip,
289 **kwargs,
290 )
291 return await self._wrap_client_response(response)
292
293 def _wrap_client_response(
294 self,
295 response,
296 ) -> typing.Awaitable[http.ClientResponse]:
297 return http.wrap_client_response(response, json_loads=json.loads)
298
299
300def _flatten(query_params):
301 result = []
302 iterable = (
303 query_params.items() if isinstance(query_params, dict) else query_params
304 )
305 for key, value in iterable:
306 if isinstance(value, (tuple, list)):
307 for element in value:
308 result.append((key, str(element)))
309 else:
310 result.append((key, str(value)))
311 return result