Skip to content

Commit e3a4ca7

Browse files
committed
Fix tests
1 parent 853ab2b commit e3a4ca7

File tree

7 files changed

+231
-77
lines changed

7 files changed

+231
-77
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,8 @@ jobs:
8282
--disable-error-code=import-untyped \
8383
--disable-error-code=no-untyped-call \
8484
packages/
85+
86+
- name: Run Tests
87+
run: |
88+
source .venv/bin/activate
89+
pytest packages

mypy.ini

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
[mypy]
22
python_version = 3.12
3-
strict = True
3+
strict = False
44
warn_unused_configs = True
5-
6-
[mypy-botbuilder.*]
7-
ignore_missing_imports = True
8-
9-
[mypy-botframework.*]
10-
ignore_missing_imports = True
11-
12-
[mypy-sqlite_vec.*]
13-
ignore_missing_imports = True
5+
exclude = tests/

packages/common/src/common/http/client.py

Lines changed: 129 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
import inspect
12
from dataclasses import dataclass, field
2-
from typing import Any, Dict, List, Optional
3+
from typing import Any, Callable, Dict, List, Optional
34

45
import httpx
6+
from httpx._models import Request, Response
7+
from httpx._types import QueryParamTypes, RequestContent, RequestData, RequestFiles
58

69
from common.http.clientToken import Token, resolve_token
710
from common.http.interceptor import Interceptor, InterceptorRequestContext, InterceptorResponseContext
811
from common.logging import ConsoleLogger, Logger
912

13+
console_logger = ConsoleLogger()
14+
1015

