Skip to content

Commit

Permalink
feat(client): add client close handlers (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
stainless-bot authored and cleb11 committed Aug 1, 2023
1 parent 81d62ae commit c0af2dd
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 32 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,10 @@ client = ModernTreasury(

See the httpx documentation for information about the [`proxies`](https://www.python-httpx.org/advanced/#http-proxying) and [`transport`](https://www.python-httpx.org/advanced/#custom-transports) keyword arguments.

## Advanced: Managing HTTP resources

By default we will close the underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__) is called but you can also manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting.

# Migration guide

This section outlines the features that were deprecated in `v0.5.0`, and subsequently removed in `v0.6.0` and how to migrate your code.
Expand Down
43 changes: 43 additions & 0 deletions src/modern_treasury/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import uuid
import inspect
import platform
from types import TracebackType
from random import random
from typing import (
Any,
Expand Down Expand Up @@ -677,6 +678,27 @@ def __init__(
headers={"Accept": "application/json"},
)

def is_closed(self) -> bool:
return self._client.is_closed

def close(self) -> None:
"""Close the underlying HTTPX client.
The client will *not* be usable after this.
"""
self._client.close()

def __enter__(self: _T) -> _T:
return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.close()

@overload
def request(
self,
Expand Down Expand Up @@ -1009,6 +1031,27 @@ def __init__(
headers={"Accept": "application/json"},
)

def is_closed(self) -> bool:
return self._client.is_closed

async def close(self) -> None:
"""Close the underlying HTTPX client.
The client will *not* be usable after this.
"""
await self._client.aclose()

async def __aenter__(self: _T) -> _T:
return self

async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
await self.close()

@overload
async def request(
self,
Expand Down
10 changes: 10 additions & 0 deletions src/modern_treasury/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import os
import base64
import asyncio
from typing import Union, Mapping, Optional

import httpx
Expand Down Expand Up @@ -256,6 +257,9 @@ def copy(
# client.with_options(timeout=10).foo.create(...)
with_options = copy

def __del__(self) -> None:
self.close()

def ping(
self,
*,
Expand Down Expand Up @@ -488,6 +492,12 @@ def copy(
# client.with_options(timeout=10).foo.create(...)
with_options = copy

def __del__(self) -> None:
try:
asyncio.get_running_loop().create_task(self.close())
except Exception:
pass

async def ping(
self,
*,
Expand Down
74 changes: 42 additions & 32 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import os
import json
import asyncio
import inspect
from typing import Any, Dict, Union, cast

Expand Down Expand Up @@ -182,22 +183,6 @@ def test_default_headers_option(self) -> None:
assert request.headers.get("x-foo") == "stainless"
assert request.headers.get("x-stainless-lang") == "my-overriding-header"

def test_validate_headers(self) -> None:
client = ModernTreasury(
base_url=base_url, api_key=api_key, _strict_response_validation=True, organization_id="my-organization-ID"
)
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
assert "Basic" in request.headers.get("Authorization")

with pytest.raises(
Exception,
match="The api_key client option must be set either by passing api_key to the client or by setting the MODERN_TREASURY_API_KEY environment variable",
):
client2 = ModernTreasury(
base_url=base_url, api_key=None, _strict_response_validation=True, organization_id="my-organization-ID"
)
client2._build_request(FinalRequestOptions(method="get", url="/foo"))

def test_default_query_option(self) -> None:
client = ModernTreasury(
base_url=base_url,
Expand Down Expand Up @@ -416,6 +401,26 @@ def test_base_url_no_trailing_slash(self) -> None:
)
assert request.url == "http://localhost:5000/custom/path/foo"

def test_client_del(self) -> None:
client = ModernTreasury(
base_url=base_url, api_key=api_key, _strict_response_validation=True, organization_id="my-organization-ID"
)
assert not client.is_closed()

client.__del__()

assert client.is_closed()

def test_client_context_manager(self) -> None:
client = ModernTreasury(
base_url=base_url, api_key=api_key, _strict_response_validation=True, organization_id="my-organization-ID"
)
with client as c2:
assert c2 is client
assert not c2.is_closed()
assert not client.is_closed()
assert client.is_closed()


class TestAsyncModernTreasury:
client = AsyncModernTreasury(
Expand Down Expand Up @@ -574,22 +579,6 @@ def test_default_headers_option(self) -> None:
assert request.headers.get("x-foo") == "stainless"
assert request.headers.get("x-stainless-lang") == "my-overriding-header"

def test_validate_headers(self) -> None:
client = AsyncModernTreasury(
base_url=base_url, api_key=api_key, _strict_response_validation=True, organization_id="my-organization-ID"
)
request = client._build_request(FinalRequestOptions(method="get", url="/foo"))
assert "Basic" in request.headers.get("Authorization")

with pytest.raises(
Exception,
match="The api_key client option must be set either by passing api_key to the client or by setting the MODERN_TREASURY_API_KEY environment variable",
):
client2 = AsyncModernTreasury(
base_url=base_url, api_key=None, _strict_response_validation=True, organization_id="my-organization-ID"
)
client2._build_request(FinalRequestOptions(method="get", url="/foo"))

def test_default_query_option(self) -> None:
client = AsyncModernTreasury(
base_url=base_url,
Expand Down Expand Up @@ -807,3 +796,24 @@ def test_base_url_no_trailing_slash(self) -> None:
),
)
assert request.url == "http://localhost:5000/custom/path/foo"

async def test_client_del(self) -> None:
client = AsyncModernTreasury(
base_url=base_url, api_key=api_key, _strict_response_validation=True, organization_id="my-organization-ID"
)
assert not client.is_closed()

client.__del__()

await asyncio.sleep(0.2)
assert client.is_closed()

async def test_client_context_manager(self) -> None:
client = AsyncModernTreasury(
base_url=base_url, api_key=api_key, _strict_response_validation=True, organization_id="my-organization-ID"
)
async with client as c2:
assert c2 is client
assert not c2.is_closed()
assert not client.is_closed()
assert client.is_closed()

0 comments on commit c0af2dd

Please sign in to comment.