From c99d7fbd56ec0c3294de737690da575da44068fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Mon, 3 Jan 2022 16:54:01 +0100 Subject: [PATCH 01/28] add poetry dependency --- poetry.lock | 55 ++++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 1 + 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index aa669793..73f941e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -365,6 +365,21 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pydantic" +version = "1.9.0" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + [[package]] name = "pyparsing" version = "2.4.7" @@ -566,7 +581,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "0696dca9aeb87a3e255ce2633f6efe4c561f1f1feee4b9750c807552ebc3a2fe" +content-hash = "1f178b71af0c570b4389e54382a4b07b0e45723a2b25c87aea341006b1cfabe7" [metadata.files] anyio = [ @@ -752,6 +767,43 @@ py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] +pydantic = [ + {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, + {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, + {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, + {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, + {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, + {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, + {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, + {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, + {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, + {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, + {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, +] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, @@ -861,7 +913,6 @@ typer = [ ] typing-extensions = [ {file = "typing_extensions-4.0.0-py3-none-any.whl", hash = "sha256:829704698b22e13ec9eaf959122315eabb370b0884400e9818334d8b677023d9"}, - {file = "typing_extensions-4.0.0.tar.gz", hash = "sha256:2cdf80e4e04866a9b3689a51869016d36db0814d84b8d8a568d22781d45d27ed"}, ] unasync = [ {file = "unasync-0.5.0-py3-none-any.whl", hash = "sha256:8d4536dae85e87b8751dfcc776f7656fd0baf54bb022a7889440dc1b9dc3becb"}, diff --git a/pyproject.toml b/pyproject.toml index a8fdce69..c8ad39be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ repository = "https://github.com/supabase/postgrest-py" python = "^3.7" httpx = ">=0.19,<0.22" deprecation = "^2.1.0" +pydantic = "^1.9.0" [tool.poetry.dev-dependencies] pytest = "^6.2.5" From e9112228670758efe55cda4ef76c92ba15a2b4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Mon, 3 Jan 2022 16:55:19 +0100 Subject: [PATCH 02/28] create APIResponse model --- postgrest_py/base_request_builder.py | 37 +++++++++++++++++++--------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/postgrest_py/base_request_builder.py b/postgrest_py/base_request_builder.py index 0cd8cec4..7a2298df 100644 --- a/postgrest_py/base_request_builder.py +++ b/postgrest_py/base_request_builder.py @@ -1,8 +1,7 @@ +from __future__ import annotations from re import search from typing import Any, Dict, Iterable, Optional, Tuple, Union -from httpx import Response - from postgrest_py.types import CountMethod, Filters, RequestMethod, ReturnMethod from postgrest_py.utils import ( AsyncClient, @@ -10,6 +9,8 @@ sanitize_param, sanitize_pattern_param, ) +from pydantic import BaseModel +from httpx import Response as RequestResponse def pre_select( @@ -93,21 +94,33 @@ def pre_delete( return RequestMethod.DELETE, {} -def process_response( - session: Union[AsyncClient, SyncClient], - r: Response, -) -> Tuple[Any, Optional[int]]: - count = None - prefer_header = session.headers.get("prefer") - if prefer_header: +class APIResponse(BaseModel): + data: Any + count: Optional[int] = None + + @staticmethod + def get_count_from_http_request_response_if_exists( + request_response: RequestResponse, + ) -> Optional[int]: + prefer_header: Optional[str] = request_response.request.headers.get("prefer") + if not prefer_header: + return None pattern = f"count=({'|'.join([cm.value for cm in CountMethod])})" count_header_match = search(pattern, prefer_header) - content_range_header = r.headers.get("content-range") + content_range_header: Optional[str] = request_response.headers.get( + "content-range" + ) if count_header_match and content_range_header: content_range = content_range_header.split("/") if len(content_range) >= 2: - count = int(content_range[1]) - return r.json(), count + return content_range[1] + return None + + @classmethod + def from_http_request_response(cls: APIResponse, request_response: RequestResponse): + count = cls.get_count_from_http_request_response_if_exists(request_response) + data = request_response.json() + return cls(data=data, count=count) class BaseFilterRequestBuilder: From 315a3032015e0f46732b012dd2fa497c9c815b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Mon, 3 Jan 2022 16:55:55 +0100 Subject: [PATCH 03/28] return APIResponse model in execute method --- postgrest_py/_async/request_builder.py | 4 ++-- postgrest_py/_sync/request_builder.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/postgrest_py/_async/request_builder.py b/postgrest_py/_async/request_builder.py index 5566b6f4..d8fa26f5 100644 --- a/postgrest_py/_async/request_builder.py +++ b/postgrest_py/_async/request_builder.py @@ -9,7 +9,7 @@ pre_select, pre_update, pre_upsert, - process_response, + APIResponse, ) from postgrest_py.types import ReturnMethod from postgrest_py.utils import AsyncClient @@ -34,7 +34,7 @@ async def execute(self) -> Tuple[Any, Optional[int]]: self.path, json=self.json, ) - return process_response(self.session, r) + return APIResponse.from_http_request_response(r) class AsyncFilterRequestBuilder(BaseFilterRequestBuilder, AsyncQueryRequestBuilder): diff --git a/postgrest_py/_sync/request_builder.py b/postgrest_py/_sync/request_builder.py index 3144cbd9..ae20cffd 100644 --- a/postgrest_py/_sync/request_builder.py +++ b/postgrest_py/_sync/request_builder.py @@ -9,7 +9,7 @@ pre_select, pre_update, pre_upsert, - process_response, + APIResponse, ) from postgrest_py.types import ReturnMethod from postgrest_py.utils import SyncClient @@ -34,7 +34,7 @@ def execute(self) -> Tuple[Any, Optional[int]]: self.path, json=self.json, ) - return process_response(self.session, r) + return APIResponse.from_http_request_response(r) class SyncFilterRequestBuilder(BaseFilterRequestBuilder, SyncQueryRequestBuilder): From e6310c52e51d9f8d4fd5331746c370987149abf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Mon, 3 Jan 2022 22:35:47 +0100 Subject: [PATCH 04/28] sort imports --- postgrest_py/_async/request_builder.py | 2 +- postgrest_py/_sync/request_builder.py | 2 +- postgrest_py/base_request_builder.py | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/postgrest_py/_async/request_builder.py b/postgrest_py/_async/request_builder.py index d8fa26f5..3c5de4e7 100644 --- a/postgrest_py/_async/request_builder.py +++ b/postgrest_py/_async/request_builder.py @@ -1,6 +1,7 @@ from typing import Any, Optional, Tuple from postgrest_py.base_request_builder import ( + APIResponse, BaseFilterRequestBuilder, BaseSelectRequestBuilder, CountMethod, @@ -9,7 +10,6 @@ pre_select, pre_update, pre_upsert, - APIResponse, ) from postgrest_py.types import ReturnMethod from postgrest_py.utils import AsyncClient diff --git a/postgrest_py/_sync/request_builder.py b/postgrest_py/_sync/request_builder.py index ae20cffd..e3f5cb57 100644 --- a/postgrest_py/_sync/request_builder.py +++ b/postgrest_py/_sync/request_builder.py @@ -1,6 +1,7 @@ from typing import Any, Optional, Tuple from postgrest_py.base_request_builder import ( + APIResponse, BaseFilterRequestBuilder, BaseSelectRequestBuilder, CountMethod, @@ -9,7 +10,6 @@ pre_select, pre_update, pre_upsert, - APIResponse, ) from postgrest_py.types import ReturnMethod from postgrest_py.utils import SyncClient diff --git a/postgrest_py/base_request_builder.py b/postgrest_py/base_request_builder.py index 7a2298df..3fff7482 100644 --- a/postgrest_py/base_request_builder.py +++ b/postgrest_py/base_request_builder.py @@ -1,7 +1,11 @@ from __future__ import annotations + from re import search from typing import Any, Dict, Iterable, Optional, Tuple, Union +from httpx import Response as RequestResponse +from pydantic import BaseModel + from postgrest_py.types import CountMethod, Filters, RequestMethod, ReturnMethod from postgrest_py.utils import ( AsyncClient, @@ -9,8 +13,6 @@ sanitize_param, sanitize_pattern_param, ) -from pydantic import BaseModel -from httpx import Response as RequestResponse def pre_select( From 790811f00764b3fb1734bfdb25964972ed93a2ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 15:50:20 +0100 Subject: [PATCH 05/28] mypy bug workaround (https://github.com/python/mypy/issues/9319) --- postgrest_py/_async/request_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postgrest_py/_async/request_builder.py b/postgrest_py/_async/request_builder.py index 3c5de4e7..9a16404d 100644 --- a/postgrest_py/_async/request_builder.py +++ b/postgrest_py/_async/request_builder.py @@ -37,7 +37,7 @@ async def execute(self) -> Tuple[Any, Optional[int]]: return APIResponse.from_http_request_response(r) -class AsyncFilterRequestBuilder(BaseFilterRequestBuilder, AsyncQueryRequestBuilder): +class AsyncFilterRequestBuilder(BaseFilterRequestBuilder, AsyncQueryRequestBuilder): # type: ignore def __init__( self, session: AsyncClient, @@ -49,7 +49,7 @@ def __init__( AsyncQueryRequestBuilder.__init__(self, session, path, http_method, json) -class AsyncSelectRequestBuilder(BaseSelectRequestBuilder, AsyncQueryRequestBuilder): +class AsyncSelectRequestBuilder(BaseSelectRequestBuilder, AsyncQueryRequestBuilder): # type: ignore def __init__( self, session: AsyncClient, From 54df5b2819b976e42cde254d2a4733feb25f6d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 15:54:00 +0100 Subject: [PATCH 06/28] split logic, validate error existance and better type APIResponse --- postgrest_py/base_request_builder.py | 43 +++++++++++++++++++++------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/postgrest_py/base_request_builder.py b/postgrest_py/base_request_builder.py index 3fff7482..be0411fe 100644 --- a/postgrest_py/base_request_builder.py +++ b/postgrest_py/base_request_builder.py @@ -1,10 +1,10 @@ from __future__ import annotations from re import search -from typing import Any, Dict, Iterable, Optional, Tuple, Union +from typing import Any, Dict, Iterable, Optional, Tuple, Type, Union from httpx import Response as RequestResponse -from pydantic import BaseModel +from pydantic import BaseModel, validator from postgrest_py.types import CountMethod, Filters, RequestMethod, ReturnMethod from postgrest_py.utils import ( @@ -100,27 +100,48 @@ class APIResponse(BaseModel): data: Any count: Optional[int] = None + @validator("data") + @classmethod + def raise_when_api_error(cls: Type[APIResponse], value: Any) -> Any: + if isinstance(value, dict) and value.get("message"): + raise ValueError("You are passing an API error to the data field.") + return value + + @staticmethod + def get_count_from_content_range_header( + content_range_header: str, + ) -> Optional[int]: + content_range = content_range_header.split("/") + if len(content_range) < 2: + return None + return int(content_range[1]) + @staticmethod - def get_count_from_http_request_response_if_exists( + def is_count_in_prefer_header(prefer_header: str) -> bool: + pattern = f"count=({'|'.join([cm.value for cm in CountMethod])})" + return bool(search(pattern, prefer_header)) + + @classmethod + def get_count_from_http_request_response( + cls: Type[APIResponse], request_response: RequestResponse, ) -> Optional[int]: prefer_header: Optional[str] = request_response.request.headers.get("prefer") if not prefer_header: return None - pattern = f"count=({'|'.join([cm.value for cm in CountMethod])})" - count_header_match = search(pattern, prefer_header) + is_count_in_prefer_header = cls.is_count_in_prefer_header(prefer_header) content_range_header: Optional[str] = request_response.headers.get( "content-range" ) - if count_header_match and content_range_header: - content_range = content_range_header.split("/") - if len(content_range) >= 2: - return content_range[1] + if is_count_in_prefer_header and content_range_header: + return cls.get_count_from_content_range_header(content_range_header) return None @classmethod - def from_http_request_response(cls: APIResponse, request_response: RequestResponse): - count = cls.get_count_from_http_request_response_if_exists(request_response) + def from_http_request_response( + cls: Type[APIResponse], request_response: RequestResponse + ): + count = cls.get_count_from_http_request_response(request_response) data = request_response.json() return cls(data=data, count=count) From c6a3f2576142824879eab20b314e1503a4421993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 15:54:18 +0100 Subject: [PATCH 07/28] Implement APIError --- postgrest_py/_async/request_builder.py | 6 +++++- postgrest_py/exceptions.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 postgrest_py/exceptions.py diff --git a/postgrest_py/_async/request_builder.py b/postgrest_py/_async/request_builder.py index 9a16404d..bc358442 100644 --- a/postgrest_py/_async/request_builder.py +++ b/postgrest_py/_async/request_builder.py @@ -1,4 +1,5 @@ from typing import Any, Optional, Tuple +from postgrest_py.exceptions import APIError from postgrest_py.base_request_builder import ( APIResponse, @@ -34,7 +35,10 @@ async def execute(self) -> Tuple[Any, Optional[int]]: self.path, json=self.json, ) - return APIResponse.from_http_request_response(r) + try: + return APIResponse.from_http_request_response(r) + except ValueError as e: + raise APIError(r.json()) from e class AsyncFilterRequestBuilder(BaseFilterRequestBuilder, AsyncQueryRequestBuilder): # type: ignore diff --git a/postgrest_py/exceptions.py b/postgrest_py/exceptions.py new file mode 100644 index 00000000..35fadec5 --- /dev/null +++ b/postgrest_py/exceptions.py @@ -0,0 +1,23 @@ +class APIError(Exception): + """ + Base exception for all API errors. + """ + + def __init__(self, error: dict[str, str]) -> None: + self._raw_error = error + self.message = error["message"] + self.code = error["code"] + self.hint = error["hint"] + self.details = error["details"] + super().__init__(str(self)) + + def __str__(self): + error_text = f"Error {self.code}:" if self.code else "" + message_text = f"\nMessage: {self.message}"if self.message else "" + hint_text = f"\nHint: {self.hint}" if self.hint else "" + details_text = f"\nDetails: {self.details}" if self.details else "" + complete_error_text = f"{error_text}{message_text}{hint_text}{details_text}" + return complete_error_text or "Empty error" + + def json(self): + return self._raw_error From df86617cdb6368e1264c06e2f9aaeeabe117d17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 16:12:18 +0100 Subject: [PATCH 08/28] add missing black config in pre-commit config --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d6f1c122..57d9b028 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,6 +38,7 @@ repos: rev: 21.11b1 hooks: - id: black + args: [--line-length, "90"] - repo: https://github.com/asottile/pyupgrade rev: v2.29.1 From 3708992522e613f7fdff23152fc249dc571fd823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 16:12:38 +0100 Subject: [PATCH 09/28] type APIError properties --- postgrest_py/exceptions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/postgrest_py/exceptions.py b/postgrest_py/exceptions.py index 35fadec5..05adaab2 100644 --- a/postgrest_py/exceptions.py +++ b/postgrest_py/exceptions.py @@ -3,6 +3,12 @@ class APIError(Exception): Base exception for all API errors. """ + _raw_error: dict[str, str] + message: str + code: str + hint: str + details: str + def __init__(self, error: dict[str, str]) -> None: self._raw_error = error self.message = error["message"] @@ -13,7 +19,7 @@ def __init__(self, error: dict[str, str]) -> None: def __str__(self): error_text = f"Error {self.code}:" if self.code else "" - message_text = f"\nMessage: {self.message}"if self.message else "" + message_text = f"\nMessage: {self.message}" if self.message else "" hint_text = f"\nHint: {self.hint}" if self.hint else "" details_text = f"\nDetails: {self.details}" if self.details else "" complete_error_text = f"{error_text}{message_text}{hint_text}{details_text}" From 032a221a40cf8da24b8a8a36073a21ee550e71ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 18:18:11 +0100 Subject: [PATCH 10/28] fix: rm unused code and use returning param in update --- postgrest_py/base_request_builder.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/postgrest_py/base_request_builder.py b/postgrest_py/base_request_builder.py index be0411fe..c647b580 100644 --- a/postgrest_py/base_request_builder.py +++ b/postgrest_py/base_request_builder.py @@ -17,7 +17,6 @@ def pre_select( session: Union[AsyncClient, SyncClient], - path: str, *columns: str, count: Optional[CountMethod] = None, ) -> Tuple[RequestMethod, dict]: @@ -33,7 +32,6 @@ def pre_select( def pre_insert( session: Union[AsyncClient, SyncClient], - path: str, json: dict, *, count: Optional[CountMethod], @@ -51,14 +49,13 @@ def pre_insert( def pre_upsert( session: Union[AsyncClient, SyncClient], - path: str, json: dict, *, count: Optional[CountMethod], returning: ReturnMethod, ignore_duplicates: bool, ) -> Tuple[RequestMethod, dict]: - prefer_headers = ["return=representation"] + prefer_headers = [f"return={returning}"] if count: prefer_headers.append(f"count={count}") resolution = "ignore" if ignore_duplicates else "merge" @@ -69,7 +66,6 @@ def pre_upsert( def pre_update( session: Union[AsyncClient, SyncClient], - path: str, json: dict, *, count: Optional[CountMethod], @@ -84,7 +80,6 @@ def pre_update( def pre_delete( session: Union[AsyncClient, SyncClient], - path: str, *, count: Optional[CountMethod], returning: ReturnMethod, From 425a1c6630b482fb28845fc045076768fb478ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 18:35:11 +0100 Subject: [PATCH 11/28] refactor: reorder lines --- postgrest_py/base_request_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgrest_py/base_request_builder.py b/postgrest_py/base_request_builder.py index c647b580..769f942f 100644 --- a/postgrest_py/base_request_builder.py +++ b/postgrest_py/base_request_builder.py @@ -136,8 +136,8 @@ def get_count_from_http_request_response( def from_http_request_response( cls: Type[APIResponse], request_response: RequestResponse ): - count = cls.get_count_from_http_request_response(request_response) data = request_response.json() + count = cls.get_count_from_http_request_response(request_response) return cls(data=data, count=count) From d0638b4109df41495a1a892656b218e7ad675043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 18:53:10 +0100 Subject: [PATCH 12/28] chore: rebuild sync --- postgrest_py/_sync/client.py | 63 +++++++++ postgrest_py/_sync/request_builder.py | 143 +++++++++++++++++++++ tests/_sync/__init__.py | 0 tests/_sync/test_client.py | 73 +++++++++++ tests/_sync/test_filter_request_builder.py | 42 ++++++ tests/_sync/test_query_request_builder.py | 18 +++ tests/_sync/test_request_builder.py | 116 +++++++++++++++++ 7 files changed, 455 insertions(+) create mode 100644 postgrest_py/_sync/client.py create mode 100644 tests/_sync/__init__.py create mode 100644 tests/_sync/test_client.py create mode 100644 tests/_sync/test_filter_request_builder.py create mode 100644 tests/_sync/test_query_request_builder.py create mode 100644 tests/_sync/test_request_builder.py diff --git a/postgrest_py/_sync/client.py b/postgrest_py/_sync/client.py new file mode 100644 index 00000000..270c977b --- /dev/null +++ b/postgrest_py/_sync/client.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from typing import Dict, cast + +from deprecation import deprecated +from httpx import Response + +from .. import __version__ +from ..base_client import DEFAULT_POSTGREST_CLIENT_HEADERS, BasePostgrestClient +from ..utils import SyncClient +from .request_builder import SyncRequestBuilder + + +class SyncPostgrestClient(BasePostgrestClient): + """PostgREST client.""" + + def __init__( + self, + base_url: str, + *, + schema: str = "public", + headers: Dict[str, str] = DEFAULT_POSTGREST_CLIENT_HEADERS, + ) -> None: + BasePostgrestClient.__init__(self, base_url, schema=schema, headers=headers) + self.session = cast(SyncClient, self.session) + + def create_session( + self, + base_url: str, + headers: Dict[str, str], + ) -> SyncClient: + return SyncClient(base_url=base_url, headers=headers) + + def __enter__(self) -> "SyncPostgrestClient": + return self + + def __exit__(self, exc_type, exc, tb) -> None: + self.aclose() + + def aclose(self) -> None: + self.session.aclose() + + def from_(self, table: str) -> SyncRequestBuilder: + """Perform a table operation.""" + base_url = str(self.session.base_url) + headers = dict(self.session.headers.items()) + session = self.create_session(base_url, headers) + session.auth = self.session.auth + return SyncRequestBuilder(session, f"/{table}") + + def table(self, table: str) -> SyncRequestBuilder: + """Alias to self.from_().""" + return self.from_(table) + + @deprecated("0.2.0", "1.0.0", __version__, "Use self.from_() instead") + def from_table(self, table: str) -> SyncRequestBuilder: + """Alias to self.from_().""" + return self.from_(table) + + def rpc(self, func: str, params: dict) -> Response: + """Perform a stored procedure call.""" + path = f"/rpc/{func}" + return self.session.post(path, json=params) diff --git a/postgrest_py/_sync/request_builder.py b/postgrest_py/_sync/request_builder.py index e69de29b..8a70ce0b 100644 --- a/postgrest_py/_sync/request_builder.py +++ b/postgrest_py/_sync/request_builder.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +from typing import Any, Optional, Tuple +from postgrest_py.exceptions import APIError + +from ..base_request_builder import ( + APIResponse, + BaseFilterRequestBuilder, + BaseSelectRequestBuilder, + CountMethod, + pre_delete, + pre_insert, + pre_select, + pre_update, + pre_upsert, +) +from ..types import ReturnMethod +from ..utils import SyncClient + + +class SyncQueryRequestBuilder: + def __init__( + self, + session: SyncClient, + path: str, + http_method: str, + json: dict, + ) -> None: + self.session = session + self.path = path + self.http_method = http_method + self.json = json + + def execute(self) -> Tuple[Any, Optional[int]]: + r = self.session.request( + self.http_method, + self.path, + json=self.json, + ) + try: + return APIResponse.from_http_request_response(r) + except ValueError as e: + raise APIError(r.json()) from e + + +class SyncFilterRequestBuilder(BaseFilterRequestBuilder, SyncQueryRequestBuilder): # type: ignore + def __init__( + self, + session: SyncClient, + path: str, + http_method: str, + json: dict, + ) -> None: + BaseFilterRequestBuilder.__init__(self, session) + SyncQueryRequestBuilder.__init__(self, session, path, http_method, json) + + +class SyncSelectRequestBuilder(BaseSelectRequestBuilder, SyncQueryRequestBuilder): # type: ignore + def __init__( + self, + session: SyncClient, + path: str, + http_method: str, + json: dict, + ) -> None: + BaseSelectRequestBuilder.__init__(self, session) + SyncQueryRequestBuilder.__init__(self, session, path, http_method, json) + + +class SyncRequestBuilder: + def __init__(self, session: SyncClient, path: str) -> None: + self.session = session + self.path = path + + def select( + self, + *columns: str, + count: Optional[CountMethod] = None, + ) -> SyncSelectRequestBuilder: + method, json = pre_select(self.session, self.path, *columns, count=count) + return SyncSelectRequestBuilder(self.session, self.path, method, json) + + def insert( + self, + json: dict, + *, + count: Optional[CountMethod] = None, + returning: ReturnMethod = ReturnMethod.representation, + upsert=False, + ) -> SyncQueryRequestBuilder: + method, json = pre_insert( + self.session, + json, + count=count, + returning=returning, + upsert=upsert, + ) + return SyncQueryRequestBuilder(self.session, self.path, method, json) + + def upsert( + self, + json: dict, + *, + count: Optional[CountMethod] = None, + returning: ReturnMethod = ReturnMethod.representation, + ignore_duplicates=False, + ) -> SyncQueryRequestBuilder: + method, json = pre_upsert( + self.session, + json, + count=count, + returning=returning, + ignore_duplicates=ignore_duplicates, + ) + return SyncQueryRequestBuilder(self.session, self.path, method, json) + + def update( + self, + json: dict, + *, + count: Optional[CountMethod] = None, + returning: ReturnMethod = ReturnMethod.representation, + ) -> SyncFilterRequestBuilder: + method, json = pre_update( + self.session, + json, + count=count, + returning=returning, + ) + return SyncFilterRequestBuilder(self.session, self.path, method, json) + + def delete( + self, + *, + count: Optional[CountMethod] = None, + returning: ReturnMethod = ReturnMethod.representation, + ) -> SyncFilterRequestBuilder: + method, json = pre_delete( + self.session, + count=count, + returning=returning, + ) + return SyncFilterRequestBuilder(self.session, self.path, method, json) diff --git a/tests/_sync/__init__.py b/tests/_sync/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/_sync/test_client.py b/tests/_sync/test_client.py new file mode 100644 index 00000000..7401c4f5 --- /dev/null +++ b/tests/_sync/test_client.py @@ -0,0 +1,73 @@ +import pytest +from httpx import BasicAuth, Headers + +from postgrest_py import SyncPostgrestClient + + +@pytest.fixture +def postgrest_client(): + with SyncPostgrestClient("https://example.com") as client: + yield client + + +class TestConstructor: + @pytest.mark.asyncio + def test_simple(self, postgrest_client: SyncPostgrestClient): + session = postgrest_client.session + + assert session.base_url == "https://example.com" + headers = Headers( + { + "Accept": "application/json", + "Content-Type": "application/json", + "Accept-Profile": "public", + "Content-Profile": "public", + } + ) + assert session.headers.items() >= headers.items() + + @pytest.mark.asyncio + def test_custom_headers(self): + with SyncPostgrestClient( + "https://example.com", schema="pub", headers={"Custom-Header": "value"} + ) as client: + session = client.session + + assert session.base_url == "https://example.com" + headers = Headers( + { + "Accept-Profile": "pub", + "Content-Profile": "pub", + "Custom-Header": "value", + } + ) + assert session.headers.items() >= headers.items() + + +class TestAuth: + @pytest.mark.asyncio + def test_auth_token(self, postgrest_client: SyncPostgrestClient): + postgrest_client.auth("s3cr3t") + session = postgrest_client.session + + assert session.headers["Authorization"] == "Bearer s3cr3t" + + @pytest.mark.asyncio + def test_auth_basic(self, postgrest_client: SyncPostgrestClient): + postgrest_client.auth(None, username="admin", password="s3cr3t") + session = postgrest_client.session + + assert isinstance(session.auth, BasicAuth) + assert session.auth._auth_header == BasicAuth("admin", "s3cr3t")._auth_header + + +@pytest.mark.asyncio +def test_schema(postgrest_client: SyncPostgrestClient): + postgrest_client.schema("private") + session = postgrest_client.session + subheaders = { + "accept-profile": "private", + "content-profile": "private", + } + + assert subheaders.items() < dict(session.headers).items() diff --git a/tests/_sync/test_filter_request_builder.py b/tests/_sync/test_filter_request_builder.py new file mode 100644 index 00000000..a5a4c6ca --- /dev/null +++ b/tests/_sync/test_filter_request_builder.py @@ -0,0 +1,42 @@ +import pytest + +from postgrest_py import SyncFilterRequestBuilder +from postgrest_py.utils import SyncClient + + +@pytest.fixture +def filter_request_builder(): + with SyncClient() as client: + yield SyncFilterRequestBuilder(client, "/example_table", "GET", {}) + + +def test_constructor(filter_request_builder): + builder = filter_request_builder + + assert builder.path == "/example_table" + assert builder.http_method == "GET" + assert builder.json == {} + assert not builder.negate_next + + +def test_not_(filter_request_builder): + builder = filter_request_builder.not_ + + assert builder.negate_next + + +def test_filter(filter_request_builder): + builder = filter_request_builder.filter(":col.name", "eq", "val") + + assert builder.session.params['":col.name"'] == "eq.val" + + +def test_multivalued_param(filter_request_builder): + builder = filter_request_builder.lte("x", "a").gte("x", "b") + + assert str(builder.session.params) == "x=lte.a&x=gte.b" + + +def test_match(filter_request_builder): + builder = filter_request_builder.match({"id": "1", "done": "false"}) + assert str(builder.session.params) == "id=eq.1&done=eq.false" diff --git a/tests/_sync/test_query_request_builder.py b/tests/_sync/test_query_request_builder.py new file mode 100644 index 00000000..635f11b6 --- /dev/null +++ b/tests/_sync/test_query_request_builder.py @@ -0,0 +1,18 @@ +import pytest + +from postgrest_py import SyncQueryRequestBuilder +from postgrest_py.utils import SyncClient + + +@pytest.fixture +def query_request_builder(): + with SyncClient() as client: + yield SyncQueryRequestBuilder(client, "/example_table", "GET", {}) + + +def test_constructor(query_request_builder): + builder = query_request_builder + + assert builder.path == "/example_table" + assert builder.http_method == "GET" + assert builder.json == {} diff --git a/tests/_sync/test_request_builder.py b/tests/_sync/test_request_builder.py new file mode 100644 index 00000000..4fb38500 --- /dev/null +++ b/tests/_sync/test_request_builder.py @@ -0,0 +1,116 @@ +import pytest + +from postgrest_py import SyncRequestBuilder +from postgrest_py.types import CountMethod +from postgrest_py.utils import SyncClient + + +@pytest.fixture +def request_builder(): + with SyncClient() as client: + yield SyncRequestBuilder(client, "/example_table") + + +def test_constructor(request_builder): + assert request_builder.path == "/example_table" + + +class TestSelect: + def test_select(self, request_builder: SyncRequestBuilder): + builder = request_builder.select("col1", "col2") + + assert builder.session.params["select"] == "col1,col2" + assert builder.session.headers.get("prefer") is None + assert builder.http_method == "GET" + assert builder.json == {} + + def test_select_with_count(self, request_builder: SyncRequestBuilder): + builder = request_builder.select(count=CountMethod.exact) + + assert builder.session.params.get("select") is None + assert builder.session.headers["prefer"] == "count=exact" + assert builder.http_method == "HEAD" + assert builder.json == {} + + +class TestInsert: + def test_insert(self, request_builder: SyncRequestBuilder): + builder = request_builder.insert({"key1": "val1"}) + + assert builder.session.headers.get_list("prefer", True) == [ + "return=representation" + ] + assert builder.http_method == "POST" + assert builder.json == {"key1": "val1"} + + def test_insert_with_count(self, request_builder: SyncRequestBuilder): + builder = request_builder.insert({"key1": "val1"}, count=CountMethod.exact) + + assert builder.session.headers.get_list("prefer", True) == [ + "return=representation", + "count=exact", + ] + assert builder.http_method == "POST" + assert builder.json == {"key1": "val1"} + + def test_insert_with_upsert(self, request_builder: SyncRequestBuilder): + builder = request_builder.insert({"key1": "val1"}, upsert=True) + + assert builder.session.headers.get_list("prefer", True) == [ + "return=representation", + "resolution=merge-duplicates", + ] + assert builder.http_method == "POST" + assert builder.json == {"key1": "val1"} + + def test_upsert(self, request_builder: SyncRequestBuilder): + builder = request_builder.upsert({"key1": "val1"}) + + assert builder.session.headers.get_list("prefer", True) == [ + "return=representation", + "resolution=merge-duplicates", + ] + assert builder.http_method == "POST" + assert builder.json == {"key1": "val1"} + + +class TestUpdate: + def test_update(self, request_builder: SyncRequestBuilder): + builder = request_builder.update({"key1": "val1"}) + + assert builder.session.headers.get_list("prefer", True) == [ + "return=representation" + ] + assert builder.http_method == "PATCH" + assert builder.json == {"key1": "val1"} + + def test_update_with_count(self, request_builder: SyncRequestBuilder): + builder = request_builder.update({"key1": "val1"}, count=CountMethod.exact) + + assert builder.session.headers.get_list("prefer", True) == [ + "return=representation", + "count=exact", + ] + assert builder.http_method == "PATCH" + assert builder.json == {"key1": "val1"} + + +class TestDelete: + def test_delete(self, request_builder: SyncRequestBuilder): + builder = request_builder.delete() + + assert builder.session.headers.get_list("prefer", True) == [ + "return=representation" + ] + assert builder.http_method == "DELETE" + assert builder.json == {} + + def test_delete_with_count(self, request_builder: SyncRequestBuilder): + builder = request_builder.delete(count=CountMethod.exact) + + assert builder.session.headers.get_list("prefer", True) == [ + "return=representation", + "count=exact", + ] + assert builder.http_method == "DELETE" + assert builder.json == {} From b2eb3ac6cbe8750120465c09409cdb9bc7fd642e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 18:57:00 +0100 Subject: [PATCH 13/28] chore: rebuild poetry.lock --- poetry.lock | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index 329857f4..35830066 100644 --- a/poetry.lock +++ b/poetry.lock @@ -618,6 +618,8 @@ typing-extensions = ">=3.7.4.3" [package.extras] dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] + +[[package]] name = "pyflakes" version = "2.4.0" description = "passive checker of Python programs" @@ -1170,7 +1172,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "1f178b71af0c570b4389e54382a4b07b0e45723a2b25c87aea341006b1cfabe7" +content-hash = "6722490d8109481647ea1e7fa7cc1dd9a77a8d2126b761b39899aba0da2d8490" [metadata.files] alabaster = [ @@ -1559,14 +1561,6 @@ pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] -pygments = [ - {file = "Pygments-2.11.1-py3-none-any.whl", hash = "sha256:9135c1af61eec0f650cd1ea1ed8ce298e54d56bcd8cc2ef46edd7702c171337c"}, - {file = "Pygments-2.11.1.tar.gz", hash = "sha256:59b895e326f0fb0d733fd28c6839bd18ad0687ba20efc26d4277fd1d30b971f4"}, -] pydantic = [ {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, @@ -1604,6 +1598,14 @@ pydantic = [ {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, ] +pyflakes = [ + {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, + {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, +] +pygments = [ + {file = "Pygments-2.11.1-py3-none-any.whl", hash = "sha256:9135c1af61eec0f650cd1ea1ed8ce298e54d56bcd8cc2ef46edd7702c171337c"}, + {file = "Pygments-2.11.1.tar.gz", hash = "sha256:59b895e326f0fb0d733fd28c6839bd18ad0687ba20efc26d4277fd1d30b971f4"}, +] pyparsing = [ {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, From bddf02354e84e94987352016788857e5cad8adba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 19:04:09 +0100 Subject: [PATCH 14/28] fix: remove wrong parameter --- postgrest_py/_async/request_builder.py | 3 ++- postgrest_py/_sync/request_builder.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/postgrest_py/_async/request_builder.py b/postgrest_py/_async/request_builder.py index 2a10fbce..814e74ac 100644 --- a/postgrest_py/_async/request_builder.py +++ b/postgrest_py/_async/request_builder.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Any, Optional, Tuple + from postgrest_py.exceptions import APIError from ..base_request_builder import ( @@ -77,7 +78,7 @@ def select( *columns: str, count: Optional[CountMethod] = None, ) -> AsyncSelectRequestBuilder: - method, json = pre_select(self.session, self.path, *columns, count=count) + method, json = pre_select(self.session, *columns, count=count) return AsyncSelectRequestBuilder(self.session, self.path, method, json) def insert( diff --git a/postgrest_py/_sync/request_builder.py b/postgrest_py/_sync/request_builder.py index 8a70ce0b..6258f7b9 100644 --- a/postgrest_py/_sync/request_builder.py +++ b/postgrest_py/_sync/request_builder.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Any, Optional, Tuple + from postgrest_py.exceptions import APIError from ..base_request_builder import ( @@ -77,7 +78,7 @@ def select( *columns: str, count: Optional[CountMethod] = None, ) -> SyncSelectRequestBuilder: - method, json = pre_select(self.session, self.path, *columns, count=count) + method, json = pre_select(self.session, *columns, count=count) return SyncSelectRequestBuilder(self.session, self.path, method, json) def insert( From 920b3e2cabefda8b2cedb014a6d5891d0f5855e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 19:05:15 +0100 Subject: [PATCH 15/28] chore: format --- postgrest_py/_sync/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgrest_py/_sync/client.py b/postgrest_py/_sync/client.py index 270c977b..029c21db 100644 --- a/postgrest_py/_sync/client.py +++ b/postgrest_py/_sync/client.py @@ -31,7 +31,7 @@ def create_session( ) -> SyncClient: return SyncClient(base_url=base_url, headers=headers) - def __enter__(self) -> "SyncPostgrestClient": + def __enter__(self) -> SyncPostgrestClient: return self def __exit__(self, exc_type, exc, tb) -> None: From 8a7cce5466fee739123efdd78512b26287994cd7 Mon Sep 17 00:00:00 2001 From: dreinon <67071425+dreinon@users.noreply.github.com> Date: Tue, 4 Jan 2022 19:17:12 +0100 Subject: [PATCH 16/28] Chore: add missing return types Co-authored-by: Anand <40204976+anand2312@users.noreply.github.com> --- postgrest_py/base_request_builder.py | 2 +- postgrest_py/exceptions.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/postgrest_py/base_request_builder.py b/postgrest_py/base_request_builder.py index effe7897..6ec18100 100644 --- a/postgrest_py/base_request_builder.py +++ b/postgrest_py/base_request_builder.py @@ -130,7 +130,7 @@ def get_count_from_http_request_response( @classmethod def from_http_request_response( cls: Type[APIResponse], request_response: RequestResponse - ): + ) -> APIResponse: data = request_response.json() count = cls.get_count_from_http_request_response(request_response) return cls(data=data, count=count) diff --git a/postgrest_py/exceptions.py b/postgrest_py/exceptions.py index 05adaab2..5878be00 100644 --- a/postgrest_py/exceptions.py +++ b/postgrest_py/exceptions.py @@ -17,7 +17,7 @@ def __init__(self, error: dict[str, str]) -> None: self.details = error["details"] super().__init__(str(self)) - def __str__(self): + def __repr__(self) -> str: error_text = f"Error {self.code}:" if self.code else "" message_text = f"\nMessage: {self.message}" if self.message else "" hint_text = f"\nHint: {self.hint}" if self.hint else "" @@ -25,5 +25,5 @@ def __str__(self): complete_error_text = f"{error_text}{message_text}{hint_text}{details_text}" return complete_error_text or "Empty error" - def json(self): + def json(self) -> dict[str, str]: return self._raw_error From 6dbd5cbe032756e48b63f6bdb6c354cd3c0189ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 19:20:27 +0100 Subject: [PATCH 17/28] chore: replace builtin dict by Dict to support python < 3.9 --- postgrest_py/exceptions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/postgrest_py/exceptions.py b/postgrest_py/exceptions.py index 5878be00..c342e1ee 100644 --- a/postgrest_py/exceptions.py +++ b/postgrest_py/exceptions.py @@ -1,15 +1,17 @@ +from typing import Dict + class APIError(Exception): """ Base exception for all API errors. """ - _raw_error: dict[str, str] + _raw_error: Dict[str, str] message: str code: str hint: str details: str - def __init__(self, error: dict[str, str]) -> None: + def __init__(self, error: Dict[str, str]) -> None: self._raw_error = error self.message = error["message"] self.code = error["code"] @@ -25,5 +27,5 @@ def __repr__(self) -> str: complete_error_text = f"{error_text}{message_text}{hint_text}{details_text}" return complete_error_text or "Empty error" - def json(self) -> dict[str, str]: + def json(self) -> Dict[str, str]: return self._raw_error From c569b2a3ccb13d3e2a912f7f28575875831d2533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 19:23:36 +0100 Subject: [PATCH 18/28] chore: update precommit hooks --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57d9b028..10c035e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: - id: trailing-whitespace - id: check-added-large-files @@ -35,19 +35,19 @@ repos: ] - repo: https://github.com/ambv/black - rev: 21.11b1 + rev: 21.12b0 hooks: - id: black args: [--line-length, "90"] - repo: https://github.com/asottile/pyupgrade - rev: v2.29.1 + rev: v2.31.0 hooks: - id: pyupgrade args: ["--py37-plus", "--keep-runtime-typing"] - repo: https://github.com/commitizen-tools/commitizen - rev: v2.20.0 + rev: v2.20.3 hooks: - id: commitizen stages: [commit-msg] From aea8dc4986942f0bdbf38fbbe74adc08f78b1fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Tue, 4 Jan 2022 19:25:22 +0100 Subject: [PATCH 19/28] chore: apply format --- postgrest_py/exceptions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/postgrest_py/exceptions.py b/postgrest_py/exceptions.py index c342e1ee..99a44219 100644 --- a/postgrest_py/exceptions.py +++ b/postgrest_py/exceptions.py @@ -1,5 +1,6 @@ from typing import Dict + class APIError(Exception): """ Base exception for all API errors. From afdd7253537dbf447cceea0295bc159dc0c04513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Sun, 9 Jan 2022 02:03:14 +0100 Subject: [PATCH 20/28] update return type in execute method --- postgrest_py/_async/request_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postgrest_py/_async/request_builder.py b/postgrest_py/_async/request_builder.py index 814e74ac..8d19d219 100644 --- a/postgrest_py/_async/request_builder.py +++ b/postgrest_py/_async/request_builder.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Optional, Tuple +from typing import Optional from postgrest_py.exceptions import APIError @@ -32,7 +32,7 @@ def __init__( self.http_method = http_method self.json = json - async def execute(self) -> Tuple[Any, Optional[int]]: + async def execute(self) -> APIResponse: r = await self.session.request( self.http_method, self.path, From 6c9448c25e69fa83a62f135a0036b668fc3527c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Sun, 9 Jan 2022 02:03:58 +0100 Subject: [PATCH 21/28] use relative import --- postgrest_py/_async/request_builder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/postgrest_py/_async/request_builder.py b/postgrest_py/_async/request_builder.py index 8d19d219..c5b3bd9d 100644 --- a/postgrest_py/_async/request_builder.py +++ b/postgrest_py/_async/request_builder.py @@ -2,8 +2,6 @@ from typing import Optional -from postgrest_py.exceptions import APIError - from ..base_request_builder import ( APIResponse, BaseFilterRequestBuilder, @@ -15,6 +13,7 @@ pre_update, pre_upsert, ) +from ..exceptions import APIError from ..types import ReturnMethod from ..utils import AsyncClient From 2ecbecad9ed83f0cd88e47d808ac013e13d90ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Sun, 9 Jan 2022 02:06:39 +0100 Subject: [PATCH 22/28] add link to mypy issue --- postgrest_py/_async/request_builder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/postgrest_py/_async/request_builder.py b/postgrest_py/_async/request_builder.py index c5b3bd9d..177e5dc3 100644 --- a/postgrest_py/_async/request_builder.py +++ b/postgrest_py/_async/request_builder.py @@ -43,6 +43,7 @@ async def execute(self) -> APIResponse: raise APIError(r.json()) from e +# ignoring type checking as a workaround for https://github.com/python/mypy/issues/9319 class AsyncFilterRequestBuilder(BaseFilterRequestBuilder, AsyncQueryRequestBuilder): # type: ignore def __init__( self, @@ -55,6 +56,7 @@ def __init__( AsyncQueryRequestBuilder.__init__(self, session, path, http_method, json) +# ignoring type checking as a workaround for https://github.com/python/mypy/issues/9319 class AsyncSelectRequestBuilder(BaseSelectRequestBuilder, AsyncQueryRequestBuilder): # type: ignore def __init__( self, From 3df240b2a7e80389c78c11681a7636be6185453e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Sun, 9 Jan 2022 02:09:50 +0100 Subject: [PATCH 23/28] switch super init by class init to avoid future errors --- postgrest_py/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgrest_py/exceptions.py b/postgrest_py/exceptions.py index 99a44219..db774f8f 100644 --- a/postgrest_py/exceptions.py +++ b/postgrest_py/exceptions.py @@ -18,7 +18,7 @@ def __init__(self, error: Dict[str, str]) -> None: self.code = error["code"] self.hint = error["hint"] self.details = error["details"] - super().__init__(str(self)) + Exception.__init__(self, str(self)) def __repr__(self) -> str: error_text = f"Error {self.code}:" if self.code else "" From 2a806b20db45df859d4dbf4273835cc72a9b5116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Thu, 13 Jan 2022 20:50:41 +0100 Subject: [PATCH 24/28] chore: apply future annotations notation to return --- postgrest_py/_async/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postgrest_py/_async/client.py b/postgrest_py/_async/client.py index 664ee8d6..467cd477 100644 --- a/postgrest_py/_async/client.py +++ b/postgrest_py/_async/client.py @@ -31,7 +31,7 @@ def create_session( ) -> AsyncClient: return AsyncClient(base_url=base_url, headers=headers) - async def __aenter__(self) -> "AsyncPostgrestClient": + async def __aenter__(self) -> AsyncPostgrestClient: return self async def __aexit__(self, exc_type, exc, tb) -> None: From 54ade70afca1bd92a824b8e88444bd5debce6ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Thu, 13 Jan 2022 20:50:54 +0100 Subject: [PATCH 25/28] chore: rebuild sync --- postgrest_py/_sync/request_builder.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/postgrest_py/_sync/request_builder.py b/postgrest_py/_sync/request_builder.py index 6258f7b9..10553668 100644 --- a/postgrest_py/_sync/request_builder.py +++ b/postgrest_py/_sync/request_builder.py @@ -1,8 +1,6 @@ from __future__ import annotations -from typing import Any, Optional, Tuple - -from postgrest_py.exceptions import APIError +from typing import Optional from ..base_request_builder import ( APIResponse, @@ -15,6 +13,7 @@ pre_update, pre_upsert, ) +from ..exceptions import APIError from ..types import ReturnMethod from ..utils import SyncClient @@ -32,7 +31,7 @@ def __init__( self.http_method = http_method self.json = json - def execute(self) -> Tuple[Any, Optional[int]]: + def execute(self) -> APIResponse: r = self.session.request( self.http_method, self.path, @@ -44,6 +43,7 @@ def execute(self) -> Tuple[Any, Optional[int]]: raise APIError(r.json()) from e +# ignoring type checking as a workaround for https://github.com/python/mypy/issues/9319 class SyncFilterRequestBuilder(BaseFilterRequestBuilder, SyncQueryRequestBuilder): # type: ignore def __init__( self, @@ -56,6 +56,7 @@ def __init__( SyncQueryRequestBuilder.__init__(self, session, path, http_method, json) +# ignoring type checking as a workaround for https://github.com/python/mypy/issues/9319 class SyncSelectRequestBuilder(BaseSelectRequestBuilder, SyncQueryRequestBuilder): # type: ignore def __init__( self, From 2df9d14743f334285b5452a5dbce56c5405c900c Mon Sep 17 00:00:00 2001 From: Lee Yi Jie Joel Date: Fri, 28 Jan 2022 07:00:33 +0800 Subject: [PATCH 26/28] tests: Add tests for response model (#74) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial commit * tests: add fixtures for APIResponse * tests: [WIP] Test methods that don't interact with RequestResponse * tests: replace builtin type by typing type and add type annotations * tests: add requests Response fixtures * chore: change return order to improve readability * tests: add tests for left methods Co-authored-by: Joel Lee Co-authored-by: Dani Reinón --- postgrest_py/base_request_builder.py | 6 +- tests/_async/test_request_builder.py | 192 +++++++++++++++++++++++++++ tests/_sync/test_request_builder.py | 192 +++++++++++++++++++++++++++ 3 files changed, 387 insertions(+), 3 deletions(-) diff --git a/postgrest_py/base_request_builder.py b/postgrest_py/base_request_builder.py index 6ec18100..18cff5cd 100644 --- a/postgrest_py/base_request_builder.py +++ b/postgrest_py/base_request_builder.py @@ -123,9 +123,9 @@ def get_count_from_http_request_response( content_range_header: Optional[str] = request_response.headers.get( "content-range" ) - if is_count_in_prefer_header and content_range_header: - return cls.get_count_from_content_range_header(content_range_header) - return None + if not (is_count_in_prefer_header and content_range_header): + return None + return cls.get_count_from_content_range_header(content_range_header) @classmethod def from_http_request_response( diff --git a/tests/_async/test_request_builder.py b/tests/_async/test_request_builder.py index 85339b01..09ff9bcf 100644 --- a/tests/_async/test_request_builder.py +++ b/tests/_async/test_request_builder.py @@ -1,6 +1,10 @@ +from typing import Any, Dict, List + import pytest +from httpx import Request, Response from postgrest_py import AsyncRequestBuilder +from postgrest_py.base_request_builder import APIResponse from postgrest_py.types import CountMethod from postgrest_py.utils import AsyncClient @@ -114,3 +118,191 @@ def test_delete_with_count(self, request_builder: AsyncRequestBuilder): ] assert builder.http_method == "DELETE" assert builder.json == {} + + +@pytest.fixture +def api_response_with_error() -> Dict[str, Any]: + return { + "message": "Route GET:/countries?select=%2A not found", + "error": "Not Found", + "statusCode": 404, + } + + +@pytest.fixture +def api_response() -> List[Dict[str, Any]]: + return [ + { + "id": 1, + "name": "Bonaire, Sint Eustatius and Saba", + "iso2": "BQ", + "iso3": "BES", + "local_name": None, + "continent": None, + }, + { + "id": 2, + "name": "Curaçao", + "iso2": "CW", + "iso3": "CUW", + "local_name": None, + "continent": None, + }, + ] + + +@pytest.fixture +def content_range_header_with_count() -> str: + return "0-1/2" + + +@pytest.fixture +def content_range_header_without_count() -> str: + return "0-1" + + +@pytest.fixture +def prefer_header_with_count() -> str: + return "count=exact" + + +@pytest.fixture +def prefer_header_without_count() -> str: + return "random prefer header" + + +@pytest.fixture +def request_response_without_prefer_header() -> Response: + return Response( + status_code=200, request=Request(method="GET", url="http://example.com") + ) + + +@pytest.fixture +def request_response_with_prefer_header_without_count( + prefer_header_without_count: str, +) -> Response: + return Response( + status_code=200, + request=Request( + method="GET", + url="http://example.com", + headers={"prefer": prefer_header_without_count}, + ), + ) + + +@pytest.fixture +def request_response_with_prefer_header_with_count_and_content_range( + prefer_header_with_count: str, content_range_header_with_count: str +) -> Response: + return Response( + status_code=200, + headers={"content-range": content_range_header_with_count}, + request=Request( + method="GET", + url="http://example.com", + headers={"prefer": prefer_header_with_count}, + ), + ) + + +@pytest.fixture +def request_response_with_data( + prefer_header_with_count: str, + content_range_header_with_count: str, + api_response: List[Dict[str, Any]], +) -> Response: + return Response( + status_code=200, + headers={"content-range": content_range_header_with_count}, + json=api_response, + request=Request( + method="GET", + url="http://example.com", + headers={"prefer": prefer_header_with_count}, + ), + ) + + +class TestApiResponse: + def test_response_raises_when_api_error( + self, api_response_with_error: Dict[str, Any] + ): + with pytest.raises(ValueError): + APIResponse(data=api_response_with_error) + + def test_parses_valid_response_only_data(self, api_response: List[Dict[str, Any]]): + result = APIResponse(data=api_response) + assert result.data == api_response + + def test_parses_valid_response_data_and_count( + self, api_response: List[Dict[str, Any]] + ): + count = len(api_response) + result = APIResponse(data=api_response, count=count) + assert result.data == api_response + assert result.count == count + + def test_get_count_from_content_range_header_with_count( + self, content_range_header_with_count: str + ): + assert ( + APIResponse.get_count_from_content_range_header( + content_range_header_with_count + ) + == 2 + ) + + def test_get_count_from_content_range_header_without_count( + self, content_range_header_without_count: str + ): + assert ( + APIResponse.get_count_from_content_range_header( + content_range_header_without_count + ) + is None + ) + + def test_is_count_in_prefer_header_true(self, prefer_header_with_count: str): + assert APIResponse.is_count_in_prefer_header(prefer_header_with_count) + + def test_is_count_in_prefer_header_false(self, prefer_header_without_count: str): + assert not APIResponse.is_count_in_prefer_header(prefer_header_without_count) + + def test_get_count_from_http_request_response_without_prefer_header( + self, request_response_without_prefer_header: Response + ): + assert ( + APIResponse.get_count_from_http_request_response( + request_response_without_prefer_header + ) + is None + ) + + def test_get_count_from_http_request_response_with_prefer_header_without_count( + self, request_response_with_prefer_header_without_count: Response + ): + assert ( + APIResponse.get_count_from_http_request_response( + request_response_with_prefer_header_without_count + ) + is None + ) + + def test_get_count_from_http_request_response_with_count_and_content_range( + self, request_response_with_prefer_header_with_count_and_content_range: Response + ): + assert ( + APIResponse.get_count_from_http_request_response( + request_response_with_prefer_header_with_count_and_content_range + ) + == 2 + ) + + def test_from_http_request_response_constructor( + self, request_response_with_data: Response, api_response: List[Dict[str, Any]] + ): + result = APIResponse.from_http_request_response(request_response_with_data) + assert result.data == api_response + assert result.count == 2 diff --git a/tests/_sync/test_request_builder.py b/tests/_sync/test_request_builder.py index 4fb38500..48a93ae2 100644 --- a/tests/_sync/test_request_builder.py +++ b/tests/_sync/test_request_builder.py @@ -1,6 +1,10 @@ +from typing import Any, Dict, List + import pytest +from httpx import Request, Response from postgrest_py import SyncRequestBuilder +from postgrest_py.base_request_builder import APIResponse from postgrest_py.types import CountMethod from postgrest_py.utils import SyncClient @@ -114,3 +118,191 @@ def test_delete_with_count(self, request_builder: SyncRequestBuilder): ] assert builder.http_method == "DELETE" assert builder.json == {} + + +@pytest.fixture +def api_response_with_error() -> Dict[str, Any]: + return { + "message": "Route GET:/countries?select=%2A not found", + "error": "Not Found", + "statusCode": 404, + } + + +@pytest.fixture +def api_response() -> List[Dict[str, Any]]: + return [ + { + "id": 1, + "name": "Bonaire, Sint Eustatius and Saba", + "iso2": "BQ", + "iso3": "BES", + "local_name": None, + "continent": None, + }, + { + "id": 2, + "name": "Curaçao", + "iso2": "CW", + "iso3": "CUW", + "local_name": None, + "continent": None, + }, + ] + + +@pytest.fixture +def content_range_header_with_count() -> str: + return "0-1/2" + + +@pytest.fixture +def content_range_header_without_count() -> str: + return "0-1" + + +@pytest.fixture +def prefer_header_with_count() -> str: + return "count=exact" + + +@pytest.fixture +def prefer_header_without_count() -> str: + return "random prefer header" + + +@pytest.fixture +def request_response_without_prefer_header() -> Response: + return Response( + status_code=200, request=Request(method="GET", url="http://example.com") + ) + + +@pytest.fixture +def request_response_with_prefer_header_without_count( + prefer_header_without_count: str, +) -> Response: + return Response( + status_code=200, + request=Request( + method="GET", + url="http://example.com", + headers={"prefer": prefer_header_without_count}, + ), + ) + + +@pytest.fixture +def request_response_with_prefer_header_with_count_and_content_range( + prefer_header_with_count: str, content_range_header_with_count: str +) -> Response: + return Response( + status_code=200, + headers={"content-range": content_range_header_with_count}, + request=Request( + method="GET", + url="http://example.com", + headers={"prefer": prefer_header_with_count}, + ), + ) + + +@pytest.fixture +def request_response_with_data( + prefer_header_with_count: str, + content_range_header_with_count: str, + api_response: List[Dict[str, Any]], +) -> Response: + return Response( + status_code=200, + headers={"content-range": content_range_header_with_count}, + json=api_response, + request=Request( + method="GET", + url="http://example.com", + headers={"prefer": prefer_header_with_count}, + ), + ) + + +class TestApiResponse: + def test_response_raises_when_api_error( + self, api_response_with_error: Dict[str, Any] + ): + with pytest.raises(ValueError): + APIResponse(data=api_response_with_error) + + def test_parses_valid_response_only_data(self, api_response: List[Dict[str, Any]]): + result = APIResponse(data=api_response) + assert result.data == api_response + + def test_parses_valid_response_data_and_count( + self, api_response: List[Dict[str, Any]] + ): + count = len(api_response) + result = APIResponse(data=api_response, count=count) + assert result.data == api_response + assert result.count == count + + def test_get_count_from_content_range_header_with_count( + self, content_range_header_with_count: str + ): + assert ( + APIResponse.get_count_from_content_range_header( + content_range_header_with_count + ) + == 2 + ) + + def test_get_count_from_content_range_header_without_count( + self, content_range_header_without_count: str + ): + assert ( + APIResponse.get_count_from_content_range_header( + content_range_header_without_count + ) + is None + ) + + def test_is_count_in_prefer_header_true(self, prefer_header_with_count: str): + assert APIResponse.is_count_in_prefer_header(prefer_header_with_count) + + def test_is_count_in_prefer_header_false(self, prefer_header_without_count: str): + assert not APIResponse.is_count_in_prefer_header(prefer_header_without_count) + + def test_get_count_from_http_request_response_without_prefer_header( + self, request_response_without_prefer_header: Response + ): + assert ( + APIResponse.get_count_from_http_request_response( + request_response_without_prefer_header + ) + is None + ) + + def test_get_count_from_http_request_response_with_prefer_header_without_count( + self, request_response_with_prefer_header_without_count: Response + ): + assert ( + APIResponse.get_count_from_http_request_response( + request_response_with_prefer_header_without_count + ) + is None + ) + + def test_get_count_from_http_request_response_with_count_and_content_range( + self, request_response_with_prefer_header_with_count_and_content_range: Response + ): + assert ( + APIResponse.get_count_from_http_request_response( + request_response_with_prefer_header_with_count_and_content_range + ) + == 2 + ) + + def test_from_http_request_response_constructor( + self, request_response_with_data: Response, api_response: List[Dict[str, Any]] + ): + result = APIResponse.from_http_request_response(request_response_with_data) + assert result.data == api_response + assert result.count == 2 From cfe8e38fa64f8328d604f5c6bcbe218fa938afd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Fri, 28 Jan 2022 00:33:00 +0100 Subject: [PATCH 27/28] chore: modify ValueError with ValidationError --- postgrest_py/_async/request_builder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/postgrest_py/_async/request_builder.py b/postgrest_py/_async/request_builder.py index 177e5dc3..9470260e 100644 --- a/postgrest_py/_async/request_builder.py +++ b/postgrest_py/_async/request_builder.py @@ -2,6 +2,8 @@ from typing import Optional +from pydantic import ValidationError + from ..base_request_builder import ( APIResponse, BaseFilterRequestBuilder, @@ -39,7 +41,7 @@ async def execute(self) -> APIResponse: ) try: return APIResponse.from_http_request_response(r) - except ValueError as e: + except ValidationError as e: raise APIError(r.json()) from e From 11399b915e4fed12878e1892684195bd67c14b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20Rein=C3=B3n?= Date: Fri, 28 Jan 2022 15:19:27 +0100 Subject: [PATCH 28/28] chore: add "_" to internal methods --- postgrest_py/base_request_builder.py | 12 ++++++------ tests/_async/test_request_builder.py | 14 +++++++------- tests/_sync/test_request_builder.py | 14 +++++++------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/postgrest_py/base_request_builder.py b/postgrest_py/base_request_builder.py index f8a5d07b..5853c0cc 100644 --- a/postgrest_py/base_request_builder.py +++ b/postgrest_py/base_request_builder.py @@ -98,7 +98,7 @@ def raise_when_api_error(cls: Type[APIResponse], value: Any) -> Any: return value @staticmethod - def get_count_from_content_range_header( + def _get_count_from_content_range_header( content_range_header: str, ) -> Optional[int]: content_range = content_range_header.split("/") @@ -107,32 +107,32 @@ def get_count_from_content_range_header( return int(content_range[1]) @staticmethod - def is_count_in_prefer_header(prefer_header: str) -> bool: + def _is_count_in_prefer_header(prefer_header: str) -> bool: pattern = f"count=({'|'.join([cm.value for cm in CountMethod])})" return bool(search(pattern, prefer_header)) @classmethod - def get_count_from_http_request_response( + def _get_count_from_http_request_response( cls: Type[APIResponse], request_response: RequestResponse, ) -> Optional[int]: prefer_header: Optional[str] = request_response.request.headers.get("prefer") if not prefer_header: return None - is_count_in_prefer_header = cls.is_count_in_prefer_header(prefer_header) + is_count_in_prefer_header = cls._is_count_in_prefer_header(prefer_header) content_range_header: Optional[str] = request_response.headers.get( "content-range" ) if not (is_count_in_prefer_header and content_range_header): return None - return cls.get_count_from_content_range_header(content_range_header) + return cls._get_count_from_content_range_header(content_range_header) @classmethod def from_http_request_response( cls: Type[APIResponse], request_response: RequestResponse ) -> APIResponse: data = request_response.json() - count = cls.get_count_from_http_request_response(request_response) + count = cls._get_count_from_http_request_response(request_response) return cls(data=data, count=count) diff --git a/tests/_async/test_request_builder.py b/tests/_async/test_request_builder.py index 09ff9bcf..78c4f8a4 100644 --- a/tests/_async/test_request_builder.py +++ b/tests/_async/test_request_builder.py @@ -248,7 +248,7 @@ def test_get_count_from_content_range_header_with_count( self, content_range_header_with_count: str ): assert ( - APIResponse.get_count_from_content_range_header( + APIResponse._get_count_from_content_range_header( content_range_header_with_count ) == 2 @@ -258,23 +258,23 @@ def test_get_count_from_content_range_header_without_count( self, content_range_header_without_count: str ): assert ( - APIResponse.get_count_from_content_range_header( + APIResponse._get_count_from_content_range_header( content_range_header_without_count ) is None ) def test_is_count_in_prefer_header_true(self, prefer_header_with_count: str): - assert APIResponse.is_count_in_prefer_header(prefer_header_with_count) + assert APIResponse._is_count_in_prefer_header(prefer_header_with_count) def test_is_count_in_prefer_header_false(self, prefer_header_without_count: str): - assert not APIResponse.is_count_in_prefer_header(prefer_header_without_count) + assert not APIResponse._is_count_in_prefer_header(prefer_header_without_count) def test_get_count_from_http_request_response_without_prefer_header( self, request_response_without_prefer_header: Response ): assert ( - APIResponse.get_count_from_http_request_response( + APIResponse._get_count_from_http_request_response( request_response_without_prefer_header ) is None @@ -284,7 +284,7 @@ def test_get_count_from_http_request_response_with_prefer_header_without_count( self, request_response_with_prefer_header_without_count: Response ): assert ( - APIResponse.get_count_from_http_request_response( + APIResponse._get_count_from_http_request_response( request_response_with_prefer_header_without_count ) is None @@ -294,7 +294,7 @@ def test_get_count_from_http_request_response_with_count_and_content_range( self, request_response_with_prefer_header_with_count_and_content_range: Response ): assert ( - APIResponse.get_count_from_http_request_response( + APIResponse._get_count_from_http_request_response( request_response_with_prefer_header_with_count_and_content_range ) == 2 diff --git a/tests/_sync/test_request_builder.py b/tests/_sync/test_request_builder.py index 48a93ae2..c57fed49 100644 --- a/tests/_sync/test_request_builder.py +++ b/tests/_sync/test_request_builder.py @@ -248,7 +248,7 @@ def test_get_count_from_content_range_header_with_count( self, content_range_header_with_count: str ): assert ( - APIResponse.get_count_from_content_range_header( + APIResponse._get_count_from_content_range_header( content_range_header_with_count ) == 2 @@ -258,23 +258,23 @@ def test_get_count_from_content_range_header_without_count( self, content_range_header_without_count: str ): assert ( - APIResponse.get_count_from_content_range_header( + APIResponse._get_count_from_content_range_header( content_range_header_without_count ) is None ) def test_is_count_in_prefer_header_true(self, prefer_header_with_count: str): - assert APIResponse.is_count_in_prefer_header(prefer_header_with_count) + assert APIResponse._is_count_in_prefer_header(prefer_header_with_count) def test_is_count_in_prefer_header_false(self, prefer_header_without_count: str): - assert not APIResponse.is_count_in_prefer_header(prefer_header_without_count) + assert not APIResponse._is_count_in_prefer_header(prefer_header_without_count) def test_get_count_from_http_request_response_without_prefer_header( self, request_response_without_prefer_header: Response ): assert ( - APIResponse.get_count_from_http_request_response( + APIResponse._get_count_from_http_request_response( request_response_without_prefer_header ) is None @@ -284,7 +284,7 @@ def test_get_count_from_http_request_response_with_prefer_header_without_count( self, request_response_with_prefer_header_without_count: Response ): assert ( - APIResponse.get_count_from_http_request_response( + APIResponse._get_count_from_http_request_response( request_response_with_prefer_header_without_count ) is None @@ -294,7 +294,7 @@ def test_get_count_from_http_request_response_with_count_and_content_range( self, request_response_with_prefer_header_with_count_and_content_range: Response ): assert ( - APIResponse.get_count_from_http_request_response( + APIResponse._get_count_from_http_request_response( request_response_with_prefer_header_with_count_and_content_range ) == 2