Skip to content

Commit

Permalink
Implementation of maybe_single (#118)
Browse files Browse the repository at this point in the history
* add initial implementation on maybe_single

Signed-off-by: Bariq <bariqhibat@gmail.com>

* add sync maybe_single and fix error implementation

Signed-off-by: Bariq <bariqhibat@gmail.com>

* use relative import

Signed-off-by: Bariq <bariqhibat@gmail.com>

* implement new design for sync method

Signed-off-by: Bariq <bariqhibat@gmail.com>

* remove error from APIResponse

Signed-off-by: Bariq <bariqhibat@gmail.com>

* shift changes to async part

Signed-off-by: Bariq <bariqhibat@gmail.com>

* change class design to factory pattern

Signed-off-by: Bariq <bariqhibat@gmail.com>

* black and isort

Signed-off-by: Bariq <bariqhibat@gmail.com>

* fix: CI errors

Signed-off-by: Bariq <bariqhibat@gmail.com>

* fix tests and add additional test

Signed-off-by: Bariq <bariqhibat@gmail.com>

* fix new test

Signed-off-by: Bariq <bariqhibat@gmail.com>

* revamp class design

Signed-off-by: Bariq <bariqhibat@gmail.com>

* fix CI test

Signed-off-by: Bariq <bariqhibat@gmail.com>

* fix CI test 2

Signed-off-by: Bariq <bariqhibat@gmail.com>

* fix unasync error and add typing

Signed-off-by: Bariq <bariqhibat@gmail.com>

* make tests for new methods

Signed-off-by: Bariq <bariqhibat@gmail.com>

* generate code and test for sync

Signed-off-by: Bariq <bariqhibat@gmail.com>

* fix docstring

Signed-off-by: Bariq <bariqhibat@gmail.com>

* fix docstring and remove unwanted changes

Signed-off-by: Bariq <bariqhibat@gmail.com>

* fix tests on CI

Signed-off-by: Bariq <bariqhibat@gmail.com>

* remove single ok tests

Signed-off-by: Bariq <bariqhibat@gmail.com>

Signed-off-by: Bariq <bariqhibat@gmail.com>
  • Loading branch information
bariqhibat committed Jan 8, 2023
1 parent 1448861 commit 5d17f81
Show file tree
Hide file tree
Showing 8 changed files with 419 additions and 17 deletions.
108 changes: 107 additions & 1 deletion postgrest/_async/request_builder.py
Expand Up @@ -10,6 +10,7 @@
BaseFilterRequestBuilder,
BaseSelectRequestBuilder,
CountMethod,
SingleAPIResponse,
pre_delete,
pre_insert,
pre_select,
Expand Down Expand Up @@ -57,13 +58,90 @@ async def execute(self) -> APIResponse:
params=self.params,
headers=self.headers,
)
try:
if (
200 <= r.status_code <= 299
): # Response.ok from JS (https://developer.mozilla.org/en-US/docs/Web/API/Response/ok)
return APIResponse.from_http_request_response(r)
else:
raise APIError(r.json())
except ValidationError as e:
raise APIError(r.json()) from e


class AsyncSingleRequestBuilder:
def __init__(
self,
session: AsyncClient,
path: str,
http_method: str,
headers: Headers,
params: QueryParams,
json: dict,
) -> None:
self.session = session
self.path = path
self.http_method = http_method
self.headers = headers
self.params = params
self.json = json

async def execute(self) -> SingleAPIResponse:
"""Execute the query.
.. tip::
This is the last method called, after the query is built.
Returns:
:class:`SingleAPIResponse`
Raises:
:class:`APIError` If the API raised an error.
"""
r = await self.session.request(
self.http_method,
self.path,
json=self.json,
params=self.params,
headers=self.headers,
)
try:
return APIResponse.from_http_request_response(r)
if (
200 <= r.status_code <= 299
): # Response.ok from JS (https://developer.mozilla.org/en-US/docs/Web/API/Response/ok)
return SingleAPIResponse.from_http_request_response(r)
else:
raise APIError(r.json())
except ValidationError as e:
raise APIError(r.json()) from e


class AsyncMaybeSingleRequestBuilder(AsyncSingleRequestBuilder):
async def execute(self) -> SingleAPIResponse:
r = None
try:
r = await super().execute()
except APIError as e:
if e.details and "Results contain 0 rows" in e.details:
return SingleAPIResponse.from_dict(
{
"data": None,
"error": None,
"count": 0, # NOTE: needs to take value from res.count
}
)
if not r:
raise APIError(
{
"message": "Missing response",
"code": "204",
"hint": "Please check traceback of the code",
"details": "Postgrest couldn't retrieve response, please check traceback of the code. Please create an issue in `supabase-community/postgrest-py` if needed.",
}
)
return r


# ignoring type checking as a workaround for https://github.com/python/mypy/issues/9319
class AsyncFilterRequestBuilder(BaseFilterRequestBuilder, AsyncQueryRequestBuilder): # type: ignore
def __init__(
Expand Down Expand Up @@ -97,6 +175,34 @@ def __init__(
self, session, path, http_method, headers, params, json
)

def single(self) -> AsyncSingleRequestBuilder:
"""Specify that the query will only return a single row in response.
.. caution::
The API will raise an error if the query returned more than one row.
"""
self.headers["Accept"] = "application/vnd.pgrst.object+json"
return AsyncSingleRequestBuilder(
headers=self.headers,
http_method=self.http_method,
json=self.json,
params=self.params,
path=self.path,
session=self.session, # type: ignore
)

def maybe_single(self) -> AsyncMaybeSingleRequestBuilder:
"""Retrieves at most one row from the result. Result must be at most one row (e.g. using `eq` on a UNIQUE column), otherwise this will result in an error."""
self.headers["Accept"] = "application/vnd.pgrst.object+json"
return AsyncMaybeSingleRequestBuilder(
headers=self.headers,
http_method=self.http_method,
json=self.json,
params=self.params,
path=self.path,
session=self.session, # type: ignore
)


class AsyncRequestBuilder:
def __init__(self, session: AsyncClient, path: str) -> None:
Expand Down
3 changes: 2 additions & 1 deletion postgrest/_sync/client.py
Expand Up @@ -68,7 +68,7 @@ def from_(self, table: str) -> SyncRequestBuilder:
return SyncRequestBuilder(self.session, f"/{table}")

def table(self, table: str) -> SyncRequestBuilder:
"""Alias to :meth:`from_`."""
"""Alias to self.from_()."""
return self.from_(table)

@deprecated("0.2.0", "1.0.0", __version__, "Use self.from_() instead")
Expand All @@ -86,6 +86,7 @@ def rpc(self, func: str, params: dict) -> SyncFilterRequestBuilder:
:class:`SyncFilterRequestBuilder`
Example:
::
await client.rpc("foobar", {"arg": "value"}).execute()
.. versionchanged:: 0.11.0
Expand Down
108 changes: 107 additions & 1 deletion postgrest/_sync/request_builder.py
Expand Up @@ -10,6 +10,7 @@
BaseFilterRequestBuilder,
BaseSelectRequestBuilder,
CountMethod,
SingleAPIResponse,
pre_delete,
pre_insert,
pre_select,
Expand Down Expand Up @@ -57,13 +58,90 @@ def execute(self) -> APIResponse:
params=self.params,
headers=self.headers,
)
try:
if (
200 <= r.status_code <= 299
): # Response.ok from JS (https://developer.mozilla.org/en-US/docs/Web/API/Response/ok)
return APIResponse.from_http_request_response(r)
else:
raise APIError(r.json())
except ValidationError as e:
raise APIError(r.json()) from e


class SyncSingleRequestBuilder:
def __init__(
self,
session: SyncClient,
path: str,
http_method: str,
headers: Headers,
params: QueryParams,
json: dict,
) -> None:
self.session = session
self.path = path
self.http_method = http_method
self.headers = headers
self.params = params
self.json = json

def execute(self) -> SingleAPIResponse:
"""Execute the query.
.. tip::
This is the last method called, after the query is built.
Returns:
:class:`SingleAPIResponse`
Raises:
:class:`APIError` If the API raised an error.
"""
r = self.session.request(
self.http_method,
self.path,
json=self.json,
params=self.params,
headers=self.headers,
)
try:
return APIResponse.from_http_request_response(r)
if (
200 <= r.status_code <= 299
): # Response.ok from JS (https://developer.mozilla.org/en-US/docs/Web/API/Response/ok)
return SingleAPIResponse.from_http_request_response(r)
else:
raise APIError(r.json())
except ValidationError as e:
raise APIError(r.json()) from e


