File: using_plugins.rst

package info (click to toggle)
apispec 6.8.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 652 kB
  • sloc: python: 4,852; makefile: 150; sh: 10
file content (407 lines) | stat: -rw-r--r-- 13,253 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
Using Plugins
=============

What is an apispec "plugin"?
----------------------------

An apispec *plugin* is an object that provides helper methods for generating OpenAPI entities from objects in your application.

A plugin may modify the behavior of `APISpec <apispec.APISpec>` methods so that they can take your application's objects as input.

Enabling Plugins
----------------

To enable a plugin, pass an instance to the constructor of `APISpec <apispec.APISpec>`.

.. code-block:: python
    :emphasize-lines: 9

    from apispec import APISpec
    from apispec.ext.marshmallow import MarshmallowPlugin

    spec = APISpec(
        title="Gisty",
        version="1.0.0",
        openapi_version="3.0.2",
        info=dict(description="A minimal gist API"),
        plugins=[MarshmallowPlugin()],
    )


Example: Flask and Marshmallow Plugins
--------------------------------------

The bundled marshmallow plugin (`apispec.ext.marshmallow.MarshmallowPlugin`)
provides helpers for generating OpenAPI schema and parameter objects from `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ schemas and fields.

The `apispec-webframeworks <https://github.com/marshmallow-code/apispec-webframeworks>`_
package includes a Flask plugin with helpers for generating path objects from view functions.

Let's recreate the spec from the :doc:`Quickstart guide <quickstart>` using these two plugins.

First, ensure that ``apispec-webframeworks`` is installed: ::

    $ pip install apispec-webframeworks

Also, ensure that a compatible ``marshmallow`` version is used: ::

    $ pip install -U apispec[marshmallow]

We can now use the marshmallow and Flask plugins.

.. code-block:: python

    from apispec import APISpec
    from apispec.ext.marshmallow import MarshmallowPlugin
    from apispec_webframeworks.flask import FlaskPlugin

    spec = APISpec(
        title="Gisty",
        version="1.0.0",
        openapi_version="3.0.2",
        info=dict(description="A minimal gist API"),
        plugins=[FlaskPlugin(), MarshmallowPlugin()],
    )


Our application will have a marshmallow `Schema <marshmallow.Schema>` for gists.

.. code-block:: python

    from marshmallow import Schema, fields


    class GistParameter(Schema):
        gist_id = fields.Int()


    class GistSchema(Schema):
        id = fields.Int()
        content = fields.Str()


The marshmallow plugin allows us to pass this `Schema` to
`spec.components.schema <apispec.core.Components.schema>`.


.. code-block:: python

    spec.components.schema("Gist", schema=GistSchema)

The schema is now added to the spec.

.. code-block:: python

    from pprint import pprint

    pprint(spec.to_dict())
    # {'components': {'parameters': {}, 'responses': {}, 'schemas': {}},
    #  'info': {'description': 'A minimal gist API',
    #           'title': 'Gisty',
    #           'version': '1.0.0'},
    #  'openapi': '3.0.2',
    #  'paths': {},
    #  'tags': []}

Our application will have a Flask route for the gist detail endpoint.

We'll add some YAML in the docstring to add response information.

.. code-block:: python

    from flask import Flask

    app = Flask(__name__)


    # NOTE: Plugins may inspect docstrings to gather more information for the spec
    @app.route("/gists/<gist_id>")
    def gist_detail(gist_id):
        """Gist detail view.
        ---
        get:
          parameters:
          - in: path
            schema: GistParameter
          responses:
            200:
              content:
                application/json:
                  schema: GistSchema
        """
        return "details about gist {}".format(gist_id)

The Flask plugin allows us to pass this view to `spec.path <apispec.APISpec.path>`.


.. code-block:: python

    # Since path inspects the view and its route,
    # we need to be in a Flask request context
    with app.test_request_context():
        spec.path(view=gist_detail)


Our OpenAPI spec now looks like this:

.. code-block:: python

    pprint(spec.to_dict())
    # {'components': {'parameters': {},
    #                 'responses': {},
    #                 'schemas': {'Gist': {'properties': {'content': {'type': 'string'},
    #                                                     'id': {'format': 'int32',
    #                                                            'type': 'integer'}},
    #                                      'type': 'object'}}},
    #  'info': {'description': 'A minimal gist API',
    #           'title': 'Gisty',
    #           'version': '1.0.0'},
    #  'openapi': '3.0.2',
    #  'paths': {'/gists/{gist_id}': {'get': {'parameters': [{'in': 'path',
    #                                                        'name': 'gist_id',
    #                                                        'required': True,
    #                                                        'schema': {'format': 'int32',
    #                                                                   'type': 'integer'}}],
    #                                         'responses': {200: {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Gist'}}}}}}}},
    #  'tags': []}

If your API uses `method-based dispatching <http://flask.pocoo.org/docs/0.12/views/#method-based-dispatching>`_, the process is similar. Note that the method no longer needs to be included in the docstring.

.. code-block:: python

    from flask.views import MethodView


    class GistApi(MethodView):
        def get(self):
            """Gist view
            ---
            description: Get a gist
            responses:
              200:
                content:
                  application/json:
                    schema: GistSchema
            """
            pass

        def post(self):
            pass


    method_view = GistApi.as_view("gist")
    app.add_url_rule("/gist", view_func=method_view)
    with app.test_request_context():
        spec.path(view=method_view)
    pprint(dict(spec.to_dict()["paths"]["/gist"]))
    # {'get': {'description': 'get a gist',
    #          'responses': {200: {'content': {'application/json': {'schema': {'$ref': '#/components/schemas/Gist'}}}}}},
    #  'post': {}}


Marshmallow Plugin
------------------

.. _marshmallow_nested_schemas:

Nested Schemas
**************

By default, Marshmallow `Nested` fields are represented by a `JSON Reference object
<https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#referenceObject>`_.
If the schema has been added to the spec via `spec.components.schema <apispec.core.Components.schema>`,
the user-supplied name will be used in the reference. Otherwise apispec will
add the nested schema to the spec using an automatically resolved name for the
nested schema. The default `resolver <apispec.ext.marshmallow.resolver>`
function will resolve a name based on the schema's class `__name__`, dropping a
trailing "Schema" so that `class PetSchema(Schema)` resolves to "Pet".

To change the behavior of the name resolution simply pass a
function accepting a `Schema` class, `Schema` instance or a string that resolves
to a `Schema` class and returning a string to the plugin's
constructor. To easily work with these argument types the marshmallow plugin provides
`resolve_schema_cls <apispec.ext.marshmallow.common.resolve_schema_cls>`
and `resolve_schema_instance <apispec.ext.marshmallow.common.resolve_schema_instance>`
functions. If the `schema_name_resolver` function returns a value that
evaluates to `False` in a boolean context the nested schema will not be added to
the spec and instead defined in-line.

.. note::
    A `schema_name_resolver` function must return a string name when
    working with circular-referencing schemas in order to avoid infinite
    recursion.

Schema Modifiers
****************

apispec will respect schema modifiers such as ``exclude`` and ``partial`` in the generated schema definition. If a schema is initialized with modifiers, apispec will treat each combination of modifiers as a unique schema definition.

Custom DateTime formats
***********************

apispec supports all four basic formats of `marshmallow.fields.DateTime`: ``"rfc"`` (for RFC822), ``"iso"`` (for ISO8601), 
``"timestamp"``, ``"timestamp_ms"`` (for a POSIX timestamp).

If you are using a custom DateTime format you should pass a regex string to the ``pattern`` parameter in your field ``metadata`` so that it is included as documentation. 

.. code-block:: python

    class SchemaWithCustomDate(Schema):
        french_date = ma.DateTime(
            format="%d-%m%Y %H:%M:%S",
            metadata={"pattern": r"^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$"},
        )

Custom Fields
*************

