Skip to content

Commit 8a00c4c

Browse files
authored
Register close with exit handler (#94)
1 parent e943282 commit 8a00c4c

File tree

2 files changed

+104
-4
lines changed

2 files changed

+104
-4
lines changed

keystone_client/http.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"""
88

99
import abc
10+
import atexit
1011
import logging
1112
import re
1213
import uuid
@@ -73,9 +74,7 @@ def __init__(
7374
transport=transport,
7475
)
7576

76-
@abc.abstractmethod
77-
def _client_factory(self, **kwargs) -> Union[httpx.Client, httpx.AsyncClient]:
78-
"""Create a new HTTP client instance with the provided settings."""
77+
atexit.register(self.close)
7978

8079
@property
8180
def base_url(self) -> str:
@@ -116,6 +115,75 @@ def get_application_headers(self, overrides: Union[dict, None] = None) -> dict[s
116115

117116
return headers
118117

118+
@abc.abstractmethod
119+
def _client_factory(self, **kwargs) -> Union[httpx.Client, httpx.AsyncClient]:
120+
"""Create a new HTTP client instance with the provided settings."""
121+
122+
@abc.abstractmethod
123+
def close(self) -> None:
124+
"""Close any open server connections."""
125+
126+
@abc.abstractmethod
127+
def send_request(
128+
self,
129+
method: HttpMethod,
130+
endpoint: str,
131+
*,
132+
headers: Optional[dict] = None,
133+
json: Optional[RequestContent] = None,
134+
files: Optional[RequestFiles] = None,
135+
params: Optional[QueryParamTypes] = None,
136+
timeout: int = httpx.USE_CLIENT_DEFAULT,
137+
) -> httpx.Response:
138+
"""Send an HTTP request (sync or async depending on the implementation)."""
139+
140+
@abc.abstractmethod
141+
def http_get(
142+
self,
143+
endpoint: str,
144+
params: Optional[QueryParamTypes] = None,
145+
timeout: int = httpx.USE_CLIENT_DEFAULT,
146+
) -> httpx.Response:
147+
"""Send a GET request."""
148+
149+
@abc.abstractmethod
150+
def http_post(
151+
self,
152+
endpoint: str,
153+
json: Optional[RequestData] = None,
154+
files: Optional[RequestFiles] = None,
155+
timeout: int = httpx.USE_CLIENT_DEFAULT,
156+
) -> httpx.Response:
157+
"""Send a POST request."""
158+
159+
@abc.abstractmethod
160+
def http_patch(
161+
self,
162+
endpoint: str,
163+
json: Optional[RequestData] = None,
164+
files: Optional[RequestFiles] = None,
165+
timeout: int = httpx.USE_CLIENT_DEFAULT,
166+
) -> httpx.Response:
167+
"""Send a PATCH request."""
168+
169+
@abc.abstractmethod
170+
def http_put(
171+
self,
172+
endpoint: str,
173+
json: Optional[RequestData] = None,
174+
files: Optional[RequestFiles] = None,
175+
timeout: int = httpx.USE_CLIENT_DEFAULT,
176+
) -> httpx.Response:
177+
"""Send a PUT request."""
178+
179+
@abc.abstractmethod
180+
def http_delete(
181+
self,
182+
endpoint: str,
183+
timeout: int = httpx.USE_CLIENT_DEFAULT,
184+
) -> httpx.Response:
185+
"""Send a DELETE request."""
186+
119187

120188
class HTTPClient(HTTPBase):
121189
"""Synchronous HTTP Client."""

tests/unit_tests/test_http/test_HTTPBase.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,35 @@
22

33
import uuid
44
from unittest import TestCase
5-
from unittest.mock import MagicMock
5+
from unittest.mock import MagicMock, patch
66

77
from keystone_client.http import HTTPBase
88

99

1010
class DummyHTTPBase(HTTPBase):
1111
"""Concrete subclass of HTTPBase for testing."""
1212

13+
def send_request(self, *args, **kwargs) -> None:
14+
"""Method required by abstract parent for sending HTTP requests."""
15+
16+
def http_get(self, *args, **kwargs) -> None:
17+
"""Method required by abstract parent for sending GET requests."""
18+
19+
def http_post(self, *args, **kwargs) -> None:
20+
"""Method required by abstract parent for sending POST requests."""
21+
22+
def http_patch(self, *args, **kwargs) -> None:
23+
"""Method required by abstract parent for sending PATCH requests."""
24+
25+
def http_put(self, *args, **kwargs) -> None:
26+
"""Method required by abstract parent for sending PUT requests."""
27+
28+
def http_delete(self, *args, **kwargs) -> None:
29+
"""Method required by abstract parent for sending DELETE requests."""
30+
31+
def close(self) -> None:
32+
"""Method required by abstract parent for cleaning up open resources."""
33+
1334
def _client_factory(self, **kwargs) -> MagicMock:
1435
"""Create a mock object as a stand in for an HTTP client."""
1536

@@ -117,3 +138,14 @@ def test_header_overrides_replace_existing_header(self) -> None:
117138
headers = self.http_base.get_application_headers(overrides)
118139

119140
self.assertEqual(custom_cid, headers[HTTPBase.CID_HEADER])
141+
142+
143+
class CloseAtExit(TestCase):
144+
"""Test resource cleanup at application exit."""
145+
146+
@patch('atexit.register')
147+
def test_close_registered_with_atexit(self, mock_atexit_register: MagicMock) -> None:
148+
"""Verify the `close` method is registered with `atexit` on initialization."""
149+
150+
client = DummyHTTPBase('https://example.com/')
151+
mock_atexit_register.assert_called_once_with(client.close)

0 commit comments

Comments
 (0)