Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Enhance ddd exceptions #166

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading