Skip to content

Commit

Permalink
Merge 3652561 into 304bc0f
Browse files Browse the repository at this point in the history
  • Loading branch information
lafrech committed Nov 9, 2018
2 parents 304bc0f + 3652561 commit f681ca2
Show file tree
Hide file tree
Showing 9 changed files with 417 additions and 433 deletions.
23 changes: 18 additions & 5 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
Changelog
---------

0.11.0 (2018-11-09)
+++++++++++++++++++

Features:

- *Backwards-incompatible*: Rework of the ETag feature. It is now accesible
using dedicated ``Blueprint.etag`` decorator. ``check_etag`` and ``set_etag``
are methods of ``Blueprint`` and ``etag.INCLUDE_HEADERS`` is replaced with
``Blueprint.ETAG_INCLUDE_HEADERS``. It is enabled by default (only on views
decorated with ``Blueprint.etag``) and disabled with ``ETAG_DISABLED``
application configuration parameter. ``is_etag_enabled`` is now private.

0.10.0 (2018-10-24)
+++++++++++++++++++

Features:

- *Backwards-incompatible*: Don't prefix all routes in the spec with
`APPLICATION_ROOT`. If using OpenAPI v2, set `APPLICATION_ROOT` as
`basePath`. If using OpenAPI v3, the user should specify `servers` manually.
- *Backwards-incompatible*: In testing and debug modes, `verify_check_etag` not
only logs a warning but also raises `CheckEtagNotCalledError` if `check_etag`
is not called in a resource that needs it.
``APPLICATION_ROOT``. If using OpenAPI v2, set ``APPLICATION_ROOT`` as
``basePath``. If using OpenAPI v3, the user should specify ``servers``
manually.
- *Backwards-incompatible*: In testing and debug modes, ``verify_check_etag``
not only logs a warning but also raises ``CheckEtagNotCalledError`` if
``check_etag`` is not called in a resource that needs it.

0.9.2 (2018-10-16)
++++++++++++++++++
Expand Down
11 changes: 3 additions & 8 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Blueprint
.. automethod:: arguments
.. automethod:: response
.. automethod:: paginate
.. automethod:: etag
.. automethod:: check_etag
.. automethod:: set_etag

Pagination
==========
Expand All @@ -30,11 +33,3 @@ Pagination
:members:
.. autoclass:: flask_rest_api.pagination.PaginationParameters
:members:

ETag
====

.. autofunction:: flask_rest_api.etag.is_etag_enabled
.. autofunction:: flask_rest_api.etag.is_etag_enabled_for_request
.. autofunction:: flask_rest_api.etag.check_etag
.. autofunction:: flask_rest_api.etag.set_etag
101 changes: 57 additions & 44 deletions docs/etag.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ The first case is mostly useful to limit the bandwidth usage, the latter
addresses the case where two clients update a resource at the same time (known
as the "*lost update problem*").

The ETag featured is enabled with the `ETAG_ENABLED` application parameter. It
can be disabled function-wise by passing `disable_etag=False` to the
:meth:`Blueprint.response <Blueprint.response>` decorator.
The ETag featured is available through the
:meth:`Blueprint.etag <Blueprint.etag>` decorator. It can be disabled globally
with the `ETAG_DISABLED` application parameter.

`flask-rest-api` provides helpers to compute ETag, but ultimately, only the
developer knows what data is relevant to use as ETag source, so there can be
Expand All @@ -29,23 +29,23 @@ The simplest case is when the ETag is computed using returned data, using the
:class:`Schema <marshmallow.Schema>` that serializes the data.

In this case, almost eveything is automatic. Only the call to
:meth:`check_etag <etag.check_etag>` is manual.
:meth:`Blueprint.check_etag <Blueprint.check_etag>` is manual.

The :class:`Schema <marshmallow.Schema>` must be provided explicitly, even
though it is the same as the response schema.

