Skip to content

Commit 735cefd

Browse files
authored
Implement counting feature (#26)
1 parent 3e7b60e commit 735cefd

File tree

2 files changed

+124
-37
lines changed

2 files changed

+124
-37
lines changed

postgrest_py/request_builder.py

+54-16
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,59 @@
1-
from typing import Any, Iterable, Tuple, Dict, Optional
1+
import re
2+
import sys
3+
from typing import Any, Dict, Iterable, Optional, Tuple, Union
4+
5+
if sys.version_info < (3, 8):
6+
from typing_extensions import Literal
7+
else:
8+
from typing import Literal
29

310
from deprecation import deprecated
411
from httpx import AsyncClient
512

613
from postgrest_py.__version__ import __version__
714
from postgrest_py.utils import sanitize_param, sanitize_pattern_param
815

16+
CountMethod = Union[Literal["exact"], Literal["planned"], Literal["estimated"]]
17+
918

1019
class RequestBuilder:
1120
def __init__(self, session: AsyncClient, path: str):
1221
self.session = session
1322
self.path = path
1423

15-
def select(self, *columns: str):
16-
self.session.params = self.session.params.set(
17-
"select", ",".join(columns))
18-
return SelectRequestBuilder(self.session, self.path, "GET", {})
19-
20-
def insert(self, json: dict, *, upsert=False):
21-
self.session.headers[
22-
"Prefer"
23-
] = f"return=representation{',resolution=merge-duplicates' if upsert else ''}"
24+
def select(self, *columns: str, count: Optional[CountMethod] = None):
25+
if columns:
26+
method = "GET"
27+
self.session.params = self.session.params.set("select", ",".join(columns))
28+
else:
29+
method = "HEAD"
30+
31+
if count:
32+
self.session.headers["Prefer"] = f"count={count}"
33+
34+
return SelectRequestBuilder(self.session, self.path, method, {})
35+
36+
def insert(self, json: dict, *, count: Optional[CountMethod] = None, upsert=False):
37+
prefer_headers = ["return=representation"]
38+
if count:
39+
prefer_headers.append(f"count={count}")
40+
if upsert:
41+
prefer_headers.append("resolution=merge-duplicates")
42+
self.session.headers["prefer"] = ",".join(prefer_headers)
2443
return QueryRequestBuilder(self.session, self.path, "POST", json)
2544

26-
def update(self, json: dict):
27-
self.session.headers["Prefer"] = "return=representation"
45+
def update(self, json: dict, *, count: Optional[CountMethod] = None):
46+
prefer_headers = ["return=representation"]
47+
if count:
48+
prefer_headers.append(f"count={count}")
49+
self.session.headers["prefer"] = ",".join(prefer_headers)
2850
return FilterRequestBuilder(self.session, self.path, "PATCH", json)
2951

30-
def delete(self):
52+
def delete(self, *, count: Optional[CountMethod] = None):
53+
prefer_headers = ["return=representation"]
54+
if count:
55+
prefer_headers.append(f"count={count}")
56+
self.session.headers["prefer"] = ",".join(prefer_headers)
3157
return FilterRequestBuilder(self.session, self.path, "DELETE", {})
3258

3359

@@ -38,9 +64,21 @@ def __init__(self, session: AsyncClient, path: str, http_method: str, json: dict
3864
self.http_method = http_method
3965
self.json = json
4066

41-
async def execute(self):
67+
async def execute(self) -> Tuple[Any, Optional[int]]:
4268
r = await self.session.request(self.http_method, self.path, json=self.json)
43-
return r.json()
69+
70+
count = None
71+
try:
72+
count_header_match = re.search(
73+
"count=(exact|planned|estimated)", self.session.headers["prefer"]
74+
)
75+
content_range = r.headers["content-range"].split("/")
76+
if count_header_match and len(content_range) >= 2:
77+
count = int(content_range[1])
78+
except KeyError:
79+
...
80+
81+
return r.json(), count
4482

4583

4684
class FilterRequestBuilder(QueryRequestBuilder):
@@ -140,7 +178,7 @@ def adj(self, column: str, range: Tuple[int, int]):
140178
def match(self, query: Dict[str, Any]):
141179
updated_query = None
142180
for key in query.keys():
143-
value = query.get(key, '')
181+
value = query.get(key, "")
144182
updated_query = self.eq(key, value)
145183
return updated_query
146184

tests/test_request_builder.py

+70-21
Original file line numberDiff line numberDiff line change
@@ -13,43 +13,92 @@ def test_constructor(request_builder):
1313
assert request_builder.path == "/example_table"
1414

1515

16-
def test_select(request_builder):
17-
builder = request_builder.select("col1", "col2")
16+
class TestSelect:
17+
def test_select(self, request_builder: RequestBuilder):
18+
builder = request_builder.select("col1", "col2")
1819

19-
assert builder.session.params["select"] == "col1,col2"
20-
assert builder.http_method == "GET"
21-
assert builder.json == {}
20+
assert builder.session.params["select"] == "col1,col2"
21+
assert builder.session.headers.get("prefer") == None
22+
assert builder.http_method == "GET"
23+
assert builder.json == {}
24+
25+
def test_select_with_count(self, request_builder: RequestBuilder):
26+
builder = request_builder.select(count="exact")
27+
28+
assert builder.session.params.get("select") == None
29+
assert builder.session.headers["prefer"] == "count=exact"
30+
assert builder.http_method == "HEAD"
31+
assert builder.json == {}
2232

2333

2434
class TestInsert:
25-
def test_insert(self, request_builder):
35+
def test_insert(self, request_builder: RequestBuilder):
2636
builder = request_builder.insert({"key1": "val1"})
2737

28-
assert builder.session.headers["prefer"] == "return=representation"
38+
assert builder.session.headers.get_list("prefer", True) == [
39+
"return=representation"
40+
]
41+
assert builder.http_method == "POST"
42+
assert builder.json == {"key1": "val1"}
43+
44+
def test_insert_with_count(self, request_builder: RequestBuilder):
45+
builder = request_builder.insert({"key1": "val1"}, count="exact")
46+
47+
assert builder.session.headers.get_list("prefer", True) == [
48+
"return=representation",
49+
"count=exact",
50+
]
2951
assert builder.http_method == "POST"
3052
assert builder.json == {"key1": "val1"}
3153

32-
def test_upsert(self, request_builder):
54+
def test_upsert(self, request_builder: RequestBuilder):
3355
builder = request_builder.insert({"key1": "val1"}, upsert=True)
3456

35-
assert (
36-
builder.session.headers["prefer"]
37-
== "return=representation,resolution=merge-duplicates"
38-
)
57+
assert builder.session.headers.get_list("prefer", True) == [
58+
"return=representation",
59+
"resolution=merge-duplicates",
60+
]
3961
assert builder.http_method == "POST"
4062
assert builder.json == {"key1": "val1"}
4163

4264

43-
def test_update(request_builder):
44-
builder = request_builder.update({"key1": "val1"})
65+
class TestUpdate:
66+
def test_update(self, request_builder: RequestBuilder):
67+
builder = request_builder.update({"key1": "val1"})
68+
69+
assert builder.session.headers.get_list("prefer", True) == [
70+
"return=representation"
71+
]
72+
assert builder.http_method == "PATCH"
73+
assert builder.json == {"key1": "val1"}
74+
75+
def test_update_with_count(self, request_builder: RequestBuilder):
76+
builder = request_builder.update({"key1": "val1"}, count="exact")
77+
78+
assert builder.session.headers.get_list("prefer", True) == [
79+
"return=representation",
80+
"count=exact",
81+
]
82+
assert builder.http_method == "PATCH"
83+
assert builder.json == {"key1": "val1"}
84+
4585

46-
assert builder.session.headers["prefer"] == "return=representation"
47-
assert builder.http_method == "PATCH"
48-
assert builder.json == {"key1": "val1"}
86+
class TestDelete:
87+
def test_delete(self, request_builder: RequestBuilder):
88+
builder = request_builder.delete()
4989

90+
assert builder.session.headers.get_list("prefer", True) == [
91+
"return=representation"
92+
]
93+
assert builder.http_method == "DELETE"
94+
assert builder.json == {}
5095

51-
def test_delete(request_builder):
52-
builder = request_builder.delete()
96+
def test_delete_with_count(self, request_builder: RequestBuilder):
97+
builder = request_builder.delete(count="exact")
5398

54-
assert builder.http_method == "DELETE"
55-
assert builder.json == {}
99+
assert builder.session.headers.get_list("prefer", True) == [
100+
"return=representation",
101+
"count=exact",
102+
]
103+
assert builder.http_method == "DELETE"
104+
assert builder.json == {}

0 commit comments

Comments
 (0)