Skip to content

Commit

Permalink
feat: error subspecification (#15)
Browse files Browse the repository at this point in the history
* feat: error subspecification

* chore: version bump
  • Loading branch information
thijsmie authored Sep 15, 2024
1 parent d29f1ee commit 70fe815
Show file tree
Hide file tree
Showing 15 changed files with 206 additions and 170 deletions.
4 changes: 2 additions & 2 deletions examples/hello_world.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pocketbase import PocketBase, PocketbaseError
from pocketbase import PocketBase, PocketBaseError

CONNECTION_URL = "http://localhost:8123"
ADMIN_EMAIL = "test@example.com"
Expand Down Expand Up @@ -29,7 +29,7 @@ async def hello_world():
],
}
)
except PocketbaseError:
except PocketBaseError:
# You probably ran this example before, and the collection already exists!
# No problem, we'll continue as normal :)
pass
Expand Down
4 changes: 2 additions & 2 deletions examples/working_with_files.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pocketbase import FileUpload, PocketBase, PocketbaseError
from pocketbase import FileUpload, PocketBase, PocketBaseError

CONNECTION_URL = "http://localhost:8123"
ADMIN_EMAIL = "test@example.com"
Expand Down Expand Up @@ -32,7 +32,7 @@ async def working_with_files():
],
}
)
except PocketbaseError:
except PocketBaseError:
# Collection probably exists
pass

Expand Down
253 changes: 127 additions & 126 deletions poetry.lock

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pocketbase-async"
version = "0.10.0"
version = "0.11.0"
description = "Async pocketbase SDK for Python"
repository = "https://github.com/thijsmie/pocketbase-async"
authors = ["Thijs Miedema <opensource@tmiedema.com>"]
Expand All @@ -24,25 +24,27 @@ httpx = ">=0.25.1,<1"
httpx-sse = ">=0.4.0,<1"

[tool.poetry.group.dev.dependencies]
pytest = "^7.2.2"
pytest-cov = "^4.0.0"
pytest = "^8.3.2"
pytest-cov = "^5.0.0"
pytest-asyncio = ">=0.21.0,<1"
ruff = "0.1.*"
ruff = "0.5.*"
mypy = "^1.1.1"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.ruff]
line-length = 120
target-version = "py311"

[tool.ruff.lint]
select = ["A", "C", "E", "F", "UP", "RUF", "I", "PL", "PTH", "TID252", "SIM"]
ignore = ["E402", "PLR2004", "PLR0913"]
fixable = ["C", "E", "F", "UP", "I", "PL", "RUF", "PTH", "PLC", "TID252", "SIM"]
line-length = 120
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
target-version = "py311"

[tool.ruff.mccabe]
[tool.ruff.lint.mccabe]
max-complexity = 10

[tool.mypy]
Expand Down
4 changes: 2 additions & 2 deletions src/pocketbase/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from pocketbase.client import PocketBase
from pocketbase.models.errors import PocketbaseError
from pocketbase.models.errors import PocketBaseError
from pocketbase.utils.types import FileUpload

__all__ = ["PocketBase", "FileUpload", "PocketbaseError"]
__all__ = ["PocketBase", "FileUpload", "PocketBaseError"]
41 changes: 39 additions & 2 deletions src/pocketbase/models/errors.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
from typing import Any

from httpx import Response

class PocketbaseError(Exception):

class PocketBaseError(Exception):
def __init__(self, url: str, status: int, data: Any) -> None:
self.url = url
self.status = status
self.data = data

def __repr__(self) -> str:
return f"PocketbaseError(url={self.url},status={self.status},data={self.data})"
return f"{self.__class__.__name__}(url={self.url},status={self.status},data={self.data})"

__str__ = __repr__

@classmethod
def raise_for_status(cls, response: Response) -> None:
if response.status_code == 400:
raise PocketBaseBadRequestError(str(response.url), response.status_code, response.json())
elif response.status_code == 401:
raise PocketBaseUnauthorizedError(str(response.url), response.status_code, response.json())
elif response.status_code == 403:
raise PocketBaseForbiddenError(str(response.url), response.status_code, response.json())
elif response.status_code == 404:
raise PocketBaseNotFoundError(str(response.url), response.status_code, response.json())
elif response.status_code == 500:
raise PocketBaseServerError(str(response.url), response.status_code, response.json())
elif response.status_code >= 400:
raise PocketBaseError(str(response.url), response.status_code, response.json())


