Skip to content
Open
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
1 change: 1 addition & 0 deletions sdk-compliance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ features:

database.configuration.auto_retry:
status: implemented
note: "Per request via .retry(); client-wide via the retry_enabled constructor option on AsyncPostgrestClient/SyncPostgrestClient, exposed as ClientOptions.postgrest_client_retry."
symbols:
- AsyncSelectRequestBuilder.retry
- SyncSelectRequestBuilder.retry
Expand Down
10 changes: 9 additions & 1 deletion src/postgrest/src/postgrest/_async/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def __init__(
verify: Optional[bool] = None,
proxy: Optional[str] = None,
http_client: Optional[AsyncClient] = None,
retry_enabled: bool = True,
) -> None:
headers = {
"X-Client-Info": (
Expand Down Expand Up @@ -93,6 +94,7 @@ def __init__(
verify=self.verify,
proxy=proxy,
)
self.retry_enabled = retry_enabled

self.session = http_client or AsyncClient(
base_url=base_url,
Expand All @@ -113,6 +115,7 @@ def schema(self, schema: str) -> AsyncPostgrestClient:
timeout=self.timeout,
verify=self.verify,
proxy=self.proxy,
retry_enabled=self.retry_enabled,
)

async def __aenter__(self) -> AsyncPostgrestClient:
Expand All @@ -134,7 +137,11 @@ def from_(self, table: str) -> AsyncRequestBuilder:
:class:`AsyncRequestBuilder`
"""
return AsyncRequestBuilder(
self.session, self.base_url.joinpath(table), self.headers, self.basic_auth
self.session,
self.base_url.joinpath(table),
self.headers,
self.basic_auth,
retry_enabled=self.retry_enabled,
)

def table(self, table: str) -> AsyncRequestBuilder:
Expand Down Expand Up @@ -193,5 +200,6 @@ def rpc(
http_params,
self.basic_auth,
json,
retry_enabled=self.retry_enabled,
)
return AsyncRPCFilterRequestBuilder(request)
13 changes: 12 additions & 1 deletion src/postgrest/src/postgrest/_async/request_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,12 +294,18 @@ def explain(

class AsyncRequestBuilder: #
def __init__(
self, session: AsyncClient, path: URL, headers: Headers, auth: BasicAuth | None
self,
session: AsyncClient,
path: URL,
headers: Headers,
auth: BasicAuth | None,
retry_enabled: bool = True,
) -> None:
self.session = session
self.path = path
self.headers = headers
self.auth = auth
self.retry_enabled = retry_enabled

def select(
self,
Expand All @@ -325,6 +331,7 @@ def select(
http_method=method,
headers=headers,
json=json,
retry_enabled=self.retry_enabled,
)
return AsyncSelectRequestBuilder(request)

Expand Down Expand Up @@ -366,6 +373,7 @@ def insert(
http_method=method,
headers=headers,
json=json,
retry_enabled=self.retry_enabled,
)
return AsyncQueryRequestBuilder(request)

Expand Down Expand Up @@ -411,6 +419,7 @@ def upsert(
http_method=method,
headers=headers,
json=json,
retry_enabled=self.retry_enabled,
)
return AsyncQueryRequestBuilder(request)

Expand Down Expand Up @@ -444,6 +453,7 @@ def update(
http_method=method,
headers=headers,
json=json,
retry_enabled=self.retry_enabled,
)
return AsyncFilterRequestBuilder(request)

Expand Down Expand Up @@ -474,5 +484,6 @@ def delete(
http_method=method,
headers=headers,
json=json,
retry_enabled=self.retry_enabled,
)
return AsyncFilterRequestBuilder(request)
10 changes: 9 additions & 1 deletion src/postgrest/src/postgrest/_sync/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def __init__(
verify: Optional[bool] = None,
proxy: Optional[str] = None,
http_client: Optional[Client] = None,
retry_enabled: bool = True,
) -> None:
headers = {
"X-Client-Info": (
Expand Down Expand Up @@ -93,6 +94,7 @@ def __init__(
verify=self.verify,
proxy=proxy,
)
self.retry_enabled = retry_enabled

self.session = http_client or Client(
base_url=base_url,
Expand All @@ -113,6 +115,7 @@ def schema(self, schema: str) -> SyncPostgrestClient:
timeout=self.timeout,
verify=self.verify,
proxy=self.proxy,
retry_enabled=self.retry_enabled,
)

def __enter__(self) -> SyncPostgrestClient:
Expand All @@ -134,7 +137,11 @@ def from_(self, table: str) -> SyncRequestBuilder:
:class:`AsyncRequestBuilder`
"""
return SyncRequestBuilder(
self.session, self.base_url.joinpath(table), self.headers, self.basic_auth
self.session,
self.base_url.joinpath(table),
self.headers,
self.basic_auth,
retry_enabled=self.retry_enabled,
)

def table(self, table: str) -> SyncRequestBuilder:
Expand Down Expand Up @@ -193,5 +200,6 @@ def rpc(
http_params,
self.basic_auth,
json,
retry_enabled=self.retry_enabled,
)
return SyncRPCFilterRequestBuilder(request)
13 changes: 12 additions & 1 deletion src/postgrest/src/postgrest/_sync/request_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,12 +294,18 @@ def explain(

class SyncRequestBuilder: #
def __init__(
self, session: Client, path: URL, headers: Headers, auth: BasicAuth | None
self,
session: Client,
path: URL,
headers: Headers,
auth: BasicAuth | None,
retry_enabled: bool = True,
) -> None:
self.session = session
self.path = path
self.headers = headers
self.auth = auth
self.retry_enabled = retry_enabled

def select(
self,
Expand All @@ -325,6 +331,7 @@ def select(
http_method=method,
headers=headers,
json=json,
retry_enabled=self.retry_enabled,
)
return SyncSelectRequestBuilder(request)

Expand Down Expand Up @@ -366,6 +373,7 @@ def insert(
http_method=method,
headers=headers,
json=json,
retry_enabled=self.retry_enabled,
)
return SyncQueryRequestBuilder(request)

Expand Down Expand Up @@ -411,6 +419,7 @@ def upsert(
http_method=method,
headers=headers,
json=json,
retry_enabled=self.retry_enabled,
)
return SyncQueryRequestBuilder(request)

Expand Down Expand Up @@ -444,6 +453,7 @@ def update(
http_method=method,
headers=headers,
json=json,
retry_enabled=self.retry_enabled,
)
return SyncFilterRequestBuilder(request)

Expand Down Expand Up @@ -474,5 +484,6 @@ def delete(
http_method=method,
headers=headers,
json=json,
retry_enabled=self.retry_enabled,
)
return SyncFilterRequestBuilder(request)
81 changes: 80 additions & 1 deletion src/postgrest/tests/_async/test_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from unittest.mock import patch
from unittest.mock import AsyncMock, patch

import pytest
from httpx import (
Expand All @@ -14,6 +14,7 @@
)

from postgrest import AsyncPostgrestClient
from postgrest.base_request_builder import MAX_RETRIES
from postgrest.exceptions import APIError


Expand Down Expand Up @@ -176,3 +177,81 @@ async def test_response_client_invalid_response_but_valid_json(
assert isinstance(exc_response.get("message"), str)
assert exc_response.get("message") == "JSON could not be generated"
assert "code" in exc_response and int(exc_response["code"]) == 502


class TestRetryEnabled:
def test_default_enabled(self, postgrest_client: AsyncPostgrestClient):
assert postgrest_client.retry_enabled is True
assert postgrest_client.from_("test").select("*").request.retry_enabled is True

async def test_client_level_disable_propagates(self):
async with AsyncPostgrestClient(
"https://example.com", retry_enabled=False
) as client:
assert client.from_("test").select("*").request.retry_enabled is False
assert client.rpc("test_fn", {}).request.retry_enabled is False
assert client.schema("other").retry_enabled is False

async def test_request_level_override(self):
async with AsyncPostgrestClient(
"https://example.com", retry_enabled=False
) as client:
builder = client.from_("test").select("*").retry(True)
assert builder.request.retry_enabled is True

async def test_client_level_disable_does_not_retry_on_503(self):
calls = 0

async def fake_send(request: Request, **kwargs):
nonlocal calls
calls += 1
return Response(503)

async with AsyncPostgrestClient(
"https://example.com", retry_enabled=False
) as client:
with patch.object(client.session, "send", wraps=fake_send):
with pytest.raises(APIError):
await client.from_("test").select("*").execute()

assert calls == 1

async def test_client_level_disable_request_override_retries(self):
calls = 0

async def fake_send(request: Request, **kwargs):
nonlocal calls
if calls > 0:
assert request.headers["X-Retry-Count"] == str(calls)
calls += 1
return Response(503)

async with AsyncPostgrestClient(
"https://example.com", retry_enabled=False
) as client:
with (
patch.object(client.session, "send", wraps=fake_send),
patch("asyncio.sleep", new=AsyncMock()),
):
with pytest.raises(APIError):
await client.from_("test").select("*").retry(True).execute()

assert calls == 1 + MAX_RETRIES

async def test_default_retries_on_503(self):
calls = 0

async def fake_send(request: Request, **kwargs):
nonlocal calls
calls += 1
return Response(503)

async with AsyncPostgrestClient("https://example.com") as client:
with (
patch.object(client.session, "send", wraps=fake_send),
patch("asyncio.sleep", new=AsyncMock()),
):
with pytest.raises(APIError):
await client.from_("test").select("*").execute()

assert calls == 1 + MAX_RETRIES
73 changes: 72 additions & 1 deletion src/postgrest/tests/_sync/test_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from unittest.mock import patch
from unittest.mock import Mock, patch

import pytest
from httpx import (
Expand All @@ -14,6 +14,7 @@
)

from postgrest import SyncPostgrestClient
from postgrest.base_request_builder import MAX_RETRIES
from postgrest.exceptions import APIError


Expand Down Expand Up @@ -174,3 +175,73 @@ def test_response_client_invalid_response_but_valid_json(
assert isinstance(exc_response.get("message"), str)
assert exc_response.get("message") == "JSON could not be generated"
assert "code" in exc_response and int(exc_response["code"]) == 502


class TestRetryEnabled:
def test_default_enabled(self, postgrest_client: SyncPostgrestClient):
assert postgrest_client.retry_enabled is True
assert postgrest_client.from_("test").select("*").request.retry_enabled is True

def test_client_level_disable_propagates(self):
with SyncPostgrestClient("https://example.com", retry_enabled=False) as client:
assert client.from_("test").select("*").request.retry_enabled is False
assert client.rpc("test_fn", {}).request.retry_enabled is False
assert client.schema("other").retry_enabled is False

def test_request_level_override(self):
with SyncPostgrestClient("https://example.com", retry_enabled=False) as client:
builder = client.from_("test").select("*").retry(True)
assert builder.request.retry_enabled is True

def test_client_level_disable_does_not_retry_on_503(self):
calls = 0

def fake_send(request: Request, **kwargs):
nonlocal calls
calls += 1
return Response(503)

with SyncPostgrestClient("https://example.com", retry_enabled=False) as client:
with patch.object(client.session, "send", wraps=fake_send):
with pytest.raises(APIError):
client.from_("test").select("*").execute()

assert calls == 1

def test_client_level_disable_request_override_retries(self):
calls = 0

def fake_send(request: Request, **kwargs):
nonlocal calls
if calls > 0:
assert request.headers["X-Retry-Count"] == str(calls)
calls += 1
return Response(503)

with SyncPostgrestClient("https://example.com", retry_enabled=False) as client:
with (
patch.object(client.session, "send", wraps=fake_send),
patch("time.sleep", new=Mock()),
):
with pytest.raises(APIError):
client.from_("test").select("*").retry(True).execute()

assert calls == 1 + MAX_RETRIES

def test_default_retries_on_503(self):
calls = 0

def fake_send(request: Request, **kwargs):
nonlocal calls
calls += 1
return Response(503)

with SyncPostgrestClient("https://example.com") as client:
with (
patch.object(client.session, "send", wraps=fake_send),
patch("time.sleep", new=Mock()),
):
with pytest.raises(APIError):
client.from_("test").select("*").execute()

assert calls == 1 + MAX_RETRIES
Loading