class SyncMaybeSingleRequestBuilder(SyncSingleRequestBuilder):
def execute(self) -> SingleAPIResponse:
r = None
try:
r = super().execute()
except APIError as e:
if e.details and "Results contain 0 rows" in e.details:
return SingleAPIResponse.from_dict(
{
"data": None,
"error": None,
"count": 0, # NOTE: needs to take value from res.count
}
)
if not r:
raise APIError(
{
"message": "Missing response",
"code": "204",
"hint": "Please check traceback of the code",
"details": "Postgrest couldn't retrieve response, please check traceback of the code. Please create an issue in `supabase-community/postgrest-py` if needed.",
}
)
return r


# ignoring type checking as a workaround for https://github.com/python/mypy/issues/9319
class SyncFilterRequestBuilder(BaseFilterRequestBuilder, SyncQueryRequestBuilder): # type: ignore
def __init__(
Expand Down Expand Up @@ -97,6 +175,34 @@ def __init__(
self, session, path, http_method, headers, params, json
)

def single(self) -> SyncSingleRequestBuilder:
"""Specify that the query will only return a single row in response.
.. caution::
The API will raise an error if the query returned more than one row.
"""
self.headers["Accept"] = "application/vnd.pgrst.object+json"
return SyncSingleRequestBuilder(
headers=self.headers,
http_method=self.http_method,
json=self.json,
params=self.params,
path=self.path,
session=self.session, # type: ignore
)

def maybe_single(self) -> SyncMaybeSingleRequestBuilder:
"""Retrieves at most one row from the result. Result must be at most one row (e.g. using `eq` on a UNIQUE column), otherwise this will result in an error."""
self.headers["Accept"] = "application/vnd.pgrst.object+json"
return SyncMaybeSingleRequestBuilder(
headers=self.headers,
http_method=self.http_method,
json=self.json,
params=self.params,
path=self.path,
session=self.session, # type: ignore
)


class SyncRequestBuilder:
def __init__(self, session: SyncClient, path: str) -> None:
Expand Down
43 changes: 33 additions & 10 deletions postgrest/base_request_builder.py
Expand Up @@ -6,6 +6,7 @@
Any,
Dict,
Iterable,
List,
NamedTuple,
Optional,
Tuple,
Expand Down Expand Up @@ -105,7 +106,7 @@ def pre_delete(


class APIResponse(BaseModel):
data: Any
data: List[Dict[str, Any]]
"""The data returned by the query."""
count: Optional[int] = None
"""The number of rows returned."""
Expand Down Expand Up @@ -155,6 +156,37 @@ def from_http_request_response(
count = cls._get_count_from_http_request_response(request_response)
return cls(data=data, count=count)

@classmethod
def from_dict(cls: Type[APIResponse], dict: Dict[str, Any]) -> APIResponse:
keys = dict.keys()
assert len(keys) == 3 and "data" in keys and "count" in keys and "error" in keys
return cls(
data=dict.get("data"), count=dict.get("count"), error=dict.get("error")
)


class SingleAPIResponse(APIResponse):
data: Dict[str, Any] # type: ignore
"""The data returned by the query."""

@classmethod
def from_http_request_response(
cls: Type[SingleAPIResponse], request_response: RequestResponse
) -> SingleAPIResponse:
data = request_response.json()
count = cls._get_count_from_http_request_response(request_response)
return cls(data=data, count=count)

@classmethod
def from_dict(
cls: Type[SingleAPIResponse], dict: Dict[str, Any]
) -> SingleAPIResponse:
keys = dict.keys()
assert len(keys) == 3 and "data" in keys and "count" in keys and "error" in keys
return cls(
data=dict.get("data"), count=dict.get("count"), error=dict.get("error")
)


_FilterT = TypeVar("_FilterT", bound="BaseFilterRequestBuilder")

Expand Down Expand Up @@ -410,12 +442,3 @@ def range(self: _FilterT, start: int, end: int) -> _FilterT:
self.headers["Range-Unit"] = "items"
self.headers["Range"] = f"{start}-{end - 1}"
return self

def single(self: _FilterT) -> _FilterT:
"""Specify that the query will only return a single row in response.
.. caution::
The API will raise an error if the query returned more than one row.
"""
self.headers["Accept"] = "application/vnd.pgrst.object+json"
return self

0 comments on commit 5d17f81

Please sign in to comment.