Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ import os
from sumup import Sumup

client = Sumup(api_key="sup_sk_MvxmLOl0...")

# Get merchant profile
merchant = client.merchants.get(merchant_code=os.environ["SUMUP_MERCHANT_CODE"])
print(f"Merchant: {merchant.merchant_code}")
```
Expand All @@ -51,15 +49,35 @@ from sumup import AsyncSumup

async def main():
client = AsyncSumup(api_key="sup_sk_MvxmLOl0...")

# Get merchant profile
merchant = await client.merchants.get(merchant_code=os.environ["SUMUP_MERCHANT_CODE"])
print(f"Merchant: {merchant.merchant_code}")


asyncio.run(main())
```

### Client Cleanup

Use clients directly as shown above, or use a context manager to close connections
automatically when the block exits:

```python
with Sumup(api_key="sup_sk_MvxmLOl0...") as client:
merchant = client.merchants.get(merchant_code=os.environ["SUMUP_MERCHANT_CODE"])
```

For async code, use `async with`:

```python
async def main():
async with AsyncSumup(api_key="sup_sk_MvxmLOl0...") as client:
merchant = await client.merchants.get(merchant_code=os.environ["SUMUP_MERCHANT_CODE"])
```

For long-running applications, reuse a client across requests and call `client.close()`
or `await client.aclose()` during shutdown, after outstanding work finishes.
A closed client cannot be reused.

### Creating a Checkout

```python
Expand Down
36 changes: 36 additions & 0 deletions codegen/templates/client.py.tmpl
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Code generated by `py-sdk-gen`. DO NOT EDIT.
import os
from types import TracebackType
import typing
import httpx
from typing_extensions import Self

from ._service import Resource, AsyncResource, runtime_headers
if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -42,6 +44,23 @@ class Sumup(Resource):
},
))

def close(self) -> None:
"""Close the underlying HTTP client and release its connections."""
self._client.close()

def __enter__(self) -> Self:
self._client.__enter__()
return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
"""Release connections without suppressing exceptions from the block."""
self._client.__exit__(exc_type, exc_value, traceback)

def events_handler(
self,
secret: str,
Expand Down Expand Up @@ -150,6 +169,23 @@ class AsyncSumup(AsyncResource):
},
))

async def aclose(self) -> None:
"""Close the underlying HTTP client and release its connections."""
await self._client.aclose()

async def __aenter__(self) -> Self:
await self._client.__aenter__()
return self

async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
"""Release connections without suppressing exceptions from the block."""
await self._client.__aexit__(exc_type, exc_value, traceback)

def events_handler(
self,
secret: str,
Expand Down
6 changes: 4 additions & 2 deletions examples/events-fastapi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@

@asynccontextmanager
async def lifespan(_app: FastAPI):
yield
await client._client.aclose()
try:
yield
finally:
await client.aclose()


app = FastAPI(lifespan=lifespan)
Expand Down
2 changes: 1 addition & 1 deletion examples/events-flask/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ def receive_event():
try:
app.run(port=8080)
finally:
client._client.close()
client.close()
36 changes: 36 additions & 0 deletions sumup/_client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Code generated by `py-sdk-gen`. DO NOT EDIT.
import os
import typing
from types import TracebackType

import httpx
from typing_extensions import Self

from ._service import AsyncResource, Resource, runtime_headers

Expand Down Expand Up @@ -60,6 +62,23 @@ def __init__(
)
)

def close(self) -> None:
"""Close the underlying HTTP client and release its connections."""
self._client.close()

def __enter__(self) -> Self:
self._client.__enter__()
return self

def __exit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
"""Release connections without suppressing exceptions from the block."""
self._client.__exit__(exc_type, exc_value, traceback)

def events_handler(
self,
secret: str,
Expand Down Expand Up @@ -233,6 +252,23 @@ def __init__(
)
)

async def aclose(self) -> None:
"""Close the underlying HTTP client and release its connections."""
await self._client.aclose()

async def __aenter__(self) -> Self:
await self._client.__aenter__()
return self

async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_value: BaseException | None,
traceback: TracebackType | None,
) -> None:
"""Release connections without suppressing exceptions from the block."""
await self._client.__aexit__(exc_type, exc_value, traceback)

def events_handler(
self,
secret: str,
Expand Down
98 changes: 98 additions & 0 deletions tests/test_client_lifecycle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import asyncio

import httpx
import pytest

from sumup import AsyncSumup, Sumup


class TrackingTransport(httpx.MockTransport):
def __init__(self):
super().__init__(lambda request: httpx.Response(200, json={"items": []}))
self.close_calls = 0

def close(self):
self.close_calls += 1

async def aclose(self):
self.close_calls += 1


@pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"])
def test_explicit_close(use_async, monkeypatch):
transport = TrackingTransport()
if use_async:
http_client = httpx.AsyncClient(base_url="https://api.sumup.test", transport=transport)
monkeypatch.setattr("sumup._client.httpx.AsyncClient", lambda **kwargs: http_client)

async def run():
client = AsyncSumup(api_key="test")
await client.aclose()
await client.aclose()
assert client._client.is_closed

asyncio.run(run())
else:
http_client = httpx.Client(base_url="https://api.sumup.test", transport=transport)
monkeypatch.setattr("sumup._client.httpx.Client", lambda **kwargs: http_client)
client = Sumup(api_key="test")
client.close()
client.close()
assert client._client.is_closed

assert transport.close_calls == 1


@pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"])
@pytest.mark.parametrize("raise_error", [False, True], ids=["success", "exception"])
def test_context_manager_closes_transport(use_async, raise_error, monkeypatch):
error = ValueError("processing failed")

# Inject a transport so requests and cleanup can be tested without network access.
transport = TrackingTransport()
if use_async:
http_client = httpx.AsyncClient(base_url="https://api.sumup.test", transport=transport)
monkeypatch.setattr("sumup._client.httpx.AsyncClient", lambda **kwargs: http_client)

async def run():
client = AsyncSumup(api_key="test")
try:
async with client as entered:
assert entered is client
assert not http_client.is_closed
page = await client.transactions.list("merchant-123")
assert page.items == []
if raise_error:
raise error
except ValueError as caught:
assert raise_error
assert caught is error
else:
assert not raise_error
assert http_client.is_closed
with pytest.raises(RuntimeError, match="closed"):
await client.transactions.list("merchant-123")

asyncio.run(run())
else:
http_client = httpx.Client(base_url="https://api.sumup.test", transport=transport)
monkeypatch.setattr("sumup._client.httpx.Client", lambda **kwargs: http_client)
client = Sumup(api_key="test")
try:
with client as entered:
assert entered is client
assert not http_client.is_closed
page = client.transactions.list("merchant-123")
assert page.items == []
if raise_error:
raise error
except ValueError as caught:
assert raise_error
assert caught is error
else:
assert not raise_error
assert http_client.is_closed
with pytest.raises(RuntimeError, match="closed"):
client.transactions.list("merchant-123")

assert transport.close_calls == 1
Loading