class PocketBaseNotFoundError(PocketBaseError):
pass


class PocketBaseBadRequestError(PocketBaseError):
pass


class PocketBaseUnauthorizedError(PocketBaseError):
pass


class PocketBaseForbiddenError(PocketBaseError):
pass


class PocketBaseServerError(PocketBaseError):
pass
5 changes: 2 additions & 3 deletions src/pocketbase/services/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from httpx._types import FileTypes

from pocketbase.models.errors import PocketbaseError
from pocketbase.models.errors import PocketBaseError
from pocketbase.models.options import CommonOptions, SendOptions
from pocketbase.services.base import Service

Expand Down Expand Up @@ -69,8 +69,7 @@ async def download(self, key: ZipFileName, options: CommonOptions | None = None)

response = await self._send_raw(f"/{quote(key)}", send_options)

if not 200 >= response.status_code < 300:
raise PocketbaseError(url=str(response.url), status=response.status_code, data=response.json())
PocketBaseError.raise_for_status(response)

return response.content

Expand Down
17 changes: 7 additions & 10 deletions src/pocketbase/services/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from httpx import Request, Response

from pocketbase.models.errors import PocketbaseError
from pocketbase.models.errors import PocketBaseError
from pocketbase.models.options import SendOptions
from pocketbase.utils.types import JsonType, SendableFiles, transform

Expand Down Expand Up @@ -33,19 +33,16 @@ async def _send_raw(self, path: str, options: SendOptions) -> Response:

async def _send(self, path: str, options: SendOptions) -> JsonType:
response = await self._send_raw(path, options)
data = response.json()
PocketBaseError.raise_for_status(response)

if response.status_code >= 400:
raise PocketbaseError(url=str(response.url), status=response.status_code, data=data)

return data
try:
return response.json()
except ValueError as e:
raise PocketBaseError(str(response.url), response.status_code, "PocketBase returned invalid JSON") from e

async def _send_noreturn(self, path: str, options: SendOptions) -> None:
response = await self._send_raw(path, options)

if response.status_code >= 400:
data = response.json()
raise PocketbaseError(url=str(response.url), status=response.status_code, data=data)
PocketBaseError.raise_for_status(response)

def _init_send(self, path: str, options: SendOptions) -> Request:
headers = self._pb.headers()
Expand Down
4 changes: 2 additions & 2 deletions src/pocketbase/services/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from urllib.parse import quote

