Skip to content

Commit

Permalink
🐛 Enhance ddd exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
perdy committed Dec 13, 2024
1 parent 651b8a2 commit 8bad54e
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 74 deletions.
52 changes: 43 additions & 9 deletions flama/ddd/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,55 @@
__all__ = ["RepositoryException", "IntegrityError", "NotFoundError", "MultipleRecordsError"]
import typing as t


class RepositoryException(Exception):
...
__all__ = [
"Empty",
"RepositoryException",
"IntegrityError",
"NotFoundError",
"AlreadyExistsError",
"MultipleRecordsError",
]


class Empty(Exception):
...


class IntegrityError(RepositoryException):
class RepositoryException(Exception):
...


class NotFoundError(RepositoryException):
...
class ResourceException(RepositoryException):
_error_message: t.ClassVar[str] = "exception"

def __init__(self, *, resource: str, id: t.Any = None, detail: str = "") -> None:
super().__init__()
self.resource = resource
self.id = id
self.detail = detail

class MultipleRecordsError(RepositoryException):
...
def __str__(self) -> str:
return (
f"Resource '{self.resource}'"
+ (f" ({self.id})" if self.id else "")
+ f" {self._error_message}"
+ (f" ({self.detail})" if self.detail else "")
)

def __repr__(self) -> str:
return f'{self.__class__.__name__}("{self.__str__()}")'


class IntegrityError(ResourceException):
_error_message = "integrity failed"


class NotFoundError(ResourceException):
_error_message = "not found"


class AlreadyExistsError(ResourceException):
_error_message = "already exists"


class MultipleRecordsError(ResourceException):
_error_message = "multiple records found"
18 changes: 9 additions & 9 deletions flama/ddd/repositories/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async def create(self, data: dict[str, t.Any]) -> dict[str, t.Any]:
response.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == http.HTTPStatus.BAD_REQUEST:
raise exceptions.IntegrityError()
raise exceptions.IntegrityError(resource=self.resource)
raise

return response.json()
Expand All @@ -62,7 +62,7 @@ async def retrieve(self, id: t.Union[str, uuid.UUID]) -> dict[str, t.Any]:
response.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == http.HTTPStatus.NOT_FOUND:
raise exceptions.NotFoundError()
raise exceptions.NotFoundError(resource=self.resource, id=id)
raise

return response.json()
Expand All @@ -81,9 +81,9 @@ async def update(self, id: t.Union[str, uuid.UUID], data: dict[str, t.Any]) -> d
response.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == http.HTTPStatus.NOT_FOUND:
raise exceptions.NotFoundError()
raise exceptions.NotFoundError(resource=self.resource, id=id)
if e.response.status_code == http.HTTPStatus.BAD_REQUEST:
raise exceptions.IntegrityError()
raise exceptions.IntegrityError(resource=self.resource)
raise
return response.json()

Expand All @@ -101,9 +101,9 @@ async def partial_update(self, id: t.Union[str, uuid.UUID], data: dict[str, t.An
response.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == http.HTTPStatus.NOT_FOUND:
raise exceptions.NotFoundError()
raise exceptions.NotFoundError(resource=self.resource, id=id)
if e.response.status_code == http.HTTPStatus.BAD_REQUEST:
raise exceptions.IntegrityError()
raise exceptions.IntegrityError(resource=self.resource)
raise
return response.json()

Expand All @@ -118,7 +118,7 @@ async def delete(self, id: t.Union[str, uuid.UUID]) -> None:
response.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == http.HTTPStatus.NOT_FOUND:
raise exceptions.NotFoundError()
raise exceptions.NotFoundError(resource=self.resource, id=id)
raise

async def _fetch_page_elements(self, **params: t.Any) -> t.AsyncIterator[dict[str, t.Any]]:
Expand Down Expand Up @@ -188,7 +188,7 @@ async def replace(self, data: builtins.list[dict[str, t.Any]]) -> builtins.list[
response.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == http.HTTPStatus.BAD_REQUEST:
raise exceptions.IntegrityError()
raise exceptions.IntegrityError(resource=self.resource)
raise

return [element for element in response.json()]
Expand All @@ -204,7 +204,7 @@ async def partial_replace(self, data: builtins.list[dict[str, t.Any]]) -> builti
response.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == http.HTTPStatus.BAD_REQUEST:
raise exceptions.IntegrityError()
raise exceptions.IntegrityError(resource=self.resource)
raise

return [element for element in response.json()]
Expand Down
11 changes: 6 additions & 5 deletions flama/ddd/repositories/sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class SQLAlchemyTableManager:
def __init__(self, table: sqlalchemy.Table, connection: AsyncConnection): # type: ignore
self._connection = connection
self.table = table
self.resource = table.name

def __eq__(self, other):
return (
Expand All @@ -52,8 +53,8 @@ async def create(self, *data: dict[str, t.Any]) -> list[dict[str, t.Any]]:
"""
try:
result = await self._connection.execute(sqlalchemy.insert(self.table).values(data).returning(self.table))
except sqlalchemy_exceptions.IntegrityError as e:
raise ddd_exceptions.IntegrityError(str(e))
except sqlalchemy_exceptions.IntegrityError:
raise ddd_exceptions.IntegrityError(resource=self.resource)
return [dict[str, t.Any](element._asdict()) for element in result]

async def retrieve(self, *clauses, **filters) -> dict[str, t.Any]:
Expand All @@ -77,9 +78,9 @@ async def retrieve(self, *clauses, **filters) -> dict[str, t.Any]:
try:
element = (await self._connection.execute(query)).one()
except sqlalchemy_exceptions.NoResultFound:
raise ddd_exceptions.NotFoundError()
raise ddd_exceptions.NotFoundError(resource=self.resource)
except sqlalchemy_exceptions.MultipleResultsFound:
raise ddd_exceptions.MultipleRecordsError()
raise ddd_exceptions.MultipleRecordsError(resource=self.resource)

return dict[str, t.Any](element._asdict())

Expand All @@ -100,7 +101,7 @@ async def update(self, data: dict[str, t.Any], *clauses, **filters) -> list[dict
try:
result = await self._connection.execute(query)
except sqlalchemy_exceptions.IntegrityError:
raise ddd_exceptions.IntegrityError
raise ddd_exceptions.IntegrityError(resource=self.resource)

return [dict[str, t.Any](element._asdict()) for element in result]

Expand Down
34 changes: 16 additions & 18 deletions flama/resources/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,8 @@ async def create(
repository = worker.repositories[self._meta.name]
try:
result = await repository.create(resource)
except ddd_exceptions.IntegrityError:
raise exceptions.HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Already exists or cannot be created"
)
except ddd_exceptions.IntegrityError as e:
raise exceptions.HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))

return http.APIResponse( # type: ignore[return-value]
schema=rest_schemas.output.schema, content=result[0], status_code=HTTPStatus.CREATED
Expand Down Expand Up @@ -82,8 +80,8 @@ async def retrieve(
async with worker:
repository = worker.repositories[self._meta.name]
return await repository.retrieve(**{rest_model.primary_key.name: resource_id})
except ddd_exceptions.NotFoundError:
raise exceptions.HTTPException(status_code=HTTPStatus.NOT_FOUND)
except ddd_exceptions.NotFoundError as e:
raise exceptions.HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=str(e))

retrieve.__doc__ = f"""
tags:
Expand Down Expand Up @@ -126,13 +124,13 @@ async def update(
try:
repository = worker.repositories[self._meta.name]
await repository.delete(**{rest_model.primary_key.name: resource_id})
except ddd_exceptions.NotFoundError:
raise exceptions.HTTPException(status_code=HTTPStatus.NOT_FOUND)
except ddd_exceptions.NotFoundError as e:
raise exceptions.HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=str(e))

try:
result = await repository.create(resource)
except ddd_exceptions.IntegrityError:
raise exceptions.HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Wrong input data")
except ddd_exceptions.IntegrityError as e:
raise exceptions.HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))

return result[0]

Expand Down Expand Up @@ -180,8 +178,8 @@ async def partial_update(
repository = worker.repositories[self._meta.name]
try:
result = await repository.update(resource, **{rest_model.primary_key.name: resource_id})
except ddd_exceptions.IntegrityError:
raise exceptions.HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Wrong input data")
except ddd_exceptions.IntegrityError as e:
raise exceptions.HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))

if not result:
raise exceptions.HTTPException(status_code=HTTPStatus.NOT_FOUND)
Expand Down Expand Up @@ -220,8 +218,8 @@ async def delete(self, worker: FlamaWorker, resource_id: rest_model.primary_key.
async with worker:
repository = worker.repositories[self._meta.name]
await repository.delete(**{rest_model.primary_key.name: resource_id})
except ddd_exceptions.NotFoundError:
raise exceptions.HTTPException(status_code=HTTPStatus.NOT_FOUND)
except ddd_exceptions.NotFoundError as e:
raise exceptions.HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=str(e))

return http.APIResponse(status_code=HTTPStatus.NO_CONTENT)

Expand Down Expand Up @@ -300,8 +298,8 @@ async def replace(
await repository.drop()
try:
return await repository.create(*resources)
except ddd_exceptions.IntegrityError:
raise exceptions.HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Wrong input data")
except ddd_exceptions.IntegrityError as e:
raise exceptions.HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))

