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

add post event hooks #231

Closed
wants to merge 3 commits into from
Closed
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
89 changes: 85 additions & 4 deletions docs/source/crud/hooks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Define a method, and register it with :class:`PiccoloCRUD <piccolo_api.crud.endp

# app.py
from piccolo_api.crud.endpoints import PiccoloCRUD
from piccolo_api.crud.hooks import Hook, HookType

from movies.tables import Movie

Expand All @@ -27,7 +28,7 @@ Define a method, and register it with :class:`PiccoloCRUD <piccolo_api.crud.endp


# set movie rating to 20 before saving
async def set_movie_rating_10(row: Movie):
async def set_movie_rating_20(row: Movie):
row.rating = 20
return row

Expand Down Expand Up @@ -73,11 +74,38 @@ It takes a single parameter, ``row``, and should return the row:
return row


app = PiccoloCRUD(table=Movie, read_only=False, hooks=[
Hook(hook_type=HookType.pre_save, callable=set_movie_rating_10)
app = PiccoloCRUD(
table=Movie,
read_only=False,
hooks=[
Hook(hook_type=HookType.pre_save, callable=set_movie_rating_10)
]
)


post_save
~~~~~~~~~

This hook runs during POST requests, after inserting data into the database.
It takes a single parameter, ``row``.

``post_save`` hooks should not return data.

.. code-block:: python

async def print_movie(row: Movie):
print(f'Movie {row.id} added to db.')


app = PiccoloCRUD(
table=Movie,
read_only=False,
hooks=[
Hook(hook_type=HookType.post_save, callable=print_movie)
]
)


pre_patch
~~~~~~~~~

Expand Down Expand Up @@ -107,6 +135,33 @@ Each function must return a dictionary which represent the data to be modified.
)


post_patch
~~~~~~~~~~

This hook runs during PATCH requests, after changing the specified row in
the database.

It takes two parameters, ``row_id`` which is the id of the row to be changed,
and ``values`` which is a dictionary of incoming values.

``post_patch`` hooks should not return data.

.. code-block:: python

async def print_movie_changes(row_id: int, values: dict):
current_db_row = await Movie.objects().get(Movie.id==row_id)
print(f'Movie {row_id} updated with values {values}')


app = PiccoloCRUD(
table=Movie,
read_only=False,
hooks=[
Hook(hook_type=HookType.post_patch, callable=print_movie_changes)
]
)


pre_delete
~~~~~~~~~~

Expand All @@ -131,6 +186,32 @@ It takes one parameter, ``row_id`` which is the id of the row to be deleted.
]
)


post_delete
~~~~~~~~~~~

This hook runs during DELETE requests, after deleting the specified row in
the database.

It takes one parameter, ``row_id`` which is the id of the row to be deleted.

``post_delete`` hooks should not return data.

.. code-block:: python

async def post_delete(row_id: int):
pass


app = PiccoloCRUD(
table=Movie,
read_only=False,
hooks=[
Hook(hook_type=HookType.post_delete, callable=post_delete)
]
)


Dependency injection
~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -159,4 +240,4 @@ HookType
Hook
~~~~

.. autoclass:: Hook
.. autoclass:: Hook
25 changes: 23 additions & 2 deletions piccolo_api/crud/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,14 @@ async def post_single(
row=row,
request=request,
)
response = await row.save().run()
response = (await row.save().run())[0]
if self._hook_map:
await execute_post_hooks(
hooks=self._hook_map,
hook_type=HookType.post_save,
row=row,
request=request,
)
json = dump_json(response)
# Returns the id of the inserted row.
return CustomJSONResponse(json, status_code=201)
Expand Down Expand Up @@ -1127,7 +1134,6 @@ async def put_single(
}

try:

await cls.update(values).where(
cls._meta.primary_key == row_id
).run()
Expand Down Expand Up @@ -1198,6 +1204,14 @@ async def patch_single(
.run()
)
assert new_row
if self._hook_map:
await execute_patch_hooks(
hooks=self._hook_map,
hook_type=HookType.post_patch,
row_id=row_id,
values=values,
request=request,
)
return CustomJSONResponse(
self.pydantic_model(**new_row).json()
)
Expand Down Expand Up @@ -1226,6 +1240,13 @@ async def delete_single(
await self.table.delete().where(
self.table._meta.primary_key == row_id
).run()
if self._hook_map:
await execute_delete_hooks(
hooks=self._hook_map,
hook_type=HookType.post_delete,
row_id=row_id,
request=request,
)
return Response(status_code=204)
except ValueError:
return Response("Unable to delete the resource.", status_code=500)
Expand Down
3 changes: 3 additions & 0 deletions piccolo_api/crud/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class HookType(Enum):
pre_save = "pre_save"
pre_patch = "pre_patch"
pre_delete = "pre_delete"
post_save = "post_save"
post_patch = "post_patch"
post_delete = "post_delete"


class Hook:
Expand Down
4 changes: 0 additions & 4 deletions tests/crud/test_crud_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ def test_patch_succeeds(self):
self.assertEqual(movies[0]["name"], new_name)

def test_patch_user_new_password(self):

client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))

json = {
Expand Down Expand Up @@ -211,7 +210,6 @@ def test_patch_user_new_password(self):
self.assertEqual(response.status_code, 200)

def test_patch_user_old_password(self):

client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))

json = {
Expand Down Expand Up @@ -240,7 +238,6 @@ def test_patch_user_old_password(self):
self.assertEqual(response.status_code, 200)

def test_patch_user_fails(self):

client = TestClient(PiccoloCRUD(table=BaseUser, read_only=False))

json = {
Expand Down Expand Up @@ -1163,7 +1160,6 @@ def test_post_user_fails(self):
self.assertEqual(response.status_code, 400)

def test_validation_error(self):

"""
Make sure a post returns a validation error with incorrect or missing
data.
Expand Down
69 changes: 69 additions & 0 deletions tests/crud/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,72 @@ def test_delete_hook_fails(self):

with self.assertRaises(Exception):
_ = client.delete(f"/{movie.id}/")

def test_post_save_hook_failed(self):
"""
Make sure failing post_save hook bubbles up
(this implicitly also tests that post_save hooks execute)
"""
client = TestClient(
PiccoloCRUD(
table=Movie,
read_only=False,
hooks=[
Hook(
hook_type=HookType.post_save,
callable=failing_hook,
)
],
)
)
json_req = {"name": "Star Wars", "rating": 93}

with self.assertRaises(Exception):
_ = client.post("/", json=json_req)

def test_post_patch_hook_failed(self):
"""
Make sure failing post_patch hook bubbles up
(this implicitly also tests that post_patch hooks execute)
"""
client = TestClient(
PiccoloCRUD(
table=Movie,
read_only=False,
hooks=[
Hook(
hook_type=HookType.post_patch,
callable=failing_hook,
)
],
)
)

movie = Movie(name="Star Wars", rating=93)
movie.save().run_sync()

new_name = "Star Wars: A New Hope"

with self.assertRaises(Exception):
_ = client.patch(f"/{movie.id}/", json={"name": new_name})

def test_post_delete_hook_fails(self):
"""
Make sure failing post_delete hook bubbles up
(this implicitly also tests that pre_delete hooks execute)
"""
client = TestClient(
PiccoloCRUD(
table=Movie,
read_only=False,
hooks=[
Hook(hook_type=HookType.post_delete, callable=failing_hook)
],
)
)

movie = Movie(name="Star Wars", rating=10)
movie.save().run_sync()

with self.assertRaises(Exception):
_ = client.delete(f"/{movie.id}/")
2 changes: 1 addition & 1 deletion tests/fastapi/test_fastapi_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def test_post(self):
"/movies/", json={"name": "Star Wars", "rating": 93}
)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.json(), [{"id": 2}])
self.assertEqual(response.json(), {"id": 2})

def test_put(self):
client = TestClient(app)
Expand Down