Skip to content

Commit 6ad261a

Browse files
committed
fix: add ApiClient.close() and release v0.2.2
Add RESTClientObject.close() and ApiClient.close() so callers can release urllib3 pools safely. Patch regen workflow to preserve the lifecycle hooks.
1 parent 15bfbe7 commit 6ad261a

7 files changed

Lines changed: 108 additions & 2 deletions

File tree

.github/workflows/regenerate.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ jobs:
110110
p.write_text(src)
111111
PY
112112
113+
- name: Patch ApiClient close lifecycle
114+
run: python3 scripts/patch_api_client_close.py
115+
113116
- name: Clean up generated artifacts
114117
run: |
115118
rm -f openapi.yaml

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.2.2] - 2026-05-20
11+
12+
### Fixed
13+
14+
- Add `ApiClient.close()` and `RESTClientObject.close()` so callers can release urllib3 connection pools and use context managers safely.
15+
1016
## [0.2.1] - 2026-05-20
1117

1218
### Changed

hotdata/api_client.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@ def __init__(
9797
def __enter__(self):
9898
return self
9999

100+
def close(self) -> None:
101+
if self.rest_client is not None:
102+
self.rest_client.close()
103+
100104
def __exit__(self, exc_type, exc_value, traceback):
101-
pass
105+
self.close()
102106

103107
@property
104108
def user_agent(self):

hotdata/rest.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ def __init__(self, configuration) -> None:
118118
else:
119119
self.pool_manager = urllib3.PoolManager(**pool_args)
120120

121+
def close(self) -> None:
122+
if self.pool_manager is not None:
123+
self.pool_manager.clear()
124+
121125
def request(
122126
self,
123127
method,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "hotdata"
3-
version = "0.2.1"
3+
version = "0.2.2"
44
description = "Hotdata API"
55
authors = [
66
{name = "Hotdata",email = "developers@hotdata.dev"},

scripts/patch_api_client_close.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env python3
2+
"""Re-apply ApiClient.close() after OpenAPI regeneration."""
3+
4+
from __future__ import annotations
5+
6+
import pathlib
7+
import sys
8+
9+
ROOT = pathlib.Path(__file__).resolve().parents[1]
10+
11+
12+
def patch_rest_client() -> None:
13+
path = ROOT / "hotdata" / "rest.py"
14+
src = path.read_text()
15+
needle = " self.pool_manager = urllib3.PoolManager(**pool_args)\n\n def request("
16+
insert = (
17+
" self.pool_manager = urllib3.PoolManager(**pool_args)\n\n"
18+
" def close(self) -> None:\n"
19+
" if self.pool_manager is not None:\n"
20+
" self.pool_manager.clear()\n\n"
21+
" def request("
22+
)
23+
if "def close(self) -> None:" in src and "pool_manager.clear()" in src:
24+
return
25+
if needle not in src:
26+
sys.exit(f"Failed to patch {path}: RESTClientObject anchor not found")
27+
path.write_text(src.replace(needle, insert, 1))
28+
29+
30+
def patch_api_client() -> None:
31+
path = ROOT / "hotdata" / "api_client.py"
32+
src = path.read_text()
33+
needle = (
34+
" def __enter__(self):\n"
35+
" return self\n\n"
36+
" def __exit__(self, exc_type, exc_value, traceback):\n"
37+
" pass\n"
38+
)
39+
replacement = (
40+
" def __enter__(self):\n"
41+
" return self\n\n"
42+
" def close(self) -> None:\n"
43+
" if self.rest_client is not None:\n"
44+
" self.rest_client.close()\n\n"
45+
" def __exit__(self, exc_type, exc_value, traceback):\n"
46+
" self.close()\n"
47+
)
48+
if "def close(self) -> None:" in src and "self.rest_client.close()" in src:
49+
return
50+
if needle not in src:
51+
sys.exit(f"Failed to patch {path}: ApiClient context manager anchor not found")
52+
path.write_text(src.replace(needle, replacement, 1))
53+
54+
55+
def main() -> None:
56+
patch_rest_client()
57+
patch_api_client()
58+
59+
60+
if __name__ == "__main__":
61+
main()

test/test_api_client_close.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import unittest
2+
3+
from hotdata import ApiClient
4+
5+
6+
class TestApiClientClose(unittest.TestCase):
7+
def test_close_does_not_raise(self) -> None:
8+
client = ApiClient()
9+
client.close()
10+
11+
def test_context_manager_calls_close(self) -> None:
12+
client = ApiClient()
13+
called = False
14+
original_close = client.rest_client.close
15+
16+
def tracked_close() -> None:
17+
nonlocal called
18+
called = True
19+
original_close()
20+
21+
client.rest_client.close = tracked_close
22+
with client:
23+
pass
24+
self.assertTrue(called)
25+
26+
27+
if __name__ == "__main__":
28+
unittest.main()

0 commit comments

Comments
 (0)