from pocketbase.models.dtos import ListResult
from pocketbase.models.errors import PocketbaseError
from pocketbase.models.errors import PocketBaseNotFoundError
from pocketbase.models.options import CommonOptions, FirstOptions, FullListOptions, ListOptions, SendOptions
from pocketbase.services.base import Service
from pocketbase.utils.types import BodyDict
Expand Down Expand Up @@ -65,7 +65,7 @@ async def get_first(self, options: FirstOptions | None = None) -> _T:
list_options["params"]["skipTotal"] = 1
result = await self.get_list(1, 1, list_options)
if not result["items"]:
raise PocketbaseError(
raise PocketBaseNotFoundError(
url=self._build_url(""),
status=404,
data={"code": 404, "message": "The requested resource wasn't found.", "data": {}},
Expand Down
2 changes: 1 addition & 1 deletion src/pocketbase/services/realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async def _make_connection(self, sentinel: asyncio.Event) -> None:
except: # noqa: E722
# We never want any exception to break the realtime handler.
logging.exception("Unhandled exception in realtime event handler")
except (ReadError, asyncio.TimeoutError, RemoteProtocolError):
except (TimeoutError, ReadError, RemoteProtocolError):
logging.debug("Connection lost, reconnecting automatically")
if last_event_id:
headers["Last-Event-ID"] = last_event_id
Expand Down
2 changes: 1 addition & 1 deletion tests/prep.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import httpx

POCKETBASE_VERSION = "0.22.12"
POCKETBASE_VERSION = "0.22.18"
POCKETBASE_PLATFORM = "linux_amd64"


Expand Down
6 changes: 3 additions & 3 deletions tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest
from pocketbase import PocketBase
from pocketbase.models.dtos import AdminModel
from pocketbase.models.errors import PocketbaseError
from pocketbase.models.errors import PocketBaseError


async def test_login(admin_client: PocketBase, admin: tuple[str, str]):
Expand Down Expand Up @@ -54,14 +54,14 @@ async def test_delete_admin(admin_client: PocketBase):
admin, _, _ = await create_admin(admin_client)
await admin_client.admins.delete(admin["id"])

with pytest.raises(PocketbaseError) as e:
with pytest.raises(PocketBaseError) as e:
await admin_client.admins.get_one(admin["id"])

assert e.value.status == 404


async def test_invalid_login_exception(client: PocketBase):
with pytest.raises(PocketbaseError) as exc:
with pytest.raises(PocketBaseError) as exc:
await client.admins.auth.with_password(uuid4().hex, uuid4().hex)
assert exc.value.status == 400 # invalid login

Expand Down
8 changes: 4 additions & 4 deletions tests/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest
from pocketbase import PocketBase
from pocketbase.models.dtos import Collection
from pocketbase.models.errors import PocketbaseError
from pocketbase.models.errors import PocketBaseError


async def create_collection(client: PocketBase) -> tuple[Collection, str]:
Expand Down Expand Up @@ -61,19 +61,19 @@ async def test_update(admin_client: PocketBase):
async def test_delete(admin_client: PocketBase):
collection, _ = await create_collection(admin_client)
await admin_client.collections.delete(collection["id"])
with pytest.raises(PocketbaseError) as exc:
with pytest.raises(PocketBaseError) as exc:
await admin_client.collections.delete(collection["id"])
assert exc.value.status == 404 # double already deleted


async def test_delete_nonexisting_exception(admin_client: PocketBase):
with pytest.raises(PocketbaseError) as exc:
with pytest.raises(PocketBaseError) as exc:
await admin_client.collections.delete(uuid4().hex)
assert exc.value.status == 404 # delete nonexisting


async def test_get_nonexisting_exception(admin_client: PocketBase):
with pytest.raises(PocketbaseError) as exc:
with pytest.raises(PocketBaseError) as exc:
await admin_client.collections.get_one(uuid4().hex)
assert exc.value.status == 404

Expand Down
6 changes: 3 additions & 3 deletions tests/test_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from uuid import uuid4

import pytest
from pocketbase import PocketBase, PocketbaseError
from pocketbase import PocketBase, PocketBaseError
from pocketbase.models.dtos import CollectionModel


Expand Down Expand Up @@ -191,14 +191,14 @@ async def test_delete(admin_client: PocketBase, collection: CollectionModel):
record = await col.create({"title": "a"})
await col.delete(record["id"])
# deleting already deleted record should give 404
with pytest.raises(PocketbaseError) as exc:
with pytest.raises(PocketBaseError) as exc:
await col.delete(record["id"])
assert exc.value.status == 404


async def test_get_one(admin_client: PocketBase, collection: CollectionModel):
col = admin_client.collection(collection["id"])
with pytest.raises(PocketbaseError) as exc:
with pytest.raises(PocketBaseError) as exc:
await col.get_one("blblblbllb")
assert exc.value.status == 404

Expand Down
4 changes: 2 additions & 2 deletions tests/test_record_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest
from pocketbase import PocketBase
from pocketbase.models.dtos import Record
from pocketbase.models.errors import PocketbaseError
from pocketbase.models.errors import PocketBaseError


@pytest.fixture
Expand Down Expand Up @@ -43,7 +43,7 @@ async def test_delete_user(admin_client: PocketBase, client: PocketBase, user: t


async def test_invalid_login_exception(client: PocketBase):
with pytest.raises(PocketbaseError) as exc:
with pytest.raises(PocketBaseError) as exc:
await client.collection("users").auth.with_password(uuid4().hex, uuid4().hex)
assert exc.value.status == 400

Expand Down

0 comments on commit 70fe815

Please sign in to comment.