parent
9f14114520
commit
1f190744cd
|
|
@ -14,6 +14,7 @@ exclude_lines =
|
||||||
|
|
||||||
# Don't complain if tests don't hit defensive assertion code:
|
# Don't complain if tests don't hit defensive assertion code:
|
||||||
raise AssertionError
|
raise AssertionError
|
||||||
|
raise ImproperlyConfigured
|
||||||
raise TypeError
|
raise TypeError
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
warnings.warn
|
warnings.warn
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,9 @@ Pull requests
|
||||||
|
|
||||||
You want to contribute some code? Great! Here are a few steps to get you started:
|
You want to contribute some code? Great! Here are a few steps to get you started:
|
||||||
|
|
||||||
#. Fork the repository on GitHub
|
#. **Fork the repository on GitHub**
|
||||||
#. Clone your fork and create a branch for the code you want to add
|
#. **Clone your fork and create a branch for the code you want to add**
|
||||||
#. Create a new virtualenv and install the package in development mode
|
#. **Create a new virtualenv and install the package in development mode**
|
||||||
|
|
||||||
.. code:: console
|
.. code:: console
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
|
||||||
(venv) $ pip install -e .[validation]
|
(venv) $ pip install -e .[validation]
|
||||||
(venv) $ pip install -rrequirements/dev.txt -rrequirements/test.txt
|
(venv) $ pip install -rrequirements/dev.txt -rrequirements/test.txt
|
||||||
|
|
||||||
#. Make your changes and check them against the test project
|
#. **Make your changes and check them against the test project**
|
||||||
|
|
||||||
.. code:: console
|
.. code:: console
|
||||||
|
|
||||||
|
|
@ -46,17 +46,24 @@ You want to contribute some code? Great! Here are a few steps to get you started
|
||||||
(venv) $ python manage.py migrate
|
(venv) $ python manage.py migrate
|
||||||
(venv) $ cat createsuperuser.py | python manage.py shell
|
(venv) $ cat createsuperuser.py | python manage.py shell
|
||||||
(venv) $ python manage.py runserver
|
(venv) $ python manage.py runserver
|
||||||
(venv) $ curl localhost:8000/swagger.yaml
|
(venv) $ firefox localhost:8000/swagger/
|
||||||
|
|
||||||
#. Update the tests if necessary
|
#. **Update the tests if necessary**
|
||||||
|
|
||||||
You can find them in the ``tests`` directory.
|
You can find them in the ``tests`` directory.
|
||||||
|
|
||||||
If your change modifies the expected schema output, you should download the new generated ``swagger.yaml``, diff it
|
If your change modifies the expected schema output, you should regenerate the reference schema at
|
||||||
against the old reference output in ``tests/reference.yaml``, and replace it after checking that no unexpected
|
``tests/reference.yaml``:
|
||||||
changes appeared.
|
|
||||||
|
|
||||||
#. Run tests. The project is setup to use tox and pytest for testing
|
.. code:: console
|
||||||
|
|
||||||
|
(venv) $ cd testproj
|
||||||
|
(venv) $ python manage.py generate_swagger ../tests/reference.yaml --overwrite --user admin --url http://test.local:8002/
|
||||||
|
|
||||||
|
After checking the git diff to verify that no unexpected changes appeared, you should commit the new
|
||||||
|
``reference.yaml`` together with your changes.
|
||||||
|
|
||||||
|
#. **Run tests. The project is setup to use tox and pytest for testing**
|
||||||
|
|
||||||
.. code:: console
|
.. code:: console
|
||||||
|
|
||||||
|
|
@ -65,7 +72,7 @@ You want to contribute some code? Great! Here are a few steps to get you started
|
||||||
# (optional) run tests for other python versions in separate environments
|
# (optional) run tests for other python versions in separate environments
|
||||||
(venv) $ tox
|
(venv) $ tox
|
||||||
|
|
||||||
#. Update documentation
|
#. **Update documentation**
|
||||||
|
|
||||||
If the change modifies behaviour or adds new features, you should update the documentation and ``README.rst``
|
If the change modifies behaviour or adds new features, you should update the documentation and ``README.rst``
|
||||||
accordingly. Documentation is written in reStructuredText and built using Sphinx. You can find the sources in the
|
accordingly. Documentation is written in reStructuredText and built using Sphinx. You can find the sources in the
|
||||||
|
|
@ -77,10 +84,11 @@ You want to contribute some code? Great! Here are a few steps to get you started
|
||||||
|
|
||||||
(venv) $ tox -e docs
|
(venv) $ tox -e docs
|
||||||
|
|
||||||
#. Push your branch and submit a pull request to the master branch on GitHub
|
#. **Push your branch and submit a pull request to the master branch on GitHub**
|
||||||
|
|
||||||
Incomplete/Work In Progress pull requests are encouraged, because they allow you to get feedback and help more
|
Incomplete/Work In Progress pull requests are encouraged, because they allow you to get feedback and help more
|
||||||
easily.
|
easily.
|
||||||
|
|
||||||
#. Your code must pass all the required travis jobs before it is merged. As of now, this includes running on
|
#. **Your code must pass all the required travis jobs before it is merged**
|
||||||
Python 2.7, 3.4, 3.5 and 3.6, and building the docs succesfully.
|
|
||||||
|
As of now, this consists of running on Python 2.7, 3.4, 3.5 and 3.6, and building the docs succesfully.
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,11 @@ The possible settings and their default values are as follows:
|
||||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
# default api Info if none is otherwise given; should be an import string to an openapi.Info object
|
||||||
|
'DEFAULT_INFO': None,
|
||||||
|
# default API url if none is otherwise given
|
||||||
|
'DEFAULT_API_URL': '',
|
||||||
|
|
||||||
'USE_SESSION_AUTH': True, # add Django Login and Django Logout buttons, CSRF token to swagger UI page
|
'USE_SESSION_AUTH': True, # add Django Login and Django Logout buttons, CSRF token to swagger UI page
|
||||||
'LOGIN_URL': getattr(django.conf.settings, 'LOGIN_URL', None), # URL for the login button
|
'LOGIN_URL': getattr(django.conf.settings, 'LOGIN_URL', None), # URL for the login button
|
||||||
'LOGOUT_URL': getattr(django.conf.settings, 'LOGOUT_URL', None), # URL for the logout button
|
'LOGOUT_URL': getattr(django.conf.settings, 'LOGOUT_URL', None), # URL for the logout button
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,13 @@ Changelog
|
||||||
#########
|
#########
|
||||||
|
|
||||||
|
|
||||||
|
*********
|
||||||
|
**1.1.1**
|
||||||
|
*********
|
||||||
|
|
||||||
|
- **ADDED:** :ref:`generate_swagger management command <management-command>`
|
||||||
|
(:issue:`29`, :pr:`31`, thanks to :ghuser:`beaugunderson`)
|
||||||
|
|
||||||
*********
|
*********
|
||||||
**1.1.0**
|
**1.1.0**
|
||||||
*********
|
*********
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ The ``@swagger_auto_schema`` decorator
|
||||||
You can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decorator on view functions to override
|
You can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decorator on view functions to override
|
||||||
some properties of the generated :class:`.Operation`. For example, in a ``ViewSet``,
|
some properties of the generated :class:`.Operation`. For example, in a ``ViewSet``,
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@swagger_auto_schema(operation_description="partial_update description override", responses={404: 'slug not found'})
|
@swagger_auto_schema(operation_description="partial_update description override", responses={404: 'slug not found'})
|
||||||
def partial_update(self, request, *args, **kwargs):
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
|
@ -153,7 +153,7 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
|
||||||
* for function based ``@api_view``\ s, because the same view can handle multiple methods, and thus represent multiple
|
* for function based ``@api_view``\ s, because the same view can handle multiple methods, and thus represent multiple
|
||||||
operations, you have to add the decorator multiple times if you want to override different operations:
|
operations, you have to add the decorator multiple times if you want to override different operations:
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
test_param = openapi.Parameter('test', openapi.IN_QUERY, description="test manual param", type=openapi.TYPE_BOOLEAN)
|
test_param = openapi.Parameter('test', openapi.IN_QUERY, description="test manual param", type=openapi.TYPE_BOOLEAN)
|
||||||
user_response = openapi.Response('response description', UserSerializer)
|
user_response = openapi.Response('response description', UserSerializer)
|
||||||
|
|
@ -169,7 +169,7 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
|
||||||
* for class based ``APIView``, ``GenericAPIView`` and non-``ViewSet`` derivatives, you have to decorate the respective
|
* for class based ``APIView``, ``GenericAPIView`` and non-``ViewSet`` derivatives, you have to decorate the respective
|
||||||
method of each operation:
|
method of each operation:
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class UserList(APIView):
|
class UserList(APIView):
|
||||||
@swagger_auto_schema(responses={200: UserSerializer(many=True)})
|
@swagger_auto_schema(responses={200: UserSerializer(many=True)})
|
||||||
|
|
@ -186,7 +186,7 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
|
||||||
respond to multiple HTTP methods and thus have multiple operations that must be decorated separately:
|
respond to multiple HTTP methods and thus have multiple operations that must be decorated separately:
|
||||||
|
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class ArticleViewSet(viewsets.ModelViewSet):
|
class ArticleViewSet(viewsets.ModelViewSet):
|
||||||
# method or 'methods' can be skipped because the list_route only handles a single method (GET)
|
# method or 'methods' can be skipped because the list_route only handles a single method (GET)
|
||||||
|
|
@ -214,7 +214,7 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
|
||||||
If you want to customize the generation of a method you are not implementing yourself, you can use
|
If you want to customize the generation of a method you are not implementing yourself, you can use
|
||||||
``swagger_auto_schema`` in combination with Django's ``method_decorator``:
|
``swagger_auto_schema`` in combination with Django's ``method_decorator``:
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@method_decorator(name='list', decorator=swagger_auto_schema(
|
@method_decorator(name='list', decorator=swagger_auto_schema(
|
||||||
operation_description="description from swagger_auto_schema via method_decorator"
|
operation_description="description from swagger_auto_schema via method_decorator"
|
||||||
|
|
@ -229,7 +229,7 @@ Where you can use the :func:`@swagger_auto_schema <.swagger_auto_schema>` decora
|
||||||
You can go even further and directly decorate the result of ``as_view``, in the same manner you would
|
You can go even further and directly decorate the result of ``as_view``, in the same manner you would
|
||||||
override an ``@api_view`` as described above:
|
override an ``@api_view`` as described above:
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
decorated_login_view = \
|
decorated_login_view = \
|
||||||
swagger_auto_schema(
|
swagger_auto_schema(
|
||||||
|
|
@ -256,7 +256,7 @@ Serializer ``Meta`` nested class
|
||||||
|
|
||||||
You can define some per-serializer options by adding a ``Meta`` class to your serializer, e.g.:
|
You can define some per-serializer options by adding a ``Meta`` class to your serializer, e.g.:
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class WhateverSerializer(Serializer):
|
class WhateverSerializer(Serializer):
|
||||||
...
|
...
|
||||||
|
|
@ -288,7 +288,7 @@ class-level attribute named ``swagger_schema`` on the view class, or
|
||||||
|
|
||||||
For example, to generate all operation IDs as camel case, you could do:
|
For example, to generate all operation IDs as camel case, you could do:
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from inflection import camelize
|
from inflection import camelize
|
||||||
|
|
||||||
|
|
@ -331,7 +331,7 @@ For customizing behavior related to specific field, serializer, filter or pagina
|
||||||
A :class:`~.inspectors.FilterInspector` that adds a description to all ``DjangoFilterBackend`` parameters could be
|
A :class:`~.inspectors.FilterInspector` that adds a description to all ``DjangoFilterBackend`` parameters could be
|
||||||
implemented like so:
|
implemented like so:
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class DjangoFilterDescriptionInspector(CoreAPICompatInspector):
|
class DjangoFilterDescriptionInspector(CoreAPICompatInspector):
|
||||||
def get_filter_parameters(self, filter_backend):
|
def get_filter_parameters(self, filter_backend):
|
||||||
|
|
@ -357,7 +357,7 @@ implemented like so:
|
||||||
A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``title`` attribute from all generated
|
A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``title`` attribute from all generated
|
||||||
:class:`.Schema` objects:
|
:class:`.Schema` objects:
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class NoSchemaTitleInspector(FieldInspector):
|
class NoSchemaTitleInspector(FieldInspector):
|
||||||
def process_result(self, result, method_name, obj, **kwargs):
|
def process_result(self, result, method_name, obj, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
Serving the schema
|
Serving the schema
|
||||||
##################
|
##################
|
||||||
|
|
||||||
|
|
||||||
************************************************
|
************************************************
|
||||||
``get_schema_view`` and the ``SchemaView`` class
|
``get_schema_view`` and the ``SchemaView`` class
|
||||||
************************************************
|
************************************************
|
||||||
|
|
@ -14,7 +15,7 @@ in the README for a usage example.
|
||||||
|
|
||||||
You can also subclass :class:`.SchemaView` by extending the return value of :func:`.get_schema_view`, e.g.:
|
You can also subclass :class:`.SchemaView` by extending the return value of :func:`.get_schema_view`, e.g.:
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
SchemaView = get_schema_view(info, ...)
|
SchemaView = get_schema_view(info, ...)
|
||||||
|
|
||||||
|
|
@ -33,3 +34,27 @@ codec and the view.
|
||||||
|
|
||||||
You can use your custom renderer classes as kwargs to :meth:`.SchemaView.as_cached_view` or by subclassing
|
You can use your custom renderer classes as kwargs to :meth:`.SchemaView.as_cached_view` or by subclassing
|
||||||
:class:`.SchemaView`.
|
:class:`.SchemaView`.
|
||||||
|
|
||||||
|
.. _management-command:
|
||||||
|
|
||||||
|
******************
|
||||||
|
Management command
|
||||||
|
******************
|
||||||
|
|
||||||
|
.. versionadded:: 1.1.1
|
||||||
|
|
||||||
|
If you only need a swagger spec file in YAML or JSON format, you can use the ``generate_swagger`` management command
|
||||||
|
to get it without having to start the web server:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ python manage.py generate_swagger swagger.json
|
||||||
|
|
||||||
|
See the command help for more advanced options:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ python manage.py generate_swagger --help
|
||||||
|
usage: manage.py generate_swagger [-h] [--version] [-v {0,1,2,3}]
|
||||||
|
... more options ...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Example:
|
||||||
|
|
||||||
**settings.py**
|
**settings.py**
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
SWAGGER_SETTINGS = {
|
SWAGGER_SETTINGS = {
|
||||||
'SECURITY_DEFINITIONS': {
|
'SECURITY_DEFINITIONS': {
|
||||||
|
|
@ -91,6 +91,25 @@ Paginator inspectors given to :func:`@swagger_auto_schema <.swagger_auto_schema>
|
||||||
:class:`'drf_yasg.inspectors.CoreAPICompatInspector' <.inspectors.CoreAPICompatInspector>`, |br| \
|
:class:`'drf_yasg.inspectors.CoreAPICompatInspector' <.inspectors.CoreAPICompatInspector>`, |br| \
|
||||||
``]``
|
``]``
|
||||||
|
|
||||||
|
Swagger document attributes
|
||||||
|
===========================
|
||||||
|
|
||||||
|
DEFAULT_INFO
|
||||||
|
------------
|
||||||
|
|
||||||
|
An import string to an :class:`.openapi.Info` object. This will be used when running the ``generate_swagger``
|
||||||
|
management command, or if no ``info`` argument is passed to ``get_schema_view``.
|
||||||
|
|
||||||
|
**Default**: :python:`None`
|
||||||
|
|
||||||
|
DEFAULT_API_URL
|
||||||
|
---------------
|
||||||
|
|
||||||
|
A string representing the default API URL. This will be used to populate the ``host``, ``schemes`` and ``basePath``
|
||||||
|
attributes of the Swagger document if no API URL is otherwise provided.
|
||||||
|
|
||||||
|
**Default**: :python:`''`
|
||||||
|
|
||||||
Authorization
|
Authorization
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|
@ -124,7 +143,7 @@ See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#sec
|
||||||
|
|
||||||
**Default**:
|
**Default**:
|
||||||
|
|
||||||
.. code:: python
|
.. code-block:: python
|
||||||
|
|
||||||
'basic': {
|
'basic': {
|
||||||
'type': 'basic'
|
'type': 'basic'
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ SWAGGER_DEFAULTS = {
|
||||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'DEFAULT_INFO': None,
|
||||||
|
'DEFAULT_API_URL': '',
|
||||||
|
|
||||||
'USE_SESSION_AUTH': True,
|
'USE_SESSION_AUTH': True,
|
||||||
'SECURITY_DEFINITIONS': {
|
'SECURITY_DEFINITIONS': {
|
||||||
'basic': {
|
'basic': {
|
||||||
|
|
@ -53,6 +56,7 @@ IMPORT_STRINGS = [
|
||||||
'DEFAULT_FIELD_INSPECTORS',
|
'DEFAULT_FIELD_INSPECTORS',
|
||||||
'DEFAULT_FILTER_INSPECTORS',
|
'DEFAULT_FILTER_INSPECTORS',
|
||||||
'DEFAULT_PAGINATOR_INSPECTORS',
|
'DEFAULT_PAGINATOR_INSPECTORS',
|
||||||
|
'DEFAULT_INFO',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,12 +59,12 @@ class OpenAPISchemaGenerator(object):
|
||||||
"""
|
"""
|
||||||
endpoint_enumerator_class = EndpointEnumerator
|
endpoint_enumerator_class = EndpointEnumerator
|
||||||
|
|
||||||
def __init__(self, info, version, url=None, patterns=None, urlconf=None):
|
def __init__(self, info, version='', url=swagger_settings.DEFAULT_API_URL, patterns=None, urlconf=None):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
:param .Info info: information about the API
|
:param .Info info: information about the API
|
||||||
:param str version: API version string, takes preedence over the version in `info`
|
:param str version: API version string; can be omitted to use `info.default_version`
|
||||||
:param str url: API
|
:param str url: API url; can be empty to remove URL info from the result
|
||||||
:param patterns: if given, only these patterns will be enumerated for inclusion in the API spec
|
:param patterns: if given, only these patterns will be enumerated for inclusion in the API spec
|
||||||
:param urlconf: if patterns is not given, use this urlconf to enumerate patterns;
|
:param urlconf: if patterns is not given, use this urlconf to enumerate patterns;
|
||||||
if not given, the default urlconf is used
|
if not given, the default urlconf is used
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from ... import openapi
|
||||||
|
from ...app_settings import swagger_settings
|
||||||
|
from ...codecs import OpenAPICodecJson, OpenAPICodecYaml
|
||||||
|
from ...generators import OpenAPISchemaGenerator
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Write the Swagger schema to disk in JSON or YAML format.'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'output_file', metavar='output-file',
|
||||||
|
nargs='?',
|
||||||
|
default='-',
|
||||||
|
type=str,
|
||||||
|
help='Output path for generated swagger document, or "-" for stdout.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-o', '--overwrite',
|
||||||
|
default=False, action='store_true',
|
||||||
|
help='Overwrite the output file if it already exists. '
|
||||||
|
'Default behavior is to stop if the output file exists.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-f', '--format', dest='format',
|
||||||
|
default='', choices=('json', 'yaml'),
|
||||||
|
type=str,
|
||||||
|
help='Output format. If not given, it is guessed from the output file extension and defaults to json.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-u', '--url', dest='api_url',
|
||||||
|
default='',
|
||||||
|
type=str,
|
||||||
|
help='Base API URL - sets the host, scheme and basePath attributes of the generated document.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-m', '--mock-request', dest='mock',
|
||||||
|
default=False, action='store_true',
|
||||||
|
help='Use a mock request when generating the swagger schema. This is useful if your views or serializers'
|
||||||
|
'depend on context from a request in order to function.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--user', dest='user',
|
||||||
|
default='',
|
||||||
|
help='Username of an existing user to use for mocked authentication. This option implies --mock-request.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-p', '--private',
|
||||||
|
default=False, action="store_true",
|
||||||
|
help='Hides endpoints not accesible to the target user. If --user is not given, only shows endpoints that '
|
||||||
|
'are accesible to unauthenticated users.\n'
|
||||||
|
'This has the same effect as passing public=False to get_schema_view() or '
|
||||||
|
'OpenAPISchemaGenerator.get_schema().\n'
|
||||||
|
'This option implies --mock-request.'
|
||||||
|
)
|
||||||
|
|
||||||
|
def write_schema(self, schema, stream, format):
|
||||||
|
if format == 'json':
|
||||||
|
codec = OpenAPICodecJson(validators=[])
|
||||||
|
swagger_json = codec.encode(schema)
|
||||||
|
swagger_json = json.loads(swagger_json.decode('utf-8'), object_pairs_hook=OrderedDict)
|
||||||
|
pretty_json = json.dumps(swagger_json, indent=4, ensure_ascii=True)
|
||||||
|
stream.write(pretty_json)
|
||||||
|
elif format == 'yaml':
|
||||||
|
codec = OpenAPICodecYaml(validators=[])
|
||||||
|
swagger_yaml = codec.encode(schema).decode('utf-8')
|
||||||
|
# YAML is already pretty!
|
||||||
|
stream.write(swagger_yaml)
|
||||||
|
else: # pragma: no cover
|
||||||
|
raise ValueError("unknown format %s" % format)
|
||||||
|
|
||||||
|
def get_mock_request(self, url, format, user=None):
|
||||||
|
factory = APIRequestFactory()
|
||||||
|
|
||||||
|
request = factory.get(url + '/swagger.' + format)
|
||||||
|
if user is not None:
|
||||||
|
force_authenticate(request, user=user)
|
||||||
|
request = APIView().initialize_request(request)
|
||||||
|
return request
|
||||||
|
|
||||||
|
def handle(self, output_file, overwrite, format, api_url, mock, user, private, *args, **options):
|
||||||
|
# disable logs of WARNING and below
|
||||||
|
logging.disable(logging.WARNING)
|
||||||
|
|
||||||
|
info = getattr(swagger_settings, 'DEFAULT_INFO', None)
|
||||||
|
if not isinstance(info, openapi.Info):
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
'settings.SWAGGER_SETTINGS["DEFAULT_INFO"] should be an '
|
||||||
|
'import string pointing to an openapi.Info object'
|
||||||
|
)
|
||||||
|
|
||||||
|
if not format:
|
||||||
|
if os.path.splitext(output_file)[1] in ('.yml', '.yaml'):
|
||||||
|
format = 'yaml'
|
||||||
|
format = format or 'json'
|
||||||
|
|
||||||
|
api_url = api_url or swagger_settings.DEFAULT_API_URL
|
||||||
|
|
||||||
|
user = User.objects.get(username=user) if user else None
|
||||||
|
mock = mock or private or (user is not None)
|
||||||
|
if mock and not api_url:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
'--mock-request requires an API url; either provide '
|
||||||
|
'the --url argument or set the DEFAULT_API_URL setting'
|
||||||
|
)
|
||||||
|
|
||||||
|
request = self.get_mock_request(api_url, format, user) if mock else None
|
||||||
|
|
||||||
|
generator = OpenAPISchemaGenerator(
|
||||||
|
info=info,
|
||||||
|
url=api_url
|
||||||
|
)
|
||||||
|
schema = generator.get_schema(request=request, public=not private)
|
||||||
|
|
||||||
|
if output_file == '-':
|
||||||
|
self.write_schema(schema, self.stdout, format)
|
||||||
|
else:
|
||||||
|
flags = os.O_CREAT | os.O_WRONLY
|
||||||
|
flags = flags | (os.O_TRUNC if overwrite else os.O_EXCL)
|
||||||
|
with os.fdopen(os.open(output_file, flags), "w") as stream:
|
||||||
|
self.write_schema(schema, stream, format)
|
||||||
|
|
@ -10,6 +10,7 @@ from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from drf_yasg.app_settings import swagger_settings
|
||||||
from .generators import OpenAPISchemaGenerator
|
from .generators import OpenAPISchemaGenerator
|
||||||
from .renderers import (
|
from .renderers import (
|
||||||
SwaggerJSONRenderer, SwaggerYAMLRenderer, SwaggerUIRenderer, ReDocRenderer, OpenAPIRenderer,
|
SwaggerJSONRenderer, SwaggerYAMLRenderer, SwaggerUIRenderer, ReDocRenderer, OpenAPIRenderer,
|
||||||
|
|
@ -46,14 +47,14 @@ def deferred_never_cache(view_func):
|
||||||
return _wrapped_view_func
|
return _wrapped_view_func
|
||||||
|
|
||||||
|
|
||||||
def get_schema_view(info, url=None, patterns=None, urlconf=None, public=False, validators=None,
|
def get_schema_view(info=None, url=None, patterns=None, urlconf=None, public=False, validators=None,
|
||||||
generator_class=OpenAPISchemaGenerator,
|
generator_class=OpenAPISchemaGenerator,
|
||||||
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
|
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
|
||||||
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
|
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
|
||||||
"""
|
"""
|
||||||
Create a SchemaView class with default renderers and generators.
|
Create a SchemaView class with default renderers and generators.
|
||||||
|
|
||||||
:param .Info info: Required. Swagger API Info object
|
:param .Info info: Swagger API Info object; if omitted, defaults to `DEFAULT_INFO`
|
||||||
:param str url: API base url; if left blank will be deduced from the location the view is served at
|
:param str url: API base url; if left blank will be deduced from the location the view is served at
|
||||||
:param patterns: passed to SchemaGenerator
|
:param patterns: passed to SchemaGenerator
|
||||||
:param urlconf: passed to SchemaGenerator
|
:param urlconf: passed to SchemaGenerator
|
||||||
|
|
@ -69,6 +70,7 @@ def get_schema_view(info, url=None, patterns=None, urlconf=None, public=False, v
|
||||||
_generator_class = generator_class
|
_generator_class = generator_class
|
||||||
_auth_classes = authentication_classes
|
_auth_classes = authentication_classes
|
||||||
_perm_classes = permission_classes
|
_perm_classes = permission_classes
|
||||||
|
info = info or swagger_settings.DEFAULT_INFO
|
||||||
validators = validators or []
|
validators = validators or []
|
||||||
_spec_renderers = tuple(renderer.with_validators(validators) for renderer in SPEC_RENDERERS)
|
_spec_renderers = tuple(renderer.with_validators(validators) for renderer in SPEC_RENDERERS)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,8 @@ SWAGGER_SETTINGS = {
|
||||||
'LOGIN_URL': '/admin/login',
|
'LOGIN_URL': '/admin/login',
|
||||||
'LOGOUT_URL': '/admin/logout',
|
'LOGOUT_URL': '/admin/logout',
|
||||||
'VALIDATOR_URL': 'http://localhost:8189',
|
'VALIDATOR_URL': 'http://localhost:8189',
|
||||||
|
|
||||||
|
'DEFAULT_INFO': 'testproj.urls.swagger_info'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,16 @@ from rest_framework.decorators import api_view
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg.views import get_schema_view
|
from drf_yasg.views import get_schema_view
|
||||||
|
|
||||||
SchemaView = get_schema_view(
|
swagger_info = openapi.Info(
|
||||||
openapi.Info(
|
|
||||||
title="Snippets API",
|
title="Snippets API",
|
||||||
default_version='v1',
|
default_version='v1',
|
||||||
description="Test description",
|
description="Test description",
|
||||||
terms_of_service="https://www.google.com/policies/terms/",
|
terms_of_service="https://www.google.com/policies/terms/",
|
||||||
contact=openapi.Contact(email="contact@snippets.local"),
|
contact=openapi.Contact(email="contact@snippets.local"),
|
||||||
license=openapi.License(name="BSD License"),
|
license=openapi.License(name="BSD License"),
|
||||||
),
|
)
|
||||||
|
|
||||||
|
SchemaView = get_schema_view(
|
||||||
validators=['ssv', 'flex'],
|
validators=['ssv', 'flex'],
|
||||||
public=True,
|
public=True,
|
||||||
permission_classes=(permissions.AllowAny,),
|
permission_classes=(permissions.AllowAny,),
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from datadiff.tools import assert_equal
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from drf_yasg import openapi, codecs
|
from drf_yasg import openapi, codecs
|
||||||
from drf_yasg.codecs import yaml_sane_load
|
from drf_yasg.codecs import yaml_sane_load, yaml_sane_dump
|
||||||
from drf_yasg.generators import OpenAPISchemaGenerator
|
from drf_yasg.generators import OpenAPISchemaGenerator
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -63,6 +64,22 @@ def validate_schema(db):
|
||||||
return validate_schema
|
return validate_schema
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def compare_schemas():
|
||||||
|
def compare_schemas(schema1, schema2):
|
||||||
|
schema1 = OrderedDict(schema1)
|
||||||
|
schema2 = OrderedDict(schema2)
|
||||||
|
ignore = ['info', 'host', 'schemes', 'basePath', 'securityDefinitions']
|
||||||
|
for attr in ignore:
|
||||||
|
schema1.pop(attr, None)
|
||||||
|
schema2.pop(attr, None)
|
||||||
|
|
||||||
|
# print diff between YAML strings because it's prettier
|
||||||
|
assert_equal(yaml_sane_dump(schema1, binary=False), yaml_sane_dump(schema2, binary=False))
|
||||||
|
|
||||||
|
return compare_schemas
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def swagger_settings(settings):
|
def swagger_settings(settings):
|
||||||
swagger_settings = copy.deepcopy(settings.SWAGGER_SETTINGS)
|
swagger_settings = copy.deepcopy(settings.SWAGGER_SETTINGS)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import tempfile
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.management import call_command
|
||||||
|
from six import StringIO
|
||||||
|
|
||||||
|
from drf_yasg.codecs import yaml_sane_load
|
||||||
|
|
||||||
|
|
||||||
|
def call_generate_swagger(output_file='-', overwrite=False, format='', api_url='',
|
||||||
|
mock=False, user='', private=False, **kwargs):
|
||||||
|
out = StringIO()
|
||||||
|
call_command(
|
||||||
|
'generate_swagger', stdout=out,
|
||||||
|
output_file=output_file, overwrite=overwrite, format=format,
|
||||||
|
api_url=api_url, mock=mock, user=user, private=private,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
return out.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def test_reference_schema(db, reference_schema):
|
||||||
|
User.objects.create_superuser('admin', 'admin@admin.admin', 'blabla')
|
||||||
|
|
||||||
|
output = call_generate_swagger(format='yaml', api_url='http://test.local:8002/', user='admin')
|
||||||
|
output_schema = yaml_sane_load(output)
|
||||||
|
assert output_schema == reference_schema
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_public(db):
|
||||||
|
output = call_generate_swagger(format='yaml', api_url='http://test.local:8002/', private=True)
|
||||||
|
output_schema = yaml_sane_load(output)
|
||||||
|
assert len(output_schema['paths']) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_mock(db):
|
||||||
|
output = call_generate_swagger()
|
||||||
|
output_schema = json.loads(output, object_pairs_hook=OrderedDict)
|
||||||
|
assert len(output_schema['paths']) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def silentremove(filename):
|
||||||
|
try:
|
||||||
|
os.remove(filename)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_file_output(db):
|
||||||
|
prefix = os.path.join(tempfile.gettempdir(), tempfile.gettempprefix())
|
||||||
|
name = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8))
|
||||||
|
yaml_file = prefix + name + '.yaml'
|
||||||
|
json_file = prefix + name + '.json'
|
||||||
|
other_file = prefix + name + '.txt'
|
||||||
|
|
||||||
|
try:
|
||||||
|
# when called with output file nothing should be written to stdout
|
||||||
|
assert call_generate_swagger(output_file=yaml_file) == ''
|
||||||
|
assert call_generate_swagger(output_file=json_file) == ''
|
||||||
|
assert call_generate_swagger(output_file=other_file) == ''
|
||||||
|
|
||||||
|
with pytest.raises(OSError):
|
||||||
|
# a second call should fail because file exists
|
||||||
|
call_generate_swagger(output_file=yaml_file)
|
||||||
|
|
||||||
|
# a second call with overwrite should still succeed
|
||||||
|
assert call_generate_swagger(output_file=json_file, overwrite=True) == ''
|
||||||
|
|
||||||
|
with open(yaml_file) as f:
|
||||||
|
content = f.read()
|
||||||
|
# YAML is a superset of JSON - that means we have to check that
|
||||||
|
# the file is really YAML and not just JSON parsed by the YAML parser
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
json.loads(content)
|
||||||
|
output_yaml = yaml_sane_load(content)
|
||||||
|
with open(json_file) as f:
|
||||||
|
output_json = json.load(f, object_pairs_hook=OrderedDict)
|
||||||
|
with open(other_file) as f:
|
||||||
|
output_other = json.load(f, object_pairs_hook=OrderedDict)
|
||||||
|
|
||||||
|
assert output_yaml == output_json == output_other
|
||||||
|
finally:
|
||||||
|
silentremove(yaml_file)
|
||||||
|
silentremove(json_file)
|
||||||
|
silentremove(other_file)
|
||||||
|
|
@ -1,21 +1,8 @@
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from datadiff.tools import assert_equal
|
|
||||||
|
|
||||||
from drf_yasg.codecs import yaml_sane_dump
|
|
||||||
from drf_yasg.inspectors import FieldInspector, SerializerInspector, PaginatorInspector, FilterInspector
|
from drf_yasg.inspectors import FieldInspector, SerializerInspector, PaginatorInspector, FilterInspector
|
||||||
|
|
||||||
|
|
||||||
def test_reference_schema(swagger_dict, reference_schema):
|
def test_reference_schema(swagger_dict, reference_schema, compare_schemas):
|
||||||
swagger_dict = OrderedDict(swagger_dict)
|
compare_schemas(swagger_dict, reference_schema)
|
||||||
reference_schema = OrderedDict(reference_schema)
|
|
||||||
ignore = ['info', 'host', 'schemes', 'basePath', 'securityDefinitions']
|
|
||||||
for attr in ignore:
|
|
||||||
swagger_dict.pop(attr, None)
|
|
||||||
reference_schema.pop(attr, None)
|
|
||||||
|
|
||||||
# print diff between YAML strings because it's prettier
|
|
||||||
assert_equal(yaml_sane_dump(swagger_dict, binary=False), yaml_sane_dump(reference_schema, binary=False))
|
|
||||||
|
|
||||||
|
|
||||||
class NoOpFieldInspector(FieldInspector):
|
class NoOpFieldInspector(FieldInspector):
|
||||||
|
|
@ -34,7 +21,7 @@ class NoOpPaginatorInspector(PaginatorInspector):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_noop_inspectors(swagger_settings, swagger_dict, reference_schema):
|
def test_noop_inspectors(swagger_settings, swagger_dict, reference_schema, compare_schemas):
|
||||||
from drf_yasg import app_settings
|
from drf_yasg import app_settings
|
||||||
|
|
||||||
def set_inspectors(inspectors, setting_name):
|
def set_inspectors(inspectors, setting_name):
|
||||||
|
|
@ -43,4 +30,4 @@ def test_noop_inspectors(swagger_settings, swagger_dict, reference_schema):
|
||||||
set_inspectors([NoOpFieldInspector, NoOpSerializerInspector], 'DEFAULT_FIELD_INSPECTORS')
|
set_inspectors([NoOpFieldInspector, NoOpSerializerInspector], 'DEFAULT_FIELD_INSPECTORS')
|
||||||
set_inspectors([NoOpFilterInspector], 'DEFAULT_FILTER_INSPECTORS')
|
set_inspectors([NoOpFilterInspector], 'DEFAULT_FILTER_INSPECTORS')
|
||||||
set_inspectors([NoOpPaginatorInspector], 'DEFAULT_PAGINATOR_INSPECTORS')
|
set_inspectors([NoOpPaginatorInspector], 'DEFAULT_PAGINATOR_INSPECTORS')
|
||||||
test_reference_schema(swagger_dict, reference_schema)
|
compare_schemas(swagger_dict, reference_schema)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue