Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eba228a114 | |||
| ee4789776a | |||
| 22853b87c7 | |||
| d39764d383 | |||
| 60e266fe99 | |||
| 1f190744cd | |||
| 9f14114520 | |||
| c85acee405 |
@@ -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
|
||||||
|
|||||||
+3
-2
@@ -50,7 +50,7 @@ jobs:
|
|||||||
- stage: publish
|
- stage: publish
|
||||||
python: '3.6'
|
python: '3.6'
|
||||||
script: skip
|
script: skip
|
||||||
env:
|
env:
|
||||||
deploy: &pypi
|
deploy: &pypi
|
||||||
provider: pypi
|
provider: pypi
|
||||||
user: cvijdea
|
user: cvijdea
|
||||||
@@ -67,4 +67,5 @@ jobs:
|
|||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- publish
|
- name: publish
|
||||||
|
if: tag IS present
|
||||||
|
|||||||
+26
-15
@@ -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,26 +46,36 @@ 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
|
.. 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
|
||||||
|
|
||||||
|
# (optional) sort imports with isort and check flake8 linting
|
||||||
|
(venv) $ isort --apply
|
||||||
|
(venv) $ flake8 src/drf_yasg testproj tests setup.py
|
||||||
# run tests in the current environment, faster than tox
|
# run tests in the current environment, faster than tox
|
||||||
(venv) $ pytest --cov
|
(venv) $ pytest --cov
|
||||||
# (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 +87,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.
|
||||||
|
|||||||
+54
-49
@@ -180,62 +180,67 @@ The possible settings and their default values are as follows:
|
|||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
SWAGGER_SETTINGS = {
|
SWAGGER_SETTINGS = {
|
||||||
# default inspector classes, see advanced documentation
|
# default inspector classes, see advanced documentation
|
||||||
'DEFAULT_AUTO_SCHEMA_CLASS': 'drf_yasg.inspectors.SwaggerAutoSchema',
|
'DEFAULT_AUTO_SCHEMA_CLASS': 'drf_yasg.inspectors.SwaggerAutoSchema',
|
||||||
'DEFAULT_FIELD_INSPECTORS': [
|
'DEFAULT_FIELD_INSPECTORS': [
|
||||||
'drf_yasg.inspectors.CamelCaseJSONFilter',
|
'drf_yasg.inspectors.CamelCaseJSONFilter',
|
||||||
'drf_yasg.inspectors.ReferencingSerializerInspector',
|
'drf_yasg.inspectors.ReferencingSerializerInspector',
|
||||||
'drf_yasg.inspectors.RelatedFieldInspector',
|
'drf_yasg.inspectors.RelatedFieldInspector',
|
||||||
'drf_yasg.inspectors.ChoiceFieldInspector',
|
'drf_yasg.inspectors.ChoiceFieldInspector',
|
||||||
'drf_yasg.inspectors.FileFieldInspector',
|
'drf_yasg.inspectors.FileFieldInspector',
|
||||||
'drf_yasg.inspectors.DictFieldInspector',
|
'drf_yasg.inspectors.DictFieldInspector',
|
||||||
'drf_yasg.inspectors.SimpleFieldInspector',
|
'drf_yasg.inspectors.SimpleFieldInspector',
|
||||||
'drf_yasg.inspectors.StringDefaultFieldInspector',
|
'drf_yasg.inspectors.StringDefaultFieldInspector',
|
||||||
],
|
],
|
||||||
'DEFAULT_FILTER_INSPECTORS': [
|
'DEFAULT_FILTER_INSPECTORS': [
|
||||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||||
],
|
],
|
||||||
'DEFAULT_PAGINATOR_INSPECTORS': [
|
'DEFAULT_PAGINATOR_INSPECTORS': [
|
||||||
'drf_yasg.inspectors.DjangoRestResponsePagination',
|
'drf_yasg.inspectors.DjangoRestResponsePagination',
|
||||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||||
],
|
],
|
||||||
|
|
||||||
'USE_SESSION_AUTH': True, # add Django Login and Django Logout buttons, CSRF token to swagger UI page
|
# default api Info if none is otherwise given; should be an import string to an openapi.Info object
|
||||||
'LOGIN_URL': getattr(django.conf.settings, 'LOGIN_URL', None), # URL for the login button
|
'DEFAULT_INFO': None,
|
||||||
'LOGOUT_URL': getattr(django.conf.settings, 'LOGOUT_URL', None), # URL for the logout button
|
# default API url if none is otherwise given
|
||||||
|
'DEFAULT_API_URL': '',
|
||||||
|
|
||||||
# Swagger security definitions to include in the schema;
|
'USE_SESSION_AUTH': True, # add Django Login and Django Logout buttons, CSRF token to swagger UI page
|
||||||
# see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-definitions-object
|
'LOGIN_URL': getattr(django.conf.settings, 'LOGIN_URL', None), # URL for the login button
|
||||||
'SECURITY_DEFINITIONS': {
|
'LOGOUT_URL': getattr(django.conf.settings, 'LOGOUT_URL', None), # URL for the logout button
|
||||||
'basic': {
|
|
||||||
'type': 'basic'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
# url to an external Swagger validation service; defaults to 'http://online.swagger.io/validator/'
|
# Swagger security definitions to include in the schema;
|
||||||
# set to None to disable the schema validation badge in the UI
|
# see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-definitions-object
|
||||||
'VALIDATOR_URL': '',
|
'SECURITY_DEFINITIONS': {
|
||||||
|
'basic': {
|
||||||
|
'type': 'basic'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
# swagger-ui configuration settings, see https://github.com/swagger-api/swagger-ui/blob/112bca906553a937ac67adc2e500bdeed96d067b/docs/usage/configuration.md#parameters
|
# url to an external Swagger validation service; defaults to 'http://online.swagger.io/validator/'
|
||||||
'OPERATIONS_SORTER': None,
|
# set to None to disable the schema validation badge in the UI
|
||||||
'TAGS_SORTER': None,
|
'VALIDATOR_URL': '',
|
||||||
'DOC_EXPANSION': 'list',
|
|
||||||
'DEEP_LINKING': False,
|
# swagger-ui configuration settings, see https://github.com/swagger-api/swagger-ui/blob/112bca906553a937ac67adc2e500bdeed96d067b/docs/usage/configuration.md#parameters
|
||||||
'SHOW_EXTENSIONS': True,
|
'OPERATIONS_SORTER': None,
|
||||||
'DEFAULT_MODEL_RENDERING': 'model',
|
'TAGS_SORTER': None,
|
||||||
'DEFAULT_MODEL_DEPTH': 3,
|
'DOC_EXPANSION': 'list',
|
||||||
}
|
'DEEP_LINKING': False,
|
||||||
|
'SHOW_EXTENSIONS': True,
|
||||||
|
'DEFAULT_MODEL_RENDERING': 'model',
|
||||||
|
'DEFAULT_MODEL_DEPTH': 3,
|
||||||
|
}
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
REDOC_SETTINGS = {
|
REDOC_SETTINGS = {
|
||||||
# ReDoc UI configuration settings, see https://github.com/Rebilly/ReDoc#redoc-tag-attributes
|
# ReDoc UI configuration settings, see https://github.com/Rebilly/ReDoc#redoc-tag-attributes
|
||||||
'LAZY_RENDERING': True,
|
'LAZY_RENDERING': True,
|
||||||
'HIDE_HOSTNAME': False,
|
'HIDE_HOSTNAME': False,
|
||||||
'EXPAND_RESPONSES': 'all',
|
'EXPAND_RESPONSES': 'all',
|
||||||
'PATH_IN_MIDDLE': False,
|
'PATH_IN_MIDDLE': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
3. Caching
|
3. Caching
|
||||||
==========
|
==========
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ Changelog
|
|||||||
#########
|
#########
|
||||||
|
|
||||||
|
|
||||||
|
*********
|
||||||
|
**1.1.1**
|
||||||
|
*********
|
||||||
|
|
||||||
|
- **ADDED:** :ref:`generate_swagger management command <management-command>`
|
||||||
|
(:issue:`29`, :pr:`31`, thanks to :ghuser:`beaugunderson`)
|
||||||
|
- **FIXED:** fixed improper generation of ``\Z`` regex tokens - will now be repalced by ``$``
|
||||||
|
|
||||||
*********
|
*********
|
||||||
**1.1.0**
|
**1.1.0**
|
||||||
*********
|
*********
|
||||||
|
|||||||
+10
-10
@@ -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):
|
||||||
|
|||||||
+26
-1
@@ -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 ...
|
||||||
|
|
||||||
|
|||||||
+21
-2
@@ -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'
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
# requirements for local development
|
# requirements for local development
|
||||||
tox>=2.9.1
|
tox>=2.9.1
|
||||||
tox-battery>=0.5
|
tox-battery>=0.5
|
||||||
|
detox>=0.11
|
||||||
|
|
||||||
|
isort>=4.2
|
||||||
|
flake8>=3.5.0
|
||||||
|
flake8-isort>=2.3
|
||||||
|
|
||||||
# do not unpin this (see setup.py)
|
# do not unpin this (see setup.py)
|
||||||
setuptools_scm==1.15.6
|
setuptools_scm==1.15.6
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ Pillow==4.3.0
|
|||||||
readme_renderer==17.2
|
readme_renderer==17.2
|
||||||
|
|
||||||
Django==2.0
|
Django==2.0
|
||||||
|
djangorestframework_camel_case>=0.2.0
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import distutils.core
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from setuptools import setup, find_packages
|
from setuptools import find_packages, setup
|
||||||
import distutils.core
|
|
||||||
|
|
||||||
requirements_setup = ['setuptools_scm==1.15.6']
|
requirements_setup = ['setuptools_scm==1.15.6']
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# coding=utf-8
|
# coding=utf-8
|
||||||
from pkg_resources import get_distribution, DistributionNotFound
|
from pkg_resources import DistributionNotFound, get_distribution
|
||||||
|
|
||||||
__author__ = """Cristi V."""
|
__author__ = """Cristi V."""
|
||||||
__email__ = 'cristi@cvjd.me'
|
__email__ = 'cristi@cvjd.me'
|
||||||
|
|||||||
@@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
from future.utils import raise_from
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from coreapi.compat import force_bytes
|
from coreapi.compat import force_bytes
|
||||||
from future.utils import raise_from
|
|
||||||
from ruamel import yaml
|
from ruamel import yaml
|
||||||
|
|
||||||
from . import openapi
|
from . import openapi
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import re
|
import re
|
||||||
from collections import defaultdict, OrderedDict
|
from collections import OrderedDict, defaultdict
|
||||||
|
|
||||||
import uritemplate
|
import uritemplate
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from rest_framework import versioning
|
from rest_framework import versioning
|
||||||
from rest_framework.schemas.generators import SchemaGenerator, EndpointEnumerator as _EndpointEnumerator
|
from rest_framework.schemas.generators import EndpointEnumerator as _EndpointEnumerator
|
||||||
|
from rest_framework.schemas.generators import SchemaGenerator
|
||||||
from rest_framework.schemas.inspectors import get_pk_description
|
from rest_framework.schemas.inspectors import get_pk_description
|
||||||
|
|
||||||
from . import openapi
|
from . import openapi
|
||||||
from .app_settings import swagger_settings
|
from .app_settings import swagger_settings
|
||||||
from .inspectors.field import get_queryset_field, get_basic_type_info
|
from .inspectors.field import get_basic_type_info, get_queryset_field
|
||||||
from .openapi import ReferenceResolver
|
from .openapi import ReferenceResolver
|
||||||
|
|
||||||
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
|
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
|
||||||
@@ -59,12 +60,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
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
|
from ..app_settings import swagger_settings
|
||||||
from .base import (
|
from .base import (
|
||||||
BaseInspector, ViewInspector, FilterInspector, PaginatorInspector,
|
BaseInspector, FieldInspector, FilterInspector, NotHandled, PaginatorInspector, SerializerInspector, ViewInspector
|
||||||
FieldInspector, SerializerInspector, NotHandled
|
|
||||||
)
|
)
|
||||||
from .field import (
|
from .field import (
|
||||||
InlineSerializerInspector, ReferencingSerializerInspector, RelatedFieldInspector, SimpleFieldInspector,
|
CamelCaseJSONFilter, ChoiceFieldInspector, DictFieldInspector, FileFieldInspector, InlineSerializerInspector,
|
||||||
FileFieldInspector, ChoiceFieldInspector, DictFieldInspector, StringDefaultFieldInspector,
|
ReferencingSerializerInspector, RelatedFieldInspector, SimpleFieldInspector, StringDefaultFieldInspector
|
||||||
CamelCaseJSONFilter
|
|
||||||
)
|
|
||||||
from .query import (
|
|
||||||
CoreAPICompatInspector, DjangoRestResponsePagination
|
|
||||||
)
|
)
|
||||||
|
from .query import CoreAPICompatInspector, DjangoRestResponsePagination
|
||||||
from .view import SwaggerAutoSchema
|
from .view import SwaggerAutoSchema
|
||||||
from ..app_settings import swagger_settings
|
|
||||||
|
|
||||||
# these settings must be accesed only after definig/importing all the classes in this module to avoid ImportErrors
|
# these settings must be accesed only after definig/importing all the classes in this module to avoid ImportErrors
|
||||||
ViewInspector.field_inspectors = swagger_settings.DEFAULT_FIELD_INSPECTORS
|
ViewInspector.field_inspectors = swagger_settings.DEFAULT_FIELD_INSPECTORS
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import logging
|
|||||||
|
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.utils import json, encoders
|
from rest_framework.utils import encoders, json
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from .. import openapi
|
from .. import openapi
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ from django.db import models
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.settings import api_settings as rest_framework_settings
|
from rest_framework.settings import api_settings as rest_framework_settings
|
||||||
|
|
||||||
from .base import NotHandled, SerializerInspector, FieldInspector
|
|
||||||
from .. import openapi
|
from .. import openapi
|
||||||
from ..errors import SwaggerGenerationError
|
from ..errors import SwaggerGenerationError
|
||||||
from ..utils import filter_none
|
from ..utils import filter_none
|
||||||
|
from .base import FieldInspector, NotHandled, SerializerInspector
|
||||||
|
|
||||||
|
|
||||||
class InlineSerializerInspector(SerializerInspector):
|
class InlineSerializerInspector(SerializerInspector):
|
||||||
@@ -211,7 +211,14 @@ def find_regex(regex_field):
|
|||||||
regex_validator = validator
|
regex_validator = validator
|
||||||
|
|
||||||
# regex_validator.regex should be a compiled re object...
|
# regex_validator.regex should be a compiled re object...
|
||||||
return getattr(getattr(regex_validator, 'regex', None), 'pattern', None)
|
pattern = getattr(getattr(regex_validator, 'regex', None), 'pattern', None)
|
||||||
|
if pattern:
|
||||||
|
# attempt some basic cleanup to remove regex constructs not supported by JavaScript
|
||||||
|
# -- swagger uses javascript-style regexes - see https://github.com/swagger-api/swagger-editor/issues/1601
|
||||||
|
if pattern.endswith('\\Z') or pattern.endswith('\\z'):
|
||||||
|
pattern = pattern[:-2] + '$'
|
||||||
|
|
||||||
|
return pattern
|
||||||
|
|
||||||
|
|
||||||
numeric_fields = (serializers.IntegerField, serializers.FloatField, serializers.DecimalField)
|
numeric_fields = (serializers.IntegerField, serializers.FloatField, serializers.DecimalField)
|
||||||
@@ -423,6 +430,7 @@ try:
|
|||||||
from djangorestframework_camel_case.render import camelize
|
from djangorestframework_camel_case.render import camelize
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
class CamelCaseJSONFilter(FieldInspector):
|
class CamelCaseJSONFilter(FieldInspector):
|
||||||
|
"""Converts property names to camelCase if ``djangorestframework_camel_case`` is used."""
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
def camelize_string(s):
|
def camelize_string(s):
|
||||||
@@ -444,6 +452,8 @@ else:
|
|||||||
return schema_or_ref
|
return schema_or_ref
|
||||||
|
|
||||||
class CamelCaseJSONFilter(FieldInspector):
|
class CamelCaseJSONFilter(FieldInspector):
|
||||||
|
"""Converts property names to camelCase if ``CamelCaseJSONParser`` or ``CamelCaseJSONRenderer`` are used."""
|
||||||
|
|
||||||
def is_camel_case(self):
|
def is_camel_case(self):
|
||||||
return any(issubclass(parser, CamelCaseJSONParser) for parser in self.view.parser_classes) \
|
return any(issubclass(parser, CamelCaseJSONParser) for parser in self.view.parser_classes) \
|
||||||
or any(issubclass(renderer, CamelCaseJSONRenderer) for renderer in self.view.renderer_classes)
|
or any(issubclass(renderer, CamelCaseJSONRenderer) for renderer in self.view.renderer_classes)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import coreschema
|
import coreschema
|
||||||
from rest_framework.pagination import CursorPagination, PageNumberPagination, LimitOffsetPagination
|
from rest_framework.pagination import CursorPagination, LimitOffsetPagination, PageNumberPagination
|
||||||
|
|
||||||
from .base import PaginatorInspector, FilterInspector
|
|
||||||
from .. import openapi
|
from .. import openapi
|
||||||
|
from .base import FilterInspector, PaginatorInspector
|
||||||
|
|
||||||
|
|
||||||
class CoreAPICompatInspector(PaginatorInspector, FilterInspector):
|
class CoreAPICompatInspector(PaginatorInspector, FilterInspector):
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ from rest_framework.request import is_form_media_type
|
|||||||
from rest_framework.schemas import AutoSchema
|
from rest_framework.schemas import AutoSchema
|
||||||
from rest_framework.status import is_success
|
from rest_framework.status import is_success
|
||||||
|
|
||||||
from .base import ViewInspector
|
|
||||||
from .. import openapi
|
from .. import openapi
|
||||||
from ..errors import SwaggerGenerationError
|
from ..errors import SwaggerGenerationError
|
||||||
from ..utils import force_serializer_instance, no_body, is_list_view, param_list_to_odict, guess_response_status
|
from ..utils import force_serializer_instance, guess_response_status, is_list_view, no_body, param_list_to_odict
|
||||||
|
from .base import ViewInspector
|
||||||
|
|
||||||
|
|
||||||
class SwaggerAutoSchema(ViewInspector):
|
class SwaggerAutoSchema(ViewInspector):
|
||||||
@@ -162,9 +162,7 @@ class SwaggerAutoSchema(ViewInspector):
|
|||||||
|
|
||||||
default_status = guess_response_status(method)
|
default_status = guess_response_status(method)
|
||||||
default_schema = ''
|
default_schema = ''
|
||||||
if method == 'post':
|
if method in ('get', 'post', 'put', 'patch'):
|
||||||
default_schema = self.get_request_serializer() or self.get_view_serializer()
|
|
||||||
elif method in ('get', 'put', 'patch'):
|
|
||||||
default_schema = self.get_request_serializer() or self.get_view_serializer()
|
default_schema = self.get_request_serializer() or self.get_view_serializer()
|
||||||
|
|
||||||
default_schema = default_schema or ''
|
default_schema = default_schema or ''
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
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:
|
||||||
|
# normally this would be easily done with open(mode='x'/'w'),
|
||||||
|
# but python 2 is a pain in the ass as usual
|
||||||
|
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)
|
||||||
@@ -2,8 +2,8 @@ from django.shortcuts import render, resolve_url
|
|||||||
from rest_framework.renderers import BaseRenderer
|
from rest_framework.renderers import BaseRenderer
|
||||||
from rest_framework.utils import json
|
from rest_framework.utils import json
|
||||||
|
|
||||||
from .app_settings import swagger_settings, redoc_settings
|
from .app_settings import redoc_settings, swagger_settings
|
||||||
from .codecs import OpenAPICodecJson, VALIDATORS, OpenAPICodecYaml
|
from .codecs import VALIDATORS, OpenAPICodecJson, OpenAPICodecYaml
|
||||||
|
|
||||||
|
|
||||||
class _SpecRenderer(BaseRenderer):
|
class _SpecRenderer(BaseRenderer):
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import inspect
|
|||||||
import logging
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from rest_framework import status, serializers
|
from rest_framework import serializers, status
|
||||||
from rest_framework.mixins import RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin
|
from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, UpdateModelMixin
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ 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 .app_settings import swagger_settings
|
||||||
from .generators import OpenAPISchemaGenerator
|
from .generators import OpenAPISchemaGenerator
|
||||||
from .renderers import (
|
from .renderers import OpenAPIRenderer, ReDocRenderer, SwaggerJSONRenderer, SwaggerUIRenderer, SwaggerYAMLRenderer
|
||||||
SwaggerJSONRenderer, SwaggerYAMLRenderer, SwaggerUIRenderer, ReDocRenderer, OpenAPIRenderer,
|
|
||||||
)
|
|
||||||
|
|
||||||
SPEC_RENDERERS = (SwaggerYAMLRenderer, SwaggerJSONRenderer, OpenAPIRenderer)
|
SPEC_RENDERERS = (SwaggerYAMLRenderer, SwaggerJSONRenderer, OpenAPIRenderer)
|
||||||
UI_RENDERERS = {
|
UI_RENDERERS = {
|
||||||
@@ -46,14 +45,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 +68,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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from articles.models import Article
|
from articles.models import Article
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class ArticleSerializer(serializers.ModelSerializer):
|
class ArticleSerializer(serializers.ModelSerializer):
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from articles import serializers
|
|||||||
from articles.models import Article
|
from articles.models import Article
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg.app_settings import swagger_settings
|
from drf_yasg.app_settings import swagger_settings
|
||||||
from drf_yasg.inspectors import SwaggerAutoSchema, FieldInspector, CoreAPICompatInspector, NotHandled
|
from drf_yasg.inspectors import CoreAPICompatInspector, FieldInspector, NotHandled, SwaggerAutoSchema
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES
|
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet
|
||||||
|
|
||||||
|
|
||||||
class LanguageSerializer(serializers.Serializer):
|
class LanguageSerializer(serializers.Serializer):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from django.conf.urls import url, include
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
from rest_framework.decorators import api_view
|
from rest_framework.decorators import api_view
|
||||||
@@ -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
|
||||||
|
|
||||||
|
swagger_info = openapi.Info(
|
||||||
|
title="Snippets API",
|
||||||
|
default_version='v1',
|
||||||
|
description="Test description",
|
||||||
|
terms_of_service="https://www.google.com/policies/terms/",
|
||||||
|
contact=openapi.Contact(email="contact@snippets.local"),
|
||||||
|
license=openapi.License(name="BSD License"),
|
||||||
|
)
|
||||||
|
|
||||||
SchemaView = get_schema_view(
|
SchemaView = get_schema_view(
|
||||||
openapi.Info(
|
|
||||||
title="Snippets API",
|
|
||||||
default_version='v1',
|
|
||||||
description="Test description",
|
|
||||||
terms_of_service="https://www.google.com/policies/terms/",
|
|
||||||
contact=openapi.Contact(email="contact@snippets.local"),
|
|
||||||
license=openapi.License(name="BSD License"),
|
|
||||||
),
|
|
||||||
validators=['ssv', 'flex'],
|
validators=['ssv', 'flex'],
|
||||||
public=True,
|
public=True,
|
||||||
permission_classes=(permissions.AllowAny,),
|
permission_classes=(permissions.AllowAny,),
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg.utils import swagger_auto_schema, no_body
|
from drf_yasg.utils import no_body, swagger_auto_schema
|
||||||
from users.serializers import UserSerializerrr, UserListQuerySerializer
|
from users.serializers import UserListQuerySerializer, UserSerializerrr
|
||||||
|
|
||||||
|
|
||||||
class UserList(APIView):
|
class UserList(APIView):
|
||||||
|
|||||||
+21
-4
@@ -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 codecs, openapi
|
||||||
from drf_yasg.codecs import yaml_sane_load
|
from drf_yasg.codecs import yaml_sane_dump, yaml_sane_load
|
||||||
from drf_yasg.generators import OpenAPISchemaGenerator
|
from drf_yasg.generators import OpenAPISchemaGenerator
|
||||||
|
|
||||||
|
|
||||||
@@ -46,8 +47,8 @@ def swagger(mock_schema_request):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def swagger_dict(swagger):
|
def swagger_dict(swagger, codec_json):
|
||||||
json_bytes = codec_json().encode(swagger)
|
json_bytes = codec_json.encode(swagger)
|
||||||
return json.loads(json_bytes.decode('utf-8'), object_pairs_hook=OrderedDict)
|
return json.loads(json_bytes.decode('utf-8'), object_pairs_hook=OrderedDict)
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -1108,7 +1108,7 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
format: slug
|
format: slug
|
||||||
pattern: ^[-a-zA-Z0-9_]+\Z
|
pattern: ^[-a-zA-Z0-9_]+$
|
||||||
readOnly: true
|
readOnly: true
|
||||||
readOnly: true
|
readOnly: true
|
||||||
uniqueItems: true
|
uniqueItems: true
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
from six import StringIO
|
||||||
|
|
||||||
|
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 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,13 @@
|
|||||||
|
import json
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from datadiff.tools import assert_equal
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.generators import OpenAPISchemaGenerator
|
||||||
from drf_yasg.codecs import yaml_sane_dump
|
from drf_yasg.inspectors import FieldInspector, FilterInspector, PaginatorInspector, SerializerInspector
|
||||||
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,13 +26,23 @@ class NoOpPaginatorInspector(PaginatorInspector):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_noop_inspectors(swagger_settings, swagger_dict, reference_schema):
|
def test_noop_inspectors(swagger_settings, mock_schema_request, codec_json, 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):
|
||||||
|
inspectors = [__name__ + '.' + inspector.__name__ for inspector in inspectors]
|
||||||
swagger_settings[setting_name] = inspectors + app_settings.SWAGGER_DEFAULTS[setting_name]
|
swagger_settings[setting_name] = inspectors + app_settings.SWAGGER_DEFAULTS[setting_name]
|
||||||
|
|
||||||
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)
|
|
||||||
|
generator = OpenAPISchemaGenerator(
|
||||||
|
info=openapi.Info(title="Test generator", default_version="v1"),
|
||||||
|
version="v2",
|
||||||
|
)
|
||||||
|
swagger = generator.get_schema(mock_schema_request, True)
|
||||||
|
|
||||||
|
json_bytes = codec_json.encode(swagger)
|
||||||
|
swagger_dict = json.loads(json_bytes.decode('utf-8'), object_pairs_hook=OrderedDict)
|
||||||
|
compare_schemas(swagger_dict, reference_schema)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from collections import OrderedDict
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from drf_yasg import openapi, codecs
|
from drf_yasg import codecs, openapi
|
||||||
from drf_yasg.codecs import yaml_sane_load
|
from drf_yasg.codecs import yaml_sane_load
|
||||||
from drf_yasg.generators import OpenAPISchemaGenerator
|
from drf_yasg.generators import OpenAPISchemaGenerator
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import include, url
|
||||||
from django.conf.urls import include
|
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
|
||||||
import testproj.urls
|
import testproj.urls
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from django.conf.urls import url
|
|||||||
from rest_framework import fields
|
from rest_framework import fields
|
||||||
|
|
||||||
from snippets.serializers import SnippetSerializer
|
from snippets.serializers import SnippetSerializer
|
||||||
|
|
||||||
from .ns_version1 import SnippetList as SnippetListV1
|
from .ns_version1 import SnippetList as SnippetListV1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from django.conf.urls import url, include
|
from django.conf.urls import include, url
|
||||||
from rest_framework import versioning
|
from rest_framework import versioning
|
||||||
|
|
||||||
from testproj.urls import SchemaView
|
from testproj.urls import SchemaView
|
||||||
|
|
||||||
from . import ns_version1, ns_version2
|
from . import ns_version1, ns_version2
|
||||||
|
|
||||||
VERSION_PREFIX_NS = r"^versioned/ns/"
|
VERSION_PREFIX_NS = r"^versioned/ns/"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from rest_framework import generics, versioning, fields
|
from rest_framework import fields, generics, versioning
|
||||||
|
|
||||||
from snippets.models import Snippet
|
from snippets.models import Snippet
|
||||||
from snippets.serializers import SnippetSerializer
|
from snippets.serializers import SnippetSerializer
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pip_pre = True
|
|||||||
skip_install = true
|
skip_install = true
|
||||||
deps =
|
deps =
|
||||||
flake8
|
flake8
|
||||||
|
flake8-isort
|
||||||
commands =
|
commands =
|
||||||
flake8 src/drf_yasg testproj tests setup.py
|
flake8 src/drf_yasg testproj tests setup.py
|
||||||
|
|
||||||
@@ -56,3 +57,19 @@ python_paths = testproj
|
|||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
exclude = **/migrations/*
|
exclude = **/migrations/*
|
||||||
|
|
||||||
|
[isort]
|
||||||
|
skip = .eggs,.tox,docs
|
||||||
|
skip_glob = **/migrations/*
|
||||||
|
not_skip = __init__.py
|
||||||
|
atomic = true
|
||||||
|
multi_line_output = 5
|
||||||
|
line_length = 120
|
||||||
|
known_future_library = future,six
|
||||||
|
known_standard_library =
|
||||||
|
collections,copy,distutils,functools,inspect,io,json,logging,operator,os,pkg_resources,re,setuptools,sys,
|
||||||
|
types,warnings
|
||||||
|
known_third_party =
|
||||||
|
coreapi,coreschema,datadiff,django,django_filters,djangorestframework_camel_case,flex,inflection,pygments,
|
||||||
|
pytest,rest_framework,ruamel,setuptools_scm,swagger_spec_validator,uritemplate
|
||||||
|
known_first_party = drf_yasg,testproj,articles,snippets,users,urlconfs
|
||||||
|
|||||||
Reference in New Issue
Block a user