From 5900f320911a239c6a778ab35d44963722b56d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Lafr=C3=A9choux?= Date: Thu, 12 Jul 2018 18:36:33 +0200 Subject: [PATCH] WIP - Add docs --- docs/Makefile | 20 ++++ docs/api_reference.rst | 35 +++++++ docs/authors.rst | 1 + docs/changelog.rst | 3 + docs/conf.py | 104 +++++++++++++++++++ docs/etag.rst | 188 +++++++++++++++++++++++++++++++++++ docs/index.rst | 61 ++++++++++++ docs/license.rst | 4 + docs/make.bat | 36 +++++++ docs/pagination.rst | 114 +++++++++++++++++++++ docs/parameters.rst | 63 ++++++++++++ docs/quickstart.rst | 121 ++++++++++++++++++++++ docs/requirements.txt | 1 + docs/response.rst | 59 +++++++++++ flask_rest_api/blueprint.py | 1 + flask_rest_api/etag.py | 5 +- flask_rest_api/pagination.py | 18 +--- tests/test_examples.py | 2 + 18 files changed, 818 insertions(+), 18 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/api_reference.rst create mode 100644 docs/authors.rst create mode 100644 docs/changelog.rst create mode 100644 docs/conf.py create mode 100644 docs/etag.rst create mode 100644 docs/index.rst create mode 100644 docs/license.rst create mode 100644 docs/make.bat create mode 100644 docs/pagination.rst create mode 100644 docs/parameters.rst create mode 100644 docs/quickstart.rst create mode 100644 docs/requirements.txt create mode 100644 docs/response.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..266a6dc4 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = flask-rest-api +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/api_reference.rst b/docs/api_reference.rst new file mode 100644 index 00000000..98a83163 --- /dev/null +++ b/docs/api_reference.rst @@ -0,0 +1,35 @@ +.. _api: + +************* +API Reference +************* + +.. module:: flask_rest_api + +Api +=== + + +.. autoclass:: flask_rest_api.Api + :members: + +Blueprint +========= + +.. autoclass:: flask_rest_api.Blueprint + :members: + +Pagination +========== + +.. autoclass:: flask_rest_api.Page + :members: +.. autofunction:: flask_rest_api.set_item_count + +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 diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 00000000..e122f914 --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1 @@ +.. include:: ../AUTHORS.rst diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 00000000..76888fe1 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,3 @@ +.. _changelog: + +.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..198ac0d4 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,104 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +import flask_rest_api + +project = 'flask-rest-api' +copyright = '2018, Nobatek' + +version = release = flask_rest_api.__version__ + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', +] + +intersphinx_mapping = { + 'python': ('http://python.readthedocs.io/en/latest/', None), + 'marshmallow': ('http://marshmallow.readthedocs.io/en/latest/', None), + 'webargs': ('http://webargs.readthedocs.io/en/latest/', None), + 'flask': ('http://flask.readthedocs.io/en/latest/', None), +} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} diff --git a/docs/etag.rst b/docs/etag.rst new file mode 100644 index 00000000..9ce05ab8 --- /dev/null +++ b/docs/etag.rst @@ -0,0 +1,188 @@ +.. _etag: +.. module:: flask_rest_api + +ETag +==== + +ETag is a web cache validation mechanism. It allows an API client to make +conditional requests, such as + +- GET a resource unless it is the same as the version in cache. +- PUT/PATCH/DELETE a resource unless the version in cache is outdated. + +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 ` decorator. + +`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 +manual work involved. + + +ETag Computed with API Response Data +------------------------------------ + +The simplest case is when the ETag is computed using returned data, using the +:class:`Schema ` that serializes the data. + +In this case, almost eveything is automatic. Only the call to +:meth:`check_etag ` is manual. + +The :class:`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 + + @blp.route('/') + class Pet(MethodView): + + @blp.response(PetSchema(many=True)) + def get(self): + return Pet.get() + + @blp.arguments(PetSchema) + @blp.response(PetSchema) + def post(self, new_data): + return Pet.create(**new_data) + + @blp.route('/') + class PetById(MethodView): + + @blp.response(PetSchema) + def get(self, pet_id): + return Pet.get_by_id(pet_id) + + @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) + pet.update(update_data) + return pet + + @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) + Pet.delete(pet_id) + +ETag Computed with API Response Data Using Another Schema +--------------------------------------------------------- + +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 `. Then, it does not +need to be passed to :meth:`check_etag `. + +.. code-block:: python + :emphasize-lines: 7,12,19,24,28,32,36 + + from flask_rest_api import check_etag + + @blp.route('/') + class Pet(MethodView): + + @blp.response( + PetSchema(many=True), etag_schema=PetEtagSchema(many=True)) + def get(self): + return Pet.get() + + @blp.arguments(PetSchema) + @blp.response(PetSchema, etag_schema=PetEtagSchema) + def post(self, new_pet): + return Pet.create(**new_data) + + @blp.route('/') + class PetById(MethodView): + + @blp.response(PetSchema, etag_schema=PetEtagSchema) + def get(self, pet_id): + return Pet.get_by_id(pet_id) + + @blp.arguments(PetSchema) + @blp.response(PetSchema, etag_schema=PetEtagSchema) + 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) + pet.update(update_data) + return pet + + @blp.response(code=204, etag_schema=PetEtagSchema) + def delete(self, pet_id): + pet = self._get_pet(pet_id) + # Check ETag is a manual action, ETag schema is used + 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 ` manually. + +The example below illustrates this with no ETag schema, but it is also possible +to pass an ETag schema to :meth:`set_etag ` and +:meth:`check_etag ` or equivalently to +:meth:`Blueprint.response `. + +.. code-block:: python + :emphasize-lines: 10,17,26,34,37,44 + + from flask_rest_api import check_etag, set_etag + + @blp.route('/') + class Pet(MethodView): + + @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]) + return pets + + @blp.arguments(PetSchema) + @blp.response(PetSchema) + def post(self, new_data): + # Compute ETag using arbitrary data + set_etag(new_data['update_time']) + return Pet.create(**new_data) + + @blp.route('/') + class PetById(MethodView): + + @blp.response(PetSchema) + def get(self, pet_id): + # Compute ETag using arbitrary data + set_etag(new_data['update_time']) + return Pet.get_by_id(pet_id) + + @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']) + pet.update(update_data) + # Compute ETag using arbitrary data + set_etag(new_data['update_time']) + return pet + + @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']) + Pet.delete(pet_id) diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..018f860f --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,61 @@ +flask-rest-api: build a REST API on Flask using Marshmallow +=========================================================== + +Release v\ |version|. (:ref:`Changelog `) + +**flask-rest-api** is a framework library for creating REST APIs. + +It uses Flask as a webserver, and marshmallow_ to serialize and deserialize data. +It relies extensively on the marshmallow ecosystem, using webargs_ to get arguments +from requests, and apispec_ to generate an OpenAPI_ specification file as +automatically as possible. + + +Install +======= + +flask-rest-api requires Python >= 3.5. + +.. code-block:: bash + + $ pip install flask-rest-api + + +Guide +===== + +.. toctree:: + :maxdepth: 1 + + quickstart + parameters + response + pagination + etag + openapi + + +API Reference +============= + +.. toctree:: + :maxdepth: 2 + + api_reference + +Project Info +============ + +.. toctree:: + :maxdepth: 1 + + changelog + license + authors + + + +.. _marshmallow: https://marshmallow.readthedocs.io/ +.. _webargs: https://webargs.readthedocs.io/ +.. _apispec: https://apispec.readthedocs.io/ +.. _OpenAPI: https://www.openapis.org/ diff --git a/docs/license.rst b/docs/license.rst new file mode 100644 index 00000000..7e6291f3 --- /dev/null +++ b/docs/license.rst @@ -0,0 +1,4 @@ +License +======= + +.. include:: ../LICENSE diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..5be3e93d --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=flask-rest-api + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/pagination.rst b/docs/pagination.rst new file mode 100644 index 00000000..ebba691c --- /dev/null +++ b/docs/pagination.rst @@ -0,0 +1,114 @@ +.. _pagination: +.. module:: flask_rest_api + +Pagination +========== + +When returning a list of objects, it is generally good practice to paginate the +resource. This is when :meth:`Blueprint.paginate ` steps in. + +Pagination is more or less transparent to view function depending on the source +of the data that is returned. + + +Pagination in the View Function +------------------------------- + +In this mode, :meth:`Blueprint.paginate ` injects the +pagination parameters ``first_item`` and ``last_item`` as kwargs into the view +function. + +It is the responsability of the view function to return only selected elements. + +The view function must also specify the total number of elements using +:meth:`set_item_count `. + +.. code-block:: python + :emphasize-lines: 7,8,9,10 + + from flask_rest_api import set_item_count + + @blp.route('/') + class Pets(MethodView): + + @blp.response(PetSchema(many=True)) + @blp.paginate() + def get(self, first_item, last_item): + set_item_count(Pet.size) + return Pet.get_elements(first_item=first_item, last_item=last_item) + + +Post-Pagination +--------------- + +This is the mode to use when the data is returned as a lazy database cursor. +The view function does not need to know the pagination parameters. It just +returns the cursor. + +This mode is also used if the view function returns the complete ``list`` at no +extra cost and there is no interest in specifying the pagination parameters to +avoid fetching unneeded data. For instance, if the whole list is in already +memory. + +This mode makes the view look nicer because everything happens in the decorator. +Or so it looks like. + +In this case, :meth:`Blueprint.paginate ` must be passed a +cursor pager to take care of the pagination. `flask-rest-api` provides a pager +for `list`-like objects: :class:`Page `. When dealing with a +lazy database cursor, a custom cursor pager can be defined using a cursor +wrapper. + + +.. code-block:: python + :emphasize-lines: 18 + + from flask_rest_api import Page + + class CursorWrapper(): + def __init__(self, obj): + self.obj = obj + def __getitem__(self, key): + return self.obj[key] + def __len__(self): + return self.obj.count() + + class CursorPage(Page): + _wrapper_class = CursorWrapper + + @blp.route('/') + class Pets(MethodView): + + @blp.response(PetSchema(many=True)) + @blp.paginate(CursorPage) + def get(self): + return Pet.get() + +The custom wrapper defined in the example above works for SQLAlchemy or PyMongo +cursors. + + +Pagination Parameters +--------------------- + +Once a view function is decorated with +:meth:`Blueprint.paginate `, the client can request a +specific range of data by passing query arguments: + + +``GET /pets/?page=2&page_size=10`` + + +The view function gets default values for the pagination parameters, as well as +a maximum value for ``page_size``. + +Those default values are defined globally as + +.. code-block:: python + + DEFAULT_PAGINATION_PARAMETERS = { + 'page': 1, 'page_size': 10, 'max_page_size': 100} + +They can be modified globally by mutating ``DEFAULT_PAGINATION_PARAMETERS``, or +overwritten in a specific view function by passing them as keyword arguments to +:meth:`Blueprint.paginate `. diff --git a/docs/parameters.rst b/docs/parameters.rst new file mode 100644 index 00000000..33bdc038 --- /dev/null +++ b/docs/parameters.rst @@ -0,0 +1,63 @@ +.. _parameters: +.. module:: flask_rest_api + +Parameters +========== + +To inject parameters into a view function, use the :meth:`Blueprint.arguments ` decorator. + +This method takes a :class:`Schema ` to deserialize and +validate the parameters. + +By default, the parameters are expected to be passed as json in request body. +The location can be specified with the ``location`` parameter. + +Available locations are: + +- ``'querystring'`` or ``'query'`` +- ``'json'`` +- ``'form'`` +- ``'headers'`` +- ``'cookies'`` +- ``'files'`` + +Unlike webargs's :meth:`use_args ` decorator, +:meth:`Blueprint.arguments ` only accepts a single location. + +The input data is deserialized, validated, and injected in the view function as +a dict. + + +.. code-block:: python + :emphasize-lines: 4,9 + + @blp.route('/') + class Pets(MethodView): + + @blp.arguments(PetQueryArgsSchema, location='query') + @blp.response(PetSchema(many=True)) + def get(self, args): + return Pet.get(filters=args) + + @blp.arguments(PetSchema) + @blp.response(PetSchema, code=201) + def post(self, new_data): + return Pet.create(**new_data) + + +Keyword arguments provided to :meth:`Blueprint.arguments ` +are passed to webargs's :meth:`use_args `. + +If ``as_kwargs=True`` is passed, the decorator passes deserialized input data +as keyword arguments rather than as a single positional ``dict`` argument. + +.. code-block:: python + :emphasize-lines: 4,6,7 + + @blp.route('/') + class Pets(MethodView): + + @blp.arguments(PetQueryArgsSchema, location='query', as_kwargs=True) + @blp.response(PetSchema(many=True)) + def get(self, **kwargs): + return Pet.get(filters=**kwargs) diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 00000000..4a1af278 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,121 @@ +.. _quickstart: +.. module:: flask_rest_api + +Quickstart +========== + +``flask-rest-api`` makes a few assumptions about how the code should be structured. + +The application should be split in :class:`Blueprint `. +It is possible to use basic Flask view functions but it is generally a good idea +to use Flask :class:`MethodView ` classes instead. + +Marshmallow :class:`Schema ` are used to serialize parameters +and responses. It may look overkill for a method with a single parameter, but it +makes the code consistent and it is easier to support. + +Here is a basic Petstore example, where The ``Pet`` class is an imaginary ORM. + +First instantiate an :class:`Api ` with a :class:`Flask ` application. + + +.. code-block:: python + + from flask import Flask + from flask.views import MethodView + import marshmallow as ma + from flask_rest_api import Api, Blueprint + + from .model import Pet + + app = Flask('My API') + api = Api(app) + +Define a marshmallow :class:`Schema ` to expose the model. + +.. code-block:: python + + @api.definition('Pet') + class PetSchema(ma.Schema): + + class Meta: + strict = True + ordered = True + + id = ma.fields.Int(dump_only=True) + name = ma.fields.String() + + +Define a marshmallow :class:`Schema ` to validate the +query arguments. + +.. code-block:: python + + class PetQueryArgsSchema(ma.Schema): + + class Meta: + strict = True + ordered = True + + name = ma.fields.String() + + +Instantiate a :class:`Blueprint `. + +.. code-block:: python + + blp = Blueprint( + 'pets', 'pets', url_prefix='/pets', + description='Operations on pets' + ) + +:class:`MethodView ` classes come in handy when dealing +with REST APIs. + +.. code-block:: python + + @blp.route('/') + class Pets(MethodView): + + @blp.arguments(PetQueryArgsSchema, location='query') + @blp.response(PetSchema(many=True)) + def get(self, args): + """List pets""" + return Pet.get(filters=args) + + @blp.arguments(PetSchema) + @blp.response(PetSchema, code=201) + def post(self, new_data): + """Add a new pet""" + item = Pet.create(**new_data) + return item + + + @blp.route('/') + class PetsById(MethodView): + + @blp.response(PetSchema) + def get(self, pet_id): + """Get pet by ID""" + item = Pet.get_by_id(pet_id) + return item + + @blp.arguments(PetSchema) + @blp.response(PetSchema) + def put(self, update_data, pet_id): + """Update existing pet""" + item = Pet.get_by_id(pet_id) + item.update(update_data) + return item + + @blp.response(code=204) + def delete(self, pet_id): + """Delete pet""" + Pet.delete(pet_id) + + +Finally, register the :class:`Blueprint ` in the :class:`Api `. + +.. code-block:: python + + api.register_blueprint(blp) diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..e7b21d02 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +sphinx>=1.7.5 diff --git a/docs/response.rst b/docs/response.rst new file mode 100644 index 00000000..c28dd1cb --- /dev/null +++ b/docs/response.rst @@ -0,0 +1,59 @@ +.. _response: +.. module:: flask_rest_api + +Response +======== + +Use :meth:`Blueprint.response ` to specify a +:class:`Schema ` class or instance to serialize the +response and a status code (defaults to ``200``). + +In the following examples, the ``GET`` and ``PUT`` methods return an instance +of ``Pet`` serialized with ``PetSchema``: + +.. code-block:: python + :emphasize-lines: 4,9 + + @blp.route('/') + class PetsById(MethodView): + + @blp.response(PetSchema) + def get(self, pet_id): + return Pet.get_by_id(pet_id) + + @blp.arguments(PetSchema) + @blp.response(PetSchema) + def put(self, update_data, pet_id): + pet = Pet.get_by_id(pet_id) + pet.update(update_data) + return pet + +Here, the ``DELETE`` returns an empty response so no schema is specified. + +.. code-block:: python + :emphasize-lines: 4 + + @blp.route('/') + class PetsById(MethodView): + + @blp.response(code=204) + def delete(self, pet_id): + Pet.delete(pet_id) + +Assuming a view function returns an empty response with a ``200`` code, it is +still a good idea to decorate it with :meth:`Blueprint.response `, +because it will take care of creating a proper Flask :class:`Response `. + +If a view function returns a list of objects, the :class:`Schema ` +must be instanciated with ``many=True``. + +.. code-block:: python + :emphasize-lines: 5 + + @blp.route('/') + class Pets(MethodView): + + @blp.arguments(PetQueryArgsSchema, location='query') + @blp.response(PetSchema(many=True)) + def get(self, args): + return Pet.get(filters=args) diff --git a/flask_rest_api/blueprint.py b/flask_rest_api/blueprint.py index ffd23e98..5dedcb70 100644 --- a/flask_rest_api/blueprint.py +++ b/flask_rest_api/blueprint.py @@ -246,6 +246,7 @@ def response(schema=None, *, code=200, description='', :param schema: :class:`Schema ` class or instance. If not None, will be used to serialize response data. :param int code: HTTP status code (defaults to 200). + :param str descripton: Description of the response. :param etag_schema: :class:`Schema ` class or instance. If not None, will be used to serialize etag data. :param bool disable_etag: Disable ETag feature locally even if enabled diff --git a/flask_rest_api/etag.py b/flask_rest_api/etag.py index c9e34a4d..4cf25314 100644 --- a/flask_rest_api/etag.py +++ b/flask_rest_api/etag.py @@ -90,7 +90,7 @@ def check_precondition(): def check_etag(etag_data, etag_schema=None): """Compare If-Match header with computed ETag - Raise 412 if If-Match-Header does not match + Raise 412 if If-Match-Header does not match. Must be called from resource code to check ETag. @@ -112,7 +112,8 @@ def verify_check_etag(): Log a warning if ETag is enabled but check_etag was not called in resource code in a PUT, PATCH or DELETE method. - This is meant to warn the developer about an issue in his ETag management. + This is called automatically. It is meant to warn the developer about an + issue in his ETag management. """ if (is_etag_enabled_for_request() and request.method in METHODS_NEEDING_CHECK_ETAG): diff --git a/flask_rest_api/pagination.py b/flask_rest_api/pagination.py index f7326503..9e974188 100644 --- a/flask_rest_api/pagination.py +++ b/flask_rest_api/pagination.py @@ -136,21 +136,7 @@ class Page: """Pager for simple types such as lists. Can be subclassed to provide a pager for a specific data object. - - Example pager for Pymongo cursor: - - class PymongoCursorWrapper(): - def __init__(self, obj): - self.obj = obj - def __getitem__(self, range): - return self.obj[range] - def __len__(self): - return self.obj.count() - - class PymongoCursorPage(Page): - _wrapper_class = PymongoCursorWrapper """ - _wrapper_class = None def __init__(self, collection, page_params): @@ -188,7 +174,7 @@ def _get_pagination_ctx(): def set_item_count(item_count): """Set total number of items when paginating - When paginating in resource, this should be called from resource code + When paginating in resource, this must be called from resource code """ _get_pagination_ctx()['item_count'] = item_count @@ -201,7 +187,7 @@ def _set_pagination_header(page_params): try: item_count = _get_pagination_ctx()['item_count'] except KeyError: - # item_count is not set, this is a issue in the app. Pass and warn. + # item_count is not set, this is an issue in the app. Pass and warn. current_app.logger.warning( 'item_count not set in endpoint {}'.format(request.endpoint)) return diff --git a/tests/test_examples.py b/tests/test_examples.py index e1f9b32a..eff0cbe5 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -153,6 +153,8 @@ class Resource(MethodView): @blp.paginate() def get(self, first_item, last_item): set_item_count(len(collection.items)) + # It is better to rely on automatic ETag here, as it includes + # pagination metadata. return collection.items[first_item: last_item + 1] @blp.arguments(DocSchema)