.. code-block:: python
:emphasize-lines: 27,35
from flask_rest_api import check_etag
:emphasize-lines: 29,38
@blp.route('/')
class Pet(MethodView):
@blp.etag
@blp.response(PetSchema(many=True))
def get(self):
return Pet.get()
@blp.etag
@blp.arguments(PetSchema)
@blp.response(PetSchema)
def post(self, new_data):
Expand All @@ -54,24 +54,27 @@ though it is the same as the response schema.
@blp.route('/<pet_id>')
class PetById(MethodView):
@blp.etag
@blp.response(PetSchema)
def get(self, pet_id):
return Pet.get_by_id(pet_id)
@blp.etag
@blp.arguments(PetSchema)
@blp.response(PetSchema)
def put(self, update_data, pet_id):
pet = Pet.get_by_id(pet_id)
# Check ETag is a manual action and schema must be provided
check_etag(pet, PetSchema)
blp.check_etag(pet, PetSchema)
pet.update(update_data)
return pet
@blp.etag
@blp.response(code=204)
def delete(self, pet_id):
pet = Pet.get_by_id(pet_id)
# Check ETag is a manual action and schema must be provided
check_etag(pet, PetSchema)
blp.check_etag(pet, PetSchema)
Pet.delete(pet_id)
ETag Computed with API Response Data Using Another Schema
Expand All @@ -81,122 +84,132 @@ Sometimes, it is not possible to use the data returned by the view function as
ETag data because it contains extra information that is irrelevant, like
HATEOAS information, for instance.

In this case, a specific ETag schema can be provided as ``etag_schema`` keyword
argument to :meth:`Blueprint.response <Blueprint.response>`. Then, it does not
need to be passed to :meth:`check_etag <etag.check_etag>`.
In this case, a specific ETag schema should be provided to
:meth:`Blueprint.etag <Blueprint.etag>`. Then, it does not need to be passed to
:meth:`check_etag <Blueprint.check_etag>`.

.. code-block:: python
:emphasize-lines: 7,12,19,24,28,32,36
from flask_rest_api import check_etag
:emphasize-lines: 4,9,18,23,29,33,38
@blp.route('/')
class Pet(MethodView):
@blp.response(
PetSchema(many=True), etag_schema=PetEtagSchema(many=True))
@blp.etag(PetEtagSchema(many=True))
@blp.response(PetSchema(many=True))
def get(self):
return Pet.get()
@blp.etag(PetEtagSchema)
@blp.arguments(PetSchema)
@blp.response(PetSchema, etag_schema=PetEtagSchema)
@blp.response(PetSchema)
def post(self, new_pet):
return Pet.create(**new_data)
@blp.route('/<int:pet_id>')
class PetById(MethodView):
@blp.response(PetSchema, etag_schema=PetEtagSchema)
@blp.etag(PetEtagSchema)
@blp.response(PetSchema)
def get(self, pet_id):
return Pet.get_by_id(pet_id)
@blp.etag(PetEtagSchema)
@blp.arguments(PetSchema)
@blp.response(PetSchema, etag_schema=PetEtagSchema)
@blp.response(PetSchema)
def put(self, new_pet, pet_id):
pet = Pet.get_by_id(pet_id)
# Check ETag is a manual action and schema must be provided
check_etag(pet)
blp.check_etag(pet)
pet.update(update_data)
return pet
@blp.response(code=204, etag_schema=PetEtagSchema)
@blp.etag(PetEtagSchema)
@blp.response(code=204)
def delete(self, pet_id):
pet = self._get_pet(pet_id)
# Check ETag is a manual action, ETag schema is used
check_etag(pet)
blp.check_etag(pet)
Pet.delete(pet_id)
ETag Computed on Arbitrary Data
-------------------------------

The ETag can also be computed from arbitrary data by calling
:meth:`set_etag <etag.set_etag>` manually.
:meth:`Blueprint.set_etag <Blueprint.set_etag>` manually.

The example below illustrates this with no ETag schema, but it is also possible
to pass an ETag schema to :meth:`set_etag <etag.set_etag>` and
:meth:`check_etag <etag.check_etag>` or equivalently to
:meth:`Blueprint.response <Blueprint.response>`.
to pass an ETag schema to :meth:`set_etag <Blueprint.set_etag>` and
:meth:`check_etag <Blueprint.check_etag>` or equivalently to
:meth:`Blueprint.etag <Blueprint.etag>`.

