Skip to content

Commit 0543b91

Browse files
authored
fix: do not mutate httpx client inside storage, postgrest and functions (#1249)
1 parent bc2cf08 commit 0543b91

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1269
-1158
lines changed

src/functions/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ requires-python = ">=3.9"
1515
dependencies = [
1616
"httpx[http2] >=0.26,<0.29",
1717
"strenum >=0.4.15",
18+
"yarl>=1.20.1",
1819
]
1920

2021

src/functions/src/supabase_functions/_async/functions_client.py

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from warnings import warn
33

44
from httpx import AsyncClient, HTTPError, Response, QueryParams
5+
from yarl import URL
56

67
from ..errors import FunctionsHttpError, FunctionsRelayError
78
from ..utils import (
@@ -24,7 +25,7 @@ def __init__(
2425
):
2526
if not is_http_url(url):
2627
raise ValueError("url must be a valid HTTP URL string")
27-
self.url = url
28+
self.url = URL(url)
2829
self.headers = {
2930
"User-Agent": f"supabase-py/functions-py v{__version__}",
3031
**headers,
@@ -51,37 +52,32 @@ def __init__(
5152

5253
self.verify = bool(verify) if verify is not None else True
5354
self.timeout = int(abs(timeout)) if timeout is not None else 60
54-
55-
if http_client is not None:
56-
http_client.base_url = self.url
57-
http_client.headers.update({**self.headers})
58-
self._client = http_client
59-
else:
60-
self._client = AsyncClient(
61-
base_url=self.url,
62-
headers=self.headers,
63-
verify=self.verify,
64-
timeout=self.timeout,
65-
proxy=proxy,
66-
follow_redirects=True,
67-
http2=True,
68-
)
55+
self._client = http_client or AsyncClient(
56+
verify=self.verify,
57+
timeout=self.timeout,
58+
proxy=proxy,
59+
follow_redirects=True,
60+
http2=True,
61+
)
6962

7063
async def _request(
7164
self,
7265
method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
73-
url: str,
66+
path: list[str],
7467
headers: Optional[Dict[str, str]] = None,
7568
json: Optional[Dict[Any, Any]] = None,
7669
params: Optional[QueryParams] = None,
7770
) -> Response:
71+
url = self.url.joinpath(*path)
72+
headers = headers or dict()
73+
headers.update(self.headers)
7874
response = (
7975
await self._client.request(
80-
method, url, data=json, headers=headers, params=params
76+
method, str(url), data=json, headers=headers, params=params
8177
)
8278
if isinstance(json, str)
8379
else await self._client.request(
84-
method, url, json=json, headers=headers, params=params
80+
method, str(url), json=json, headers=headers, params=params
8581
)
8682
)
8783
try:
@@ -129,7 +125,6 @@ async def invoke(
129125
params = QueryParams()
130126
body = None
131127
response_type = "text/plain"
132-
url = f"{self.url}/{function_name}"
133128

134129
if invoke_options is not None:
135130
headers.update(invoke_options.get("headers", {}))
@@ -153,7 +148,7 @@ async def invoke(
153148
headers["Content-Type"] = "application/json"
154149

155150
response = await self._request(
156-
"POST", url, headers=headers, json=body, params=params
151+
"POST", [function_name], headers=headers, json=body, params=params
157152
)
158153
is_relay_error = response.headers.get("x-relay-header")
159154

src/functions/src/supabase_functions/_sync/functions_client.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from warnings import warn
33

44
from httpx import Client, HTTPError, Response, QueryParams
5+
from yarl import URL
56

67
from ..errors import FunctionsHttpError, FunctionsRelayError
78
from ..utils import (
@@ -24,7 +25,7 @@ def __init__(
2425
):
2526
if not is_http_url(url):
2627
raise ValueError("url must be a valid HTTP URL string")
27-
self.url = url
28+
self.url = URL(url)
2829
self.headers = {
2930
"User-Agent": f"supabase-py/functions-py v{__version__}",
3031
**headers,
@@ -51,35 +52,32 @@ def __init__(
5152

5253
self.verify = bool(verify) if verify is not None else True
5354
self.timeout = int(abs(timeout)) if timeout is not None else 60
54-
55-
if http_client is not None:
56-
http_client.base_url = self.url
57-
http_client.headers.update({**self.headers})
58-
self._client = http_client
59-
else:
60-
self._client = Client(
61-
base_url=self.url,
62-
headers=self.headers,
63-
verify=self.verify,
64-
timeout=self.timeout,
65-
proxy=proxy,
66-
follow_redirects=True,
67-
http2=True,
68-
)
55+
self._client = http_client or Client(
56+
verify=self.verify,
57+
timeout=self.timeout,
58+
proxy=proxy,
59+
follow_redirects=True,
60+
http2=True,
61+
)
6962

7063
def _request(
7164
self,
7265
method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
73-
url: str,
66+
path: list[str],
7467
headers: Optional[Dict[str, str]] = None,
7568
json: Optional[Dict[Any, Any]] = None,
7669
params: Optional[QueryParams] = None,
7770
) -> Response:
71+
url = self.url.joinpath(*path)
72+
headers = headers or dict()
73+
headers.update(self.headers)
7874
response = (
79-
self._client.request(method, url, data=json, headers=headers, params=params)
75+
self._client.request(
76+
method, str(url), data=json, headers=headers, params=params
77+
)
8078
if isinstance(json, str)
8179
else self._client.request(
82-
method, url, json=json, headers=headers, params=params
80+
method, str(url), json=json, headers=headers, params=params
8381
)
8482
)
8583
try:
@@ -127,7 +125,6 @@ def invoke(
127125
params = QueryParams()
128126
body = None
129127
response_type = "text/plain"
130-
url = f"{self.url}/{function_name}"
131128

132129
if invoke_options is not None:
133130
headers.update(invoke_options.get("headers", {}))
@@ -150,7 +147,9 @@ def invoke(
150147
elif isinstance(body, dict):
151148
headers["Content-Type"] = "application/json"
152149

153-
response = self._request("POST", url, headers=headers, json=body, params=params)
150+
response = self._request(
151+
"POST", [function_name], headers=headers, json=body, params=params
152+
)
154153
is_relay_error = response.headers.get("x-relay-header")
155154

156155
if is_relay_error and is_relay_error == "true":

src/functions/tests/_async/test_function_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async def test_init_with_valid_params(valid_url, default_headers):
3131
client = AsyncFunctionsClient(
3232
url=valid_url, headers=default_headers, timeout=10, verify=True
3333
)
34-
assert client.url == valid_url
34+
assert str(client.url) == valid_url
3535
assert "User-Agent" in client.headers
3636
assert client.headers["User-Agent"] == f"supabase-py/functions-py v{__version__}"
3737
assert client._client.timeout == Timeout(10)

src/functions/tests/_sync/test_function_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def test_init_with_valid_params(valid_url, default_headers):
3131
client = SyncFunctionsClient(
3232
url=valid_url, headers=default_headers, timeout=10, verify=True
3333
)
34-
assert client.url == valid_url
34+
assert str(client.url) == valid_url
3535
assert "User-Agent" in client.headers
3636
assert client.headers["User-Agent"] == f"supabase-py/functions-py v{__version__}"
3737
assert client._client.timeout == Timeout(10)

src/functions/tests/test_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def test_create_async_client(valid_url, valid_headers):
2222
)
2323

2424
assert isinstance(client, AsyncFunctionsClient)
25-
assert client.url == valid_url
25+
assert str(client.url) == valid_url
2626
assert all(client.headers[key] == value for key, value in valid_headers.items())
2727

2828

@@ -33,7 +33,7 @@ def test_create_sync_client(valid_url, valid_headers):
3333
)
3434

3535
assert isinstance(client, SyncFunctionsClient)
36-
assert client.url == valid_url
36+
assert str(client.url) == valid_url
3737
assert all(client.headers[key] == value for key, value in valid_headers.items())
3838

3939

src/postgrest/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ unasync:
4848

4949
build-sync: unasync
5050
sed -i 's/@pytest.mark.asyncio//g' tests/_sync/test_client.py
51-
sed -i 's/_async/_sync/g' tests/_sync/test_client.py
51+
sed -i 's/_async/_sync/g' tests/_sync/test_client.py tests/_sync/test_query_request_builder.py tests/_sync/test_filter_request_builder.py
5252
sed -i 's/Async/Sync/g' src/postgrest/_sync/request_builder.py tests/_sync/test_client.py
5353
sed -i 's/_client\.SyncClient/_client\.Client/g' tests/_sync/test_client.py
5454
sed -i 's/SyncHTTPTransport/HTTPTransport/g' tests/_sync/**.py

src/postgrest/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies = [
2525
"deprecation >=2.1.0",
2626
"pydantic >=1.9,<3.0",
2727
"strenum >=0.4.9; python_version < \"3.11\"",
28+
"yarl>=1.20.1",
2829
]
2930

3031
[project.urls]

src/postgrest/src/postgrest/_async/client.py

Lines changed: 32 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from deprecation import deprecated
77
from httpx import AsyncClient, Headers, QueryParams, Timeout
8+
from yarl import URL
89

910
from ..base_client import BasePostgrestClient
1011
from ..constants import (
@@ -13,7 +14,11 @@
1314
)
1415
from ..types import CountMethod
1516
from ..version import __version__
16-
from .request_builder import AsyncRequestBuilder, AsyncRPCFilterRequestBuilder
17+
from .request_builder import (
18+
AsyncRequestBuilder,
19+
AsyncRPCFilterRequestBuilder,
20+
RequestConfig,
21+
)
1722

1823

1924
class AsyncPostgrestClient(BasePostgrestClient):
@@ -59,52 +64,32 @@ def __init__(
5964
else DEFAULT_POSTGREST_CLIENT_TIMEOUT
6065
)
6166
)
62-
6367
BasePostgrestClient.__init__(
6468
self,
65-
base_url,
69+
URL(base_url),
6670
schema=schema,
6771
headers=headers,
6872
timeout=self.timeout,
6973
verify=self.verify,
7074
proxy=proxy,
71-
http_client=http_client,
7275
)
73-
self.session: AsyncClient = self.session
74-
75-
def create_session(
76-
self,
77-
base_url: str,
78-
headers: Dict[str, str],
79-
timeout: Union[int, float, Timeout],
80-
verify: bool = True,
81-
proxy: Optional[str] = None,
82-
) -> AsyncClient:
83-
http_client = None
84-
if isinstance(self.http_client, AsyncClient):
85-
http_client = self.http_client
8676

87-
if http_client is not None:
88-
http_client.base_url = base_url
89-
http_client.headers.update({**headers})
90-
return http_client
91-
92-
return AsyncClient(
77+
self.session = http_client or AsyncClient(
9378
base_url=base_url,
94-
headers=headers,
79+
headers=self.headers,
9580
timeout=timeout,
96-
verify=verify,
81+
verify=self.verify,
9782
proxy=proxy,
9883
follow_redirects=True,
9984
http2=True,
10085
)
10186

102-
def schema(self, schema: str):
87+
def schema(self, schema: str) -> AsyncPostgrestClient:
10388
"""Switch to another schema."""
10489
return AsyncPostgrestClient(
105-
base_url=self.base_url,
90+
base_url=str(self.base_url),
10691
schema=schema,
107-
headers=self.headers,
92+
headers=dict(self.headers),
10893
timeout=self.timeout,
10994
verify=self.verify,
11095
proxy=self.proxy,
@@ -128,7 +113,9 @@ def from_(self, table: str) -> AsyncRequestBuilder:
128113
Returns:
129114
:class:`AsyncRequestBuilder`
130115
"""
131-
return AsyncRequestBuilder(self.session, f"/{table}")
116+
return AsyncRequestBuilder(
117+
self.session, self.base_url.joinpath(table), self.headers, self.basic_auth
118+
)
132119

133120
def table(self, table: str) -> AsyncRequestBuilder:
134121
"""Alias to :meth:`from_`."""
@@ -142,7 +129,7 @@ def from_table(self, table: str) -> AsyncRequestBuilder:
142129
def rpc(
143130
self,
144131
func: str,
145-
params: dict,
132+
params: dict[str, str],
146133
count: Optional[CountMethod] = None,
147134
head: bool = False,
148135
get: bool = False,
@@ -171,17 +158,20 @@ def rpc(
171158
method = "HEAD" if head else "GET" if get else "POST"
172159

173160
headers = Headers({"Prefer": f"count={count}"}) if count else Headers()
174-
175-
if method in ("HEAD", "GET"):
176-
return AsyncRPCFilterRequestBuilder(
177-
self.session,
178-
f"/rpc/{func}",
179-
method,
180-
headers,
181-
QueryParams(params),
182-
json={},
183-
)
161+
headers.update(self.headers)
184162
# the params here are params to be sent to the RPC and not the queryparams!
185-
return AsyncRPCFilterRequestBuilder(
186-
self.session, f"/rpc/{func}", method, headers, QueryParams(), json=params
163+
json, http_params = (
164+
({}, QueryParams(params))
165+
if method in ("HEAD", "GET")
166+
else (params, QueryParams())
167+
)
168+
request = RequestConfig(
169+
self.session,
170+
self.base_url.joinpath("rpc", func),
171+
method,
172+
headers,
173+
http_params,
174+
self.basic_auth,
175+
json,
187176
)
177+
return AsyncRPCFilterRequestBuilder(request)

0 commit comments

Comments
 (0)