Skip to content

Commit

Permalink
hooks (#120)
Browse files Browse the repository at this point in the history
* initial exploration of hooks

* handle empty hook list

* completed suggestion for initial hook implementation

* lint

* refactorings and fixes from review comments

* fix older python typing incompatabiliy

* reverted old-python fix

* fixed docs typo

* fix type hint mismatch

* doc typos and corrections
  • Loading branch information
trondhindenes committed Dec 30, 2021
1 parent cd3bc74 commit bed6678
Show file tree
Hide file tree
Showing 5 changed files with 374 additions and 0 deletions.
102 changes: 102 additions & 0 deletions docs/source/crud/hooks.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
Hooks
=====

Hooks allow executing custom code as part of processing a crud request. You can use this to validate data, call another custom api, place messages on queues and many other things.

Enabling a hook
---------------

Define a method, and register it with PiccoloCRUD:

.. code-block:: python
# app.py
from piccolo_api.crud.endpoints import PiccoloCRUD
from movies.tables import Movie
# set movie rating to 10 before saving
async def set_movie_rating_10(row: Movie):
row.rating = 10
return row
# set movie rating to 20 before saving
async def set_movie_rating_10(row: Movie):
row.rating = 20
return row
async def pre_delete(row_id):
pass
# Register one or multiple hooks
app = PiccoloCRUD(table=Movie, read_only=False, hooks=[
Hook(hook_type=HookType.pre_save, callable=set_movie_rating_10),
Hook(hook_type=HookType.pre_save, callable=set_movie_rating_20),
Hook(hook_type=HookType.pre_delete, callable=pre_delete)
]
)
You can specify multiple hooks (also per hook_type). Hooks are executed in order.
You can use either async or regular functions.

Hook types
----------

There are different hook types, and each type takes a slightly different set of inputs.
It's also important to return the expected data from your hook.

pre_save
~~~~~~~~

This hook runs during POST requests, prior to inserting data into the database.
It takes a single parameter, ``row``, and should return the same:

.. code-block:: python
async def set_movie_rating_10(row: Movie):
row.rating = 10
return row
app = PiccoloCRUD(table=Movie, read_only=False, hooks=[
Hook(hook_type=HookType.pre_save, callable=set_movie_rating_10)
]
)
pre_patch
~~~~~~~~~

This hook runs during PATCH requests, prior to 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.
Each function must return a dictionary which represent the data to be modified.

.. code-block:: python
async def reset_name(row_id: int, values: dict):
current_db_row = await Movie.objects().get(Movie.id==row_id).run()
if values.get("name"):
values["name"] = values["name"].replace(" ", "")
return values
app = PiccoloCRUD(table=Movie, read_only=False, hooks=[
Hook(hook_type=HookType.pre_patch, callable=reset_name)
]
)
pre_delete
~~~~~~~~~~

This hook runs during DELETE requests, prior to deleting the specified row in the database.
It takes one parameter, ``row_id`` which is the id of the row to be deleted.
pre_delete hooks should not return data

.. code-block:: python
async def pre_delete(row_id: int):
pass
app = PiccoloCRUD(table=Movie, read_only=False, hooks=[
Hook(hook_type=HookType.pre_delete, callable=pre_delete)
]
)
1 change: 1 addition & 0 deletions docs/source/crud/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ Piccolo tables into a powerful REST API.

./piccolo_crud
./serializers
./hooks
37 changes: 37 additions & 0 deletions piccolo_api/crud/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@
from starlette.responses import JSONResponse, Response
from starlette.routing import Route, Router

from piccolo_api.crud.hooks import (
Hook,
HookType,
execute_delete_hooks,
execute_patch_hooks,
execute_post_hooks,
)

from .exceptions import MalformedQuery
from .serializers import Config, create_pydantic_model
from .validators import Validators, apply_validators
Expand Down Expand Up @@ -134,6 +142,7 @@ def __init__(
validators: Validators = Validators(),
schema_extra: t.Optional[t.Dict[str, t.Any]] = None,
max_joins: int = 0,
hooks: t.Optional[t.List[Hook]] = None,
) -> None:
"""
:param table:
Expand Down Expand Up @@ -196,6 +205,13 @@ def __init__(
self.exclude_secrets = exclude_secrets
self.validators = validators
self.max_joins = max_joins
if hooks:
self._hook_map = {
group[0]: [hook for hook in group[1]]
for group in itertools.groupby(hooks, lambda x: x.hook_type)
}
else:
self._hook_map = None # type: ignore

schema_extra = schema_extra if isinstance(schema_extra, dict) else {}
self.visible_fields_options = get_visible_fields_options(
Expand Down Expand Up @@ -741,6 +757,10 @@ async def post_single(

try:
row = self.table(**model.dict())
if self._hook_map:
row = await execute_post_hooks(
hooks=self._hook_map, hook_type=HookType.pre_save, row=row
)
response = await row.save().run()
json = dump_json(response)
# Returns the id of the inserted row.
Expand Down Expand Up @@ -938,6 +958,7 @@ async def put_single(
}

try:

await cls.update(values).where(
cls._meta.primary_key == row_id
).run()
Expand Down Expand Up @@ -971,6 +992,14 @@ async def patch_single(
f"Unrecognised keys - {unrecognised_keys}.", status_code=400
)

if self._hook_map:
values = await execute_patch_hooks(
hooks=self._hook_map,
hook_type=HookType.pre_patch,
row_id=row_id,
values=values,
)

try:
await cls.update(values).where(
cls._meta.primary_key == row_id
Expand All @@ -990,6 +1019,14 @@ async def delete_single(self, request: Request, row_id: int) -> Response:
"""
Deletes a single row.
"""

if self._hook_map:
await execute_delete_hooks(
hooks=self._hook_map,
hook_type=HookType.pre_delete,
row_id=row_id,
)

try:
await self.table.delete().where(
self.table._meta.primary_key == row_id
Expand Down
52 changes: 52 additions & 0 deletions piccolo_api/crud/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import inspect
import typing as t
from enum import Enum

from piccolo.table import Table


class HookType(Enum):
pre_save = "pre_save"
pre_patch = "pre_patch"
pre_delete = "pre_delete"


class Hook:
def __init__(self, hook_type: HookType, callable: t.Callable) -> None:
self.hook_type = hook_type
self.callable = callable


async def execute_post_hooks(
hooks: t.Dict[HookType, t.List[Hook]], hook_type: HookType, row: Table
):
for hook in hooks.get(hook_type, []):
if inspect.iscoroutinefunction(hook.callable):
row = await hook.callable(row)
else:
row = hook.callable(row)
return row


async def execute_patch_hooks(
hooks: t.Dict[HookType, t.List[Hook]],
hook_type: HookType,
row_id: t.Any,
values: t.Dict[t.Any, t.Any],
) -> t.Dict[t.Any, t.Any]:
for hook in hooks.get(hook_type, []):
if inspect.iscoroutinefunction(hook.callable):
values = await hook.callable(row_id=row_id, values=values)
else:
values = hook.callable(row_id=row_id, values=values)
return values


async def execute_delete_hooks(
hooks: t.Dict[HookType, t.List[Hook]], hook_type: HookType, row_id: t.Any
):
for hook in hooks.get(hook_type, []):
if inspect.iscoroutinefunction(hook.callable):
await hook.callable(row_id=row_id)
else:
hook.callable(row_id=row_id)

0 comments on commit bed6678

Please sign in to comment.