replace.__doc__ = f"""
tags:
Expand Down Expand Up @@ -347,8 +345,8 @@ async def partial_replace(
)
try:
return await repository.create(*resources)
except ddd_exceptions.IntegrityError:
raise exceptions.HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Wrong input data")
except ddd_exceptions.IntegrityError as e:
raise exceptions.HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))

partial_replace.__doc__ = f"""
tags:
Expand Down
6 changes: 3 additions & 3 deletions tests/authentication/test_jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,17 +142,17 @@ def test_encode(self, key, header, payload, result, exception):
),
pytest.param(
JWT(header={"alg": "HS256", "typ": "JWT"}, payload={"foo": "bar", "iat": time.time() * 2}),
(exceptions.JWTValidateException, r"Invalid claims \(iat\)"),
exceptions.JWTValidateException("Invalid claims (iat)"),
id="invalid_iat",
),
pytest.param(
JWT(header={"alg": "HS256", "typ": "JWT"}, payload={"foo": "bar", "exp": time.time() / 2}),
(exceptions.JWTValidateException, r"Invalid claims \(exp\)"),
exceptions.JWTValidateException("Invalid claims (exp)"),
id="invalid_exp",
),
pytest.param(
JWT(header={"alg": "HS256", "typ": "JWT"}, payload={"foo": "bar", "nbf": time.time() * 2}),
(exceptions.JWTValidateException, r"Invalid claims \(nbf\)"),
exceptions.JWTValidateException("Invalid claims (nbf)"),
id="invalid_nbf",
),
),
Expand Down
7 changes: 5 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
import tempfile
import warnings
from contextlib import ExitStack
Expand Down Expand Up @@ -49,10 +50,12 @@ def exception(request):
if request.param is None:
context = ExceptionContext(ExitStack())
elif isinstance(request.param, Exception):
context = ExceptionContext(pytest.raises(request.param.__class__, match=str(request.param)), request.param)
context = ExceptionContext(
pytest.raises(request.param.__class__, match=re.escape(str(request.param))), request.param
)
elif isinstance(request.param, (list, tuple)):
exception, message = request.param
context = ExceptionContext(pytest.raises(exception, match=message), exception)
context = ExceptionContext(pytest.raises(exception, match=re.escape(message)), exception)
else:
context = ExceptionContext(pytest.raises(request.param), request.param)
return context
Expand Down
Loading

0 comments on commit 8bad54e

Please sign in to comment.