apispec maps standard marshmallow fields to OpenAPI types and formats. If your
custom field subclasses a standard marshmallow `Field` class then it will
inherit the default mapping. If you want to override the OpenAPI type and format
for custom fields, use the
`map_to_openapi_type <apispec.ext.marshmallow.MarshmallowPlugin.map_to_openapi_type>`
method. It can be invoked with either a pair of strings providing the
OpenAPI type and format, or a marshmallow `Field` that has the desired target mapping.

.. code-block:: python

    from apispec import APISpec
    from apispec.ext.marshmallow import MarshmallowPlugin
    from marshmallow.fields import Integer, Field

    ma_plugin = MarshmallowPlugin()

    spec = APISpec(
        title="Demo", version="0.1", openapi_version="3.0.0", plugins=(ma_plugin,)
    )


    # Inherits Integer mapping of ('integer', None)
    class CustomInteger(Integer):
        pass


    # Override Integer mapping
    class Int32(Integer):
        pass


    ma_plugin.map_to_openapi_type(Int32, "string", "int32")


    # Map to ('integer', None) like Integer
    class IntegerLike(Field):
        pass


    ma_plugin.map_to_openapi_type(IntegerLike, Integer)

In situations where greater control of the properties generated for a custom field
is desired, users may add custom logic to the conversion of fields to OpenAPI properties
through the use of the `add_attribute_function
<apispec.ext.marshmallow.field_converter.FieldConverterMixin.add_attribute_function>`
method. Continuing from the example above:

.. code-block:: python

    def my_custom_field2properties(self, field, **kwargs):
        """Add an OpenAPI extension flag to MyCustomField instances"""
        ret = {}
        if isinstance(field, MyCustomField):
            if self.openapi_version.major > 2:
                ret["x-customString"] = True
        return ret


    ma_plugin.converter.add_attribute_function(my_custom_field2properties)

The function passed to `add_attribute_function` will be bound to the converter.
It must accept the converter instance as first positional argument.

In some rare cases, typically with container fields such as fields derived from
:class:`List <marshmallow.fields.List>`, documenting the parameters using this
field require some more customization.
This can be achieved using the `add_parameter_attribute_function
<apispec.ext.marshmallow.openapi.OpenAPIConverter.add_parameter_attribute_function>`
method.

For instance, when documenting webargs's
:class:`DelimitedList <webargs.fields.DelimitedList>` field, one may register
this function:

.. code-block:: python

    def delimited_list2param(self, field, **kwargs):
        ret: dict = {}
        if isinstance(field, DelimitedList):
            if self.openapi_version.major < 3:
                ret["collectionFormat"] = "csv"
            else:
                ret["explode"] = False
                ret["style"] = "form"
        return ret


    ma_plugin.converter.add_parameter_attribute_function(delimited_list2param)

Enum Fields
***********

When using `marshmallow.fields.Enum` fields to (de)serialize `enum.Enum` values, we recommend passing a marshmallow field to ``by_value``.
This ensures the correct ``type`` property is included in the generated OAI spec.


.. code-block:: python
    :emphasize-lines: 23,42

    from enum import Enum
    from apispec import APISpec

    from apispec.ext.marshmallow import MarshmallowPlugin
    from marshmallow import Schema, fields

    spec = APISpec(
        title="Gisty",
        version="1.0.0",
        openapi_version="3.0.2",
        info=dict(description="A minimal gist API"),
        plugins=[MarshmallowPlugin()],
    )


    class GistVisibility(Enum):
        PRIVATE = "private"
        PUBLIC = "public"


    class GistSchema(Schema):
        id = fields.Int()
        visibility = fields.Enum(GistVisibility, by_value=fields.String())


    spec.components.schema("Gist", schema=GistSchema)
    print(spec.to_yaml())
    # info:
    #   description: A minimal gist API
    #   title: Gisty
    #   version: 1.0.0
    # paths: {}
    # openapi: 3.0.2
    # components:
    #   schemas:
    #     Gist:
    #       type: object
    #       properties:
    #         id:
    #           type: integer
    #         visibility:
    #           type: string
    #           enum:
    #           - private
    #           - public


Next Steps
----------

You now know how to use plugins. The next section will show you how to write plugins: :doc:`Writing Plugins <writing_plugins>`.