1116
@dataclass(frozen=True)
1217
class ClientOptions:
@@ -23,9 +28,9 @@ class ClientOptions:
2328
"""
2429

2530
base_url: Optional[str] = None
26-
headers: Optional[Dict[str, str]] = field(default_factory=dict)
31+
headers: Dict[str, str] = field(default_factory=dict)
2732
timeout: Optional[float] = None
28-
logger: Logger = field(default_factory=ConsoleLogger)
33+
logger: Logger = field(default_factory=lambda: console_logger.create_logger("http-client"))
2934
token: Optional[Token] = None
3035
interceptors: Optional[List[Interceptor]] = field(default_factory=list)
3136

@@ -53,7 +58,7 @@ def __init__(self, options: ClientOptions):
5358
self._interceptors = list(options.interceptors or [])
5459

5560
self.http = httpx.AsyncClient(
56-
base_url=options.base_url,
61+
base_url=httpx.URL(options.base_url) if options.base_url else "",
5762
headers=options.headers,
5863
timeout=options.timeout,
5964
)
@@ -77,31 +82,42 @@ async def _prepare_headers(self, headers: Optional[Dict[str, str]], token: Optio
7782
return req_headers
7883

7984
async def get(
80-
self, url: str, *, headers: Optional[Dict[str, str]] = None, token: Optional[Token] = None, **kwargs
85+
self,
86+
url: str,
87+
*,
88+
headers: Optional[Dict[str, str]] = None,
89+
token: Optional[Token] = None,
90+
params: Optional[QueryParamTypes] = None,
91+
**kwargs: Any,
8192
) -> httpx.Response:
8293
"""
8394
Send a GET request.
8495
8596
Args:
8697
url: The URL path or full URL.
8798
headers: Optional per-request headers.
99+
params: Optional query parameters.
88100
token: Optional per-request token (overrides default).
89101
**kwargs: Additional httpx.AsyncClient.get arguments.
90102
91103
Returns:
92104
httpx.Response
93105
"""
94106
req_headers = await self._prepare_headers(headers, token)
95-
return await self.http.get(url, headers=req_headers, **kwargs)
107+
return await self.http.get(url, headers=req_headers, params=params, **kwargs)
96108

97109
async def post(
98110
self,
99111
url: str,
100-
data: Any = None,
101112
*,
113+
content: Optional[RequestContent] = None,
114+
data: Optional[RequestData] = None,
115+
files: Optional[RequestFiles] = None,
116+
json: Optional[Any] = None,
117+
params: Optional[QueryParamTypes] = None,
102118
headers: Optional[Dict[str, str]] = None,
103119
token: Optional[Token] = None,
104-
**kwargs,
120+
**kwargs: Any,
105121
) -> httpx.Response:
106122
"""
107123
Send a POST request.
@@ -110,23 +126,40 @@ async def post(
110126
url: The URL path or full URL.
111127
data: The request body.
112128
headers: Optional per-request headers.
129+
params: Optional query parameters.
130+
content: The request body.
131+
files: The request files.
132+
json: The request JSON body.
113133
token: Optional per-request token (overrides default).
114134
**kwargs: Additional httpx.AsyncClient.post arguments.
115135
116136
Returns:
117137
httpx.Response
118138
"""
119139
req_headers = await self._prepare_headers(headers, token)
120-
return await self.http.post(url, data=data, headers=req_headers, **kwargs)
140+
return await self.http.post(
141+
url,
142+
data=data,
143+
files=files,
144+
json=json,
145+
content=content,
146+
params=params,
147+
headers=req_headers,
148+
**kwargs,
149+
)
121150

122151
async def put(
123152
self,
124153
url: str,
125-
data: Any = None,
126154
*,
155+
content: Optional[RequestContent] = None,
156+
data: Optional[RequestData] = None,
157+
files: Optional[RequestFiles] = None,
158+
json: Optional[Any] = None,
159+
params: Optional[QueryParamTypes] = None,
127160
headers: Optional[Dict[str, str]] = None,
128161
token: Optional[Token] = None,
129-
**kwargs,
162+
**kwargs: Any,
130163
) -> httpx.Response:
131164
"""
132165
Send a PUT request.
@@ -135,23 +168,40 @@ async def put(
135168
url: The URL path or full URL.
136169
data: The request body.
137170
headers: Optional per-request headers.
171+
params: Optional query parameters.
172+
content: The request body.
173+
files: The request files.
174+
json: The request JSON body.
138175
token: Optional per-request token (overrides default).
139176
**kwargs: Additional httpx.AsyncClient.put arguments.
140177
141178
Returns:
142179
httpx.Response
143180
"""
144181
req_headers = await self._prepare_headers(headers, token)
145-
return await self.http.put(url, data=data, headers=req_headers, **kwargs)
182+
return await self.http.put(
183+
url,
184+
data=data,
185+
files=files,
186+
json=json,
187+
content=content,
188+
params=params,
189+
headers=req_headers,
190+
**kwargs,
191+
)
146192

147193
async def patch(
148194
self,
149195
url: str,
150-
data: Any = None,
151196
*,
197+
content: Optional[RequestContent] = None,
198+
data: Optional[RequestData] = None,
199+
files: Optional[RequestFiles] = None,
200+
json: Optional[Any] = None,
201+
params: Optional[QueryParamTypes] = None,
152202
headers: Optional[Dict[str, str]] = None,
153203
token: Optional[Token] = None,
154-
**kwargs,
204+
**kwargs: Any,
155205
) -> httpx.Response:
156206
"""
157207
Send a PATCH request.
@@ -160,41 +210,66 @@ async def patch(
160210
url: The URL path or full URL.
161211
data: The request body.
162212
headers: Optional per-request headers.
213+
params: Optional query parameters.
214+
content: The request body.
215+
files: The request files.
216+
json: The request JSON body.
163217
token: Optional per-request token (overrides default).
164218
**kwargs: Additional httpx.AsyncClient.patch arguments.
165219
166220
Returns:
167221
httpx.Response
168222
"""
169223
req_headers = await self._prepare_headers(headers, token)
170-
return await self.http.patch(url, data=data, headers=req_headers, **kwargs)
224+
return await self.http.patch(
225+
url,
226+
data=data,
227+
files=files,
228+
json=json,
229+
content=content,
230+
params=params,
231+
headers=req_headers,
232+
**kwargs,
233+
)
171234

172235
async def delete(
173-
self, url: str, *, headers: Optional[Dict[str, str]] = None, token: Optional[Token] = None, **kwargs
236+
self,
237+
url: str,
238+
*,
239+
headers: Optional[Dict[str, str]] = None,
240+
token: Optional[Token] = None,
241+
params: Optional[QueryParamTypes] = None,
242+
**kwargs: Any,
174243
) -> httpx.Response:
175244
"""
176245
Send a DELETE request.
177246
178247
Args:
179248
url: The URL path or full URL.
180249
headers: Optional per-request headers.
250+
params: Optional query parameters.
181251
token: Optional per-request token (overrides default).
182252
**kwargs: Additional httpx.AsyncClient.delete arguments.
183253
184254
Returns:
185255
httpx.Response
186256
"""
187257
req_headers = await self._prepare_headers(headers, token)
188-
return await self.http.delete(url, headers=req_headers, **kwargs)
258+
return await self.http.delete(url, headers=req_headers, params=params, **kwargs)
189259

190260
async def request(
191261
self,
192262
method: str,
193263
url: str,
194264
*,
265+
params: Optional[QueryParamTypes] = None,
195266
headers: Optional[Dict[str, str]] = None,
196267
token: Optional[Token] = None,
197-
**kwargs,
268+
content: Optional[RequestContent] = None,
269+
data: Optional[RequestData] = None,
270+
files: Optional[RequestFiles] = None,
271+
json: Optional[Any] = None,
272+
**kwargs: Any,
198273
) -> httpx.Response:
199274
"""
200275
Send a custom HTTP request.
@@ -203,14 +278,29 @@ async def request(
203278
method: HTTP method (GET, POST, etc).
204279
url: The URL path or full URL.
205280
headers: Optional per-request headers.
281+
params: Optional query parameters.
282+
content: The request body.
283+
data: The request body.
284+
files: The request files.
285+
json: The request JSON body.
206286
token: Optional per-request token (overrides default).
207287
**kwargs: Additional httpx.AsyncClient.request arguments.
208288
209289
Returns:
210290
httpx.Response
211291
"""
212292
req_headers = await self._prepare_headers(headers, token)
213-
return await self.http.request(method, url, headers=req_headers, **kwargs)
293+
return await self.http.request(
294+
method,
295+
url,
296+
headers=req_headers,
297+
params=params,
298+
content=content,
299+
data=data,
300+
files=files,
301+
json=json,
302+
**kwargs,
303+
)
214304

215305
async def _resolve_token(self, token: Optional[Token]) -> Optional[str]:
216306
"""
@@ -241,55 +331,55 @@ def _update_event_hooks(self) -> None:
241331
"""
242332
Internal: Update the httpx.AsyncClient event_hooks to match current interceptors.
243333
"""
244-
event_hooks_dict = {}
334+
event_hooks_dict: Dict[str, List[Callable[[Any], Any]]] = {}
245335
for hook in self._interceptors:
246336
if hasattr(hook, "request"):
247337

248-
def make_request_wrapper(h):
249-
async def wrapper(request):
338+
def _make_request_wrapper(h: Interceptor):
339+
async def wrapper(request: Request):
250340
ctx = InterceptorRequestContext(request, self.logger)
251341
result = h.request(ctx)
252-
if hasattr(result, "__await__"):
342+
if inspect.isawaitable(result):
253343
return await result
254344
return result
255345

256346
return wrapper
257347

258-
event_hooks_dict.setdefault("request", []).append(make_request_wrapper(hook))
348+
event_hooks_dict.setdefault("request", []).append(_make_request_wrapper(hook))
259349
if hasattr(hook, "response"):
260350

261-
def make_response_wrapper(h):
262-
async def wrapper(response):
351+
def _make_response_wrapper(h: Interceptor):
352+
async def wrapper(response: Response):
263353
ctx = InterceptorResponseContext(response, self.logger)
264354
result = h.response(ctx)
265-
if hasattr(result, "__await__"):
355+
if inspect.isawaitable(result):
266356
return await result
267357
return result
268358

269359
return wrapper
270360

271-
event_hooks_dict.setdefault("response", []).append(make_response_wrapper(hook))
361+
event_hooks_dict.setdefault("response", []).append(_make_response_wrapper(hook))
272362
self.http.event_hooks = event_hooks_dict
273363

274-
def clone(self, **overrides) -> "Client":
364+
def clone(self, overrides: Optional[ClientOptions] = None) -> "Client":
275365
"""
276366
Create a new Client instance with merged configuration.
277367
278368
Args:
279-
**overrides: Partial ClientOptions fields to override.
369+
overrides: Optional ClientOptions object to override fields.
280370
281371
Returns:
282372
A new Client instance with merged options and a cloned interceptor list.
283373
"""
284-
# Merge options, shallow copy interceptors array
374+
overrides = overrides or ClientOptions()
285375
merged_options = ClientOptions(
286-
base_url=overrides.get("base_url", self.options.base_url),
287-
headers={**self.options.headers, **overrides.get("headers", {})}
288-
if overrides.get("headers")
289-
else dict(self.options.headers),
290-
timeout=overrides.get("timeout", self.options.timeout),
291-
logger=overrides.get("logger", self.options.logger),
292-
token=overrides.get("token", self.options.token),
293-
interceptors=list(overrides.get("interceptors", self._interceptors)),
376+
base_url=overrides.base_url if overrides.base_url is not None else self.options.base_url,
377+
headers={**self.options.headers, **(overrides.headers or {})},
378+
timeout=overrides.timeout if overrides.timeout is not None else self.options.timeout,
379+
logger=overrides.logger if overrides.logger is not None else self.options.logger,
380+
token=overrides.token if overrides.token is not None else self.options.token,
381+
interceptors=list(overrides.interceptors)
382+
if overrides.interceptors is not None
383+
else list(self._interceptors),
294384
)
295385
return Client(merged_options)

0 commit comments

Comments
 (0)