Skip to content

Commit

Permalink
Add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
AMontagu committed Aug 2, 2024
1 parent 4f0094f commit fd1aeb3
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 13 deletions.
13 changes: 7 additions & 6 deletions django_socio_grpc/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ async def _simulate_function(
take an object that can proxy a django.http.request as param
and return an object that can proxy a django.http.response.
"""
# INFO - AM - 01/08/2024 - As a django decorator take only one request argument and grpc one 2 we are passing an instance of GRPCInternalProxyContext so we can get the context back from the request and make the actual call
# INFO - AM - 01/08/2024 - As a django decorator take only one request argument and grpc 2 we are passing an instance of GRPCInternalProxyContext so we can get the context back from the request and make the actual call
request = context.grpc_request
# INFO - AM - 01/08/2024 - Call the actual grpc endpoint
endpoint_result = await func(service_instance, request, context)
Expand Down Expand Up @@ -174,7 +174,7 @@ def _simulate_function(
take an object that can proxy a django.http.request as param
and return an object that can proxy a django.http.response.
"""
# INFO - AM - 01/08/2024 - As a django decorator take only one request argument and grpc one 2 we are passing an instance of GRPCInternalProxyContext so we can get the context back from the request and make the actual call
# INFO - AM - 01/08/2024 - As a django decorator take only one request argument and grpc 2 we are passing an instance of GRPCInternalProxyContext so we can get the context back from the request and make the actual call
request = context.grpc_request
# INFO - AM - 01/08/2024 - Call the actual grpc endpoint
endpoint_result = func(service_instance, request, context)
Expand Down Expand Up @@ -240,7 +240,7 @@ def cache_endpoint(*args, **kwargs):
)


def cache_endpoint_with_cache_deleter(
def cache_endpoint_with_deleter(
timeout: int,
key_prefix: str,
senders: Iterable[Model],
Expand All @@ -251,6 +251,7 @@ def cache_endpoint_with_cache_deleter(
This decorator do all the same as cache_endpoint but with the addition of a cache deleter.
The cache deleter will delete the cache when a signal is triggered.
This is useful when you want to delete the cache when a model is updated or deleted.
:param timeout: The timeout of the cache
:param key_prefix: The key prefix of the cache
:param cache: The cache alias to use. If None, it will use the default cache. It is named cache and not cache_alias to keep compatibility with Django cache_page decorator
Expand All @@ -259,15 +260,15 @@ def cache_endpoint_with_cache_deleter(
"""
if not key_prefix:
logger.warning(
"You are using cache_endpoint_with_cache_deleter without key_prefix. It's highly recommended to use it named as your service to avoid deleting all the cache without prefix when data is updated in back."
"You are using cache_endpoint_with_deleter without key_prefix. It's highly recommended to use it named as your service to avoid deleting all the cache without prefix when data is updated in back."
)
if (
cache is None
and not hasattr(default_cache, "delete_pattern")
and not grpc_settings.ENABLE_CACHE_WARNING_ON_DELETER
):
logger.warning(
"You are using cache_endpoint_with_cache_deleter with the default cache engine that is not a redis cache engine."
"You are using cache_endpoint_with_deleter with the default cache engine that is not a redis cache engine."
"Only Redis cache engine support cache pattern deletion."
"You still continue to use it but it will delete all the endpoint cache when signal will trigger."
"Please use a specific cache config per service or use redis cache engine to avoid this behavior."
Expand Down Expand Up @@ -296,7 +297,7 @@ def invalidate_cache(*args, **kwargs):
cache_instance.clear()
else:
logger.warning(
"You are using cache_endpoint_with_cache_deleter without senders. If you don't need the auto deleter just use cache_endpoint decorator."
"You are using cache_endpoint_with_deleter without senders. If you don't need the auto deleter just use cache_endpoint decorator."
)

return http_to_grpc(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django_socio_grpc import generics, mixins
from django_socio_grpc.decorators import (
cache_endpoint,
cache_endpoint_with_cache_deleter,
cache_endpoint_with_deleter,
grpc_action,
vary_on_metadata,
)
Expand Down Expand Up @@ -105,15 +105,15 @@ async def ListWithPossibilityMaxAge(self, request, context):
ListGenerationPlugin(response=True),
],
)
@cache_endpoint_with_cache_deleter(
@cache_endpoint_with_deleter(
300,
key_prefix="UnitTestModelWithCacheServiceWithCacheDeleter",
cache="second",
senders=(UnitTestModel,),
)
async def ListWithAutoCacheCleanOnSaveAndDelete(self, request, context):
"""
Test the cache_endpoint_with_cache_deleter work well
Test the cache_endpoint_with_deleter work well
"""
self.custom_function_not_called_when_cached(self)
return await super().List(request, context)
163 changes: 163 additions & 0 deletions docs/features/cache.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
.. _cache:

Cache
=====

Description
-----------

Usually django cache is used to cache page or basic ``GET`` request. However, in the context of gRPC, we use ``POST`` request and there is no `native cache system in gRPC in Python <https://github.com/grpc/grpc/issues/7945>`_.

Fortunately, DSG bring a layer of abstraction between gRPC request and Django request allowing us to use Django cache system.

To enable it follow the `Django instructions <https://docs.djangoproject.com/fr/5.0/topics/cache/#setting-up-the-cache>`_ then use the :ref:`cache_endpoint <cache-endpoint>` decorator or the :ref:`cache_endpoint_with_deleter <cache-endpoint-with-deleter>` to cache your endpoint.

.. _cache-endpoint:

cache_endpoint
--------------

Th :func:`cache_endpoint <django_socio_grpc.decorators.cache_endpoint>` decorator is used to adapt the `cache_page <https://docs.djangoproject.com/fr/5.0/topics/cache/#django.views.decorators.cache.cache_page>`_ decorator to work with grpc.

It took the same parameters and do the exact same things. See :ref:`Use Django decorators in DSG <use-django-decorators-in-dsg>` for more informations.

This decorator will cache response depending on:

* :ref:`Filters <filters>`
* :ref:`Pagination <pagination>`

Meaning that if you have a filter in your request, the cache will be different for each filter.

.. warning::

If you have request parameters that are not considered as filters or pagination, the cache will not be different for each request.


Example:

.. code-block:: python
from django_socio_grpc.decorators import cache_endpoint
...
class UnitTestModelWithCacheService(generics.AsyncModelService, mixins.AsyncStreamModelMixin):
queryset = UnitTestModel.objects.all().order_by("id")
serializer_class = UnitTestModelWithCacheSerializer
@grpc_action(
request=[],
response=UnitTestModelWithCacheSerializer,
use_generation_plugins=[
ListGenerationPlugin(response=True),
],
)
@cache_endpoint(300)
async def List(self, request, context):
return await super().List(request, context)
.. _cache-endpoint-with-deleter:

cache_endpoint_with_deleter
---------------------------

The :func:`cache_endpoint_with_deleter <django_socio_grpc.decorators.cache_endpoint_with_deleter>` decorator work the same :ref:`cache_endpoint <cache-endpoint>` but allow to automatically delete the cache when a django signals is called from the models passed in parameters.

As DSG is an API framework it's logic to add utils to invalidate cache if data is created, updated or deleted.

.. warning::

The cache will not be deleted if using bulk operations. This also integrate the usage of filter(...).update() method.
See `caveats of each meathod you wish to use to be sure of the behavior <https://docs.djangoproject.com/en/5.0/ref/models/querysets/#bulk-create>`_

There is also caveats to understand when usings cache-endpoint-with-deleter. As only Redis cache allow a pattern like deleter, if not using redis cache each specified signals on the specified models of the deleter will delete the entire cache.

To address this issue, you can:

* Use a `redis cache <https://docs.djangoproject.com/fr/5.0/topics/cache/#redis>`_
* Use a cache per model

.. note::

If you do not follow above advice a warning will show up everytimes you start the server. To disable it use the :ref:`ENABLE_CACHE_WARNING_ON_DELETER <settings-cache-warning-on-deleter>` setting.


Example:

.. code-block:: python
# SETTINGS
CACHES = {
"UnitTestModelCache": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "unit_test_model_cache_table",
}
}
# SERVICES
from django_socio_grpc.decorators import cache_endpoint_with_deleter
...
class UnitTestModelWithCacheService(generics.AsyncModelService, mixins.AsyncStreamModelMixin):
queryset = UnitTestModel.objects.all().order_by("id")
serializer_class = UnitTestModelWithCacheSerializer
@grpc_action(
request=[],
response=UnitTestModelWithCacheSerializer,
use_generation_plugins=[
ListGenerationPlugin(response=True),
],
)
@cache_endpoint_with_deleter(
300,
key_prefix="UnitTestModel",
cache="UnitTestModelCache",
senders=(UnitTestModel,),
)
async def List(self, request, context):
return await super().List(request, context)
.. _vary-on-metadata:

vary_on_metadata
----------------

Working like django `vary_on_headers <https://docs.djangoproject.com/fr/5.0/topics/cache/#using-vary-headers>`_ it's just a convenient renaming using :ref:`Use Django decorators in DSG <use-django-decorators-in-dsg>`.

It allow the cache to also `vary on metadata <https://github.com/grpc/grpc/tree/master/examples/python/metadata>`_ and not only filters and paginations.

Example:


.. code-block:: python
from django_socio_grpc.decorators import cache_endpoint, vary_on_metadata
...
class UnitTestModelWithCacheService(generics.AsyncModelService, mixins.AsyncStreamModelMixin):
queryset = UnitTestModel.objects.all().order_by("id")
serializer_class = UnitTestModelWithCacheSerializer
@grpc_action(
request=[],
response=UnitTestModelWithCacheSerializer,
use_generation_plugins=[
ListGenerationPlugin(response=True),
],
)
@cache_endpoint(300)
@vary_on_metadata("custom-metadata", "another-metadata")
async def List(self, request, context):
return await super().List(request, context)
.. _any-other-decorator:

Any other decorator
-------------------

As you can use :ref:`Django decorators in DSG <use-django-decorators-in-dsg>`. You can try to use any django decorators as long as they are wrapped into :func:`http_to_grpc decorator <django_socio_grpc.decorators.http_to_grpc>`.

If the one you are trying to use is not working as expected and it's not listed in the documentation page please fill an issue.
1 change: 1 addition & 0 deletions docs/features/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ Features
logging
streaming
commands
cache
health-check
1 change: 1 addition & 0 deletions docs/how-to/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Here is a collection of how-to guides for common tasks.
make-a-custom-retrieve
upload-file
work-with-secure-port
use-django-decorators-in-dsg
52 changes: 52 additions & 0 deletions docs/how-to/use-django-decorators-in-dsg.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.. _use-django-decorators-in-dsg:

Use Django decorators in DSG
=============================

In Django, decorators expect to decorate a function that takes a `django.http.HttpRequest <https://docs.djangoproject.com/en/5.0/ref/request-response/#httprequest-objects>`_ as its first argument and return a `django.http.HttpResponse <https://docs.djangoproject.com/en/5.0/ref/request-response/#httpresponse-objects>`_.

In DSG, decorators expect to decorate a class method that take 2 arguments: a `grpc Message<https://grpc.io/docs/languages/python/quickstart/#update-the-server>`_ as request and the `grpc context <https://grpc.github.io/grpc/python/grpc_asyncio.html#server-side-context>`_, and return a `grpc Message <https://grpc.io/docs/languages/python/quickstart/#update-the-server>`_ as response.

Both are based on HTTP protocol. So it's possible to find similar concept and usage.

By using :ref:`DSG proxy request and proxy response <request-and-response-proxy>` it is possible to simulate django behavior and apply it to gRPC calls.

See :func:`http_to_grpc decorator <django_socio_grpc.decorators.http_to_grpc>` for more detailsa and parameters.

.. _simple-example:

Simple example
--------------


.. code-block:: python
from django_socio_grpc.decorators import http_to_grpc
from django.views.decorators.vary import vary_on_headers
def vary_on_metadata(*headers):
return http_to_grpc(vary_on_headers(*headers))
.. _example-with-method-decorator-and-data-variance:

Example with method decorator and data variance
------------------------------------------------

In the following example we are transforming a fucntion decorator into a method decorator.
Then we are transforming it to a grpc decorator.
In the same time we specify that for each simulate Django request we want to set the ``method`` attribute to the value ``GET`` (this is because grpc only use POST request and Django only cache GET request)

The ``functools.wraps`` is optional and is used to keep the original function name and docstring.

.. code-block:: python
from django_socio_grpc.decorators import http_to_grpc
from django.views.decorators.cache import cache_page
@functools.wraps(cache_page)
def cache_endpoint(*args, **kwargs):
return http_to_grpc(
method_decorator(cache_page(*args, **kwargs)),
request_setter={"method": "GET"},
)
6 changes: 6 additions & 0 deletions docs/inner-workings/request-and-response-proxy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.. _request-and-response-proxy:

Request and Response proxy
==========================

Coming soon
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit fd1aeb3

Please sign in to comment.