.. code-block:: python
:emphasize-lines: 10,17,26,34,37,44
from flask_rest_api import check_etag, set_etag
:emphasize-lines: 4,9,12,17,23,27,30,36,39,42,47
@blp.route('/')
class Pet(MethodView):
@blp.etag
@blp.response(PetSchema(many=True))
def get(self):
pets = Pet.get()
# Compute ETag using arbitrary data
set_etag([pet.update_time for pet in pets])
blp.set_etag([pet.update_time for pet in pets])
return pets
@blp.etag
@blp.arguments(PetSchema)
@blp.response(PetSchema)
def post(self, new_data):
# Compute ETag using arbitrary data
set_etag(new_data['update_time'])
blp.set_etag(new_data['update_time'])
return Pet.create(**new_data)
@blp.route('/<pet_id>')
class PetById(MethodView):
@blp.etag
@blp.response(PetSchema)
def get(self, pet_id):
# Compute ETag using arbitrary data
set_etag(new_data['update_time'])
blp.set_etag(new_data['update_time'])
return Pet.get_by_id(pet_id)
@blp.etag
@blp.arguments(PetSchema)
@blp.response(PetSchema)
def put(self, update_data, pet_id):
pet = Pet.get_by_id(pet_id)
# Check ETag is a manual action
check_etag(pet, ['update_time'])
blp.check_etag(pet, ['update_time'])
pet.update(update_data)
# Compute ETag using arbitrary data
set_etag(new_data['update_time'])
blp.set_etag(new_data['update_time'])
return pet
@blp.etag
@blp.response(code=204)
def delete(self, pet_id):
pet = Pet.get_by_id(pet_id)
# Check ETag is a manual action
check_etag(pet, ['update_time'])
blp.check_etag(pet, ['update_time'])
Pet.delete(pet_id)
ETag not checked warning
------------------------

It is up to the developer to call
:meth:`Blueprint.check_etag <Blueprint.check_etag>` in the view function. It
can't be automatic.

If ETag is enabled and :meth:`check_etag <Blueprint.check_etag>` is not called,
a warning is logged at runtime. When in `DEBUG` or `TESTING` mode, an exception
is raised.

Include Headers Content in ETag
-------------------------------

When ETag is computed with response data, that data may contain headers. It is
up to the developer to decide whether this data should be part of the ETag.

By default, only pagination data is included in the ETag computation. The list
of headers to include is defined as:

.. code-block:: python
INCLUDE_HEADERS = ['X-Pagination']
It can be changed globally by mutating ``flask_rest_api.etag.INCLUDE_HEADERS``.
By default, only pagination header is included in the ETag computation. This
can be changed by customizing `Blueprint.ETAG_INCLUDE_HEADERS`.
1 change: 0 additions & 1 deletion flask_rest_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from .spec import APISpec, DocBlueprintMixin
from .blueprint import Blueprint # noqa
from .etag import is_etag_enabled, check_etag, set_etag # noqa
from .pagination import Page # noqa
from .error_handler import ErrorHandlerMixin
from .compat import APISPEC_VERSION_MAJOR
Expand Down
4 changes: 3 additions & 1 deletion flask_rest_api/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from .arguments import ArgumentsMixin
from .response import ResponseMixin
from .pagination import PaginationMixin
from .etag import EtagMixin
from .exceptions import EndpointMethodDocAlreadyRegistedError
from .compat import APISPEC_VERSION_MAJOR

Expand All @@ -46,7 +47,8 @@


class Blueprint(
FlaskBlueprint, ArgumentsMixin, ResponseMixin, PaginationMixin):
FlaskBlueprint,
ArgumentsMixin, ResponseMixin, PaginationMixin, EtagMixin):
"""Blueprint that registers info in API documentation"""

def __init__(self, *args, **kwargs):
Expand Down

0 comments on commit f681ca2

Please sign in to comment.