Compare commits

..

85 Commits

Author SHA1 Message Date
Cristi Vîjdea 6f7d14fdb2 Add 1.6.1 changelog 2018-04-01 12:08:14 +03:00
shahha 941ec8fdaf Added SUPPORTED_SUBMIT_METHODS swagger ui setting (#91) 2018-03-26 11:15:28 +03:00
Cristi Vîjdea 51ec07261d Improve OAuth2 behaviour and documentation (#90)
* Set OAUTH2_REDIRECT_URL to oauth2-redirect.html by default
* Add example SWAGGER_SETTINGS for OAuth
* Add note about redirect URL
2018-03-24 13:05:37 +02:00
Cristi Vîjdea a9cdf6d561 Fix type of properties in Schema docstring
Fixes #87.
2018-03-21 13:39:14 +02:00
Cristi Vîjdea a64ce95e3c Reset testproj migrations
Heroku deployment issues...
2018-03-18 20:35:02 +02:00
Cristi Vîjdea c9147a5746 Fix testproj people app 2018-03-18 20:20:04 +02:00
Cristi Vîjdea ccdf646c0e Run isort 2018-03-18 20:01:21 +02:00
Cristi Vîjdea 04252ebec3 Remove reodc-alpha artifacts 2018-03-18 19:52:16 +02:00
Cristi Vîjdea 848fb69009 Update swagger-ui to 3.13.0 2018-03-18 19:32:21 +02:00
ko-pp 309a6eb8cd Fix schema generation with OneToOneFields (#81)
* Fix: OneToOneRel, used by OneToOneField doesn't have help_text nor primary_key attributes, thus breaking OpenAPISchemaGenerator; use hasattr() as safe-guard.
* Fix: use getattr() with a default value instead of hasattr() + acessing the value
* Add: 'people' app that breaks drf_yasg without previous commits
* Update tests/references.yaml + run isort and flake8
* Fix: set on_delete for Person.identity as Django-2+ requires it
2018-03-18 19:30:36 +02:00
Cristi Vîjdea a7fbba4967 Update changelog for 1.5.0 2018-03-12 19:26:12 +02:00
Cristi Vîjdea 66785b1ee8 Update swagger-ui to 3.12.1 2018-03-12 19:06:20 +02:00
John Carter fb240f6a5b Detect type of SlugRelatedField with readonly=True (#83)
* Added test for #82 - readonly SlugRelatedField not showing format uuid
* Added ArticleGroup model
* Added example of SlugRelatedField to a uuid field with and without readonly
* Check remote_field.model in get_related_model

Closes #82.
2018-03-10 13:29:20 +02:00
John Carter 2c459024d1 Handle django rest framework HiddenField (#79)
* Added HiddenFieldInspector (#78)
* Updated docs + added test for HiddenField

Added HiddenField to testproj (no change in yaml as expected)

Closes #78.
2018-03-09 22:46:47 +02:00
ko-pp b62161f762 Escape '.' wildcard in testproj urlregex (#80)
Fix: Django's URL patterns being regexes, '.' has to be escaped to be used as a literal value and not as a joker.
2018-03-09 18:28:12 +02:00
Cristi Vîjdea 9ad55bac99 Guard against attempted deletion of inexistent attributes
Fixes #76
2018-03-05 19:59:03 +02:00
Cristi Vîjdea 3d3b7899e5 Update swagger-ui to 3.12.0 and ReDoc to 1.21.2 2018-03-05 17:07:45 +02:00
Cristi Vîjdea 6ea8711a1f Fix in-place modification of swagger_auto_schema arguments (#75)
Fixes #74
2018-03-05 11:51:51 +02:00
Cristi Vîjdea ee46f59fb1 Infer ChoiceField type from model field when in ModelSerializer
Fixes part of issue #69
2018-02-26 20:32:57 +02:00
Santiago Castro 3f7ad62950 Make 1.10.0 the min six version (#71) 2018-02-26 17:18:52 +02:00
Cristi Vîjdea b38d3e6805 Do not set pattern on non-string values
Fixes #68
2018-02-23 18:51:55 +02:00
Cristi Vîjdea 058fd7096d Change README link from pypi.python.org to pypi.org
Description rendering is prettier on the new version.
2018-02-22 20:46:26 +02:00
Cristi Vîjdea a32321d43a Add release dates to changelog 2018-02-22 20:46:22 +02:00
Cristi Vîjdea f15e70b7db Fix missing assignment for default in openapi.Parameter 2018-02-22 20:33:47 +02:00
Cristi Vîjdea 64c280e222 Allow Response objects with no schema
Fixes #66.
2018-02-22 03:46:44 +02:00
Cristi Vîjdea d5073081d9 Fix ModelViewSet Nested bug (#65)
* Add todo repro app
* Add explicit test against model named "Nested"
* Force serializers named NestedSerializer to be output as inline models
* Allow ref_name to rescue a NestedSerializer
* Add tests and documentation
2018-02-22 03:46:16 +02:00
Cristi Vîjdea 10c7e22940 Merge pull request #64 from axnsan12/release/1.4.1
Release version 1.4.1
2018-02-21 05:17:57 +02:00
Cristi Vîjdea 6c497b32b4 Use TemplateHTMLRenderer for error responses in UIRenderer
Closes #58.
2018-02-21 04:59:03 +02:00
Cristi Vîjdea 73c60a8fda Add assertion against mistaken swagger_auto_schema methods usage 2018-02-21 04:42:56 +02:00
Cristi Vîjdea 97082e8898 Update swagger-ui to 3.10.0 and ReDoc to 1.21.0 2018-02-21 04:42:54 +02:00
Cristi Vîjdea 743396617e Upgrade sphinx to 1.7.0 2018-02-21 04:42:53 +02:00
Cristi Vîjdea 97cd1b63d9 Take coerce_to_string into account when handling DecimalField
Closes #62.
2018-02-21 04:42:34 +02:00
Cristi Vîjdea 71dee6eb45 Add OAuth2 client configuration for swagger-ui (#57) 2018-02-04 14:36:54 +02:00
Cristi Vîjdea 7fa0cc0639 Make DRF master test run with stable Django 2018-02-04 14:36:16 +02:00
Cristi Vîjdea e8c688f981 Update swagger-ui to 3.9.3 2018-02-04 14:23:00 +02:00
Cristi Vîjdea c01a6ead26 Bring project into 2018 2018-01-29 00:18:53 +02:00
Cristi Vîjdea 05901e99aa Add 1.3.1 changelog 2018-01-24 17:43:38 +02:00
Cristi Vîjdea a211184478 Add documentation about excluding endpoints
Closes #50.
2018-01-24 17:38:10 +02:00
Cristi Vîjdea a3e81ef7f6 Improve handling of consumes and produces attributes (#55)
* Fix get_consumes
* Generate produces for Operation
* Set global consumes and produces from rest framework DEFAULT_ settings
2018-01-24 14:44:00 +02:00
Cristi Vîjdea a46b684fea Add 1.3.0 changelog 2018-01-23 12:44:16 +02:00
Cristi Vîjdea fc35d9043e Add security requirements handling (#54)
* Add security requirements handling
* Update swagger-ui to 3.9.2, ReDoc to 1.20.0

Closes #39.
2018-01-23 12:43:25 +02:00
Cristi Vîjdea f18ff60ae7 Update swagger-ui to 3.9.1 2018-01-19 18:37:44 +01:00
Daniel Hahler a82730d32b Fix logging message in get_path_from_regex (#49)
Adds the missing param.
2018-01-17 21:49:22 +01:00
Cristi Vîjdea e30af0287f Fix README and CONTRIBUTING 2018-01-12 15:18:18 +01:00
Cristi Vîjdea fc253665dd Release 1.2.2 2018-01-12 09:44:42 +01:00
h-hirokawa 33199aec59 Update the minimum required version of DRF to 3.7.7
Closes #44, #45.
2018-01-12 09:40:26 +01:00
Cristi Vîjdea 9d1150f1ab Fix travis deployment 2018-01-12 04:38:30 +01:00
Cristi Vîjdea bc4037f721 Fix coverage problems 2018-01-12 04:36:04 +01:00
Cristi Vîjdea 6c4dcb18bb Improve basePath and versioning documentation 2018-01-12 04:18:40 +01:00
Cristi Vîjdea 4445137d55 Add release checklist 2018-01-12 04:06:17 +01:00
Cristi Vîjdea 57870d12a4 Add 1.2.0 changelog 2018-01-12 03:45:32 +01:00
Cristi Vîjdea 7a3fe8ec0c Improve host, schemes and basePath handling (#42)
* added handling of basePath by taking into account SCRIPT_NAME and the longest common prefix
* improved handling of NamespaceVersioning by excluding URLs of differing versions
* added documentation and error messages for the problem reported in #37
2018-01-12 03:37:04 +01:00
Cristi Vîjdea 757d47e1c0 Add py3-django111 testing combination
Tests are also now run in parallel using detox and pytest-xdist.
2018-01-12 03:35:36 +01:00
Cristi Vîjdea 59a51ba4a7 Remove inline style from redoc 2018-01-11 23:37:23 +01:00
Cristi Vîjdea 55223e32e2 Add some assertions to check for common mistakes like #40 2018-01-11 22:40:42 +01:00
Cristi Vîjdea 6a478e14d7 Add overriden path parameter example 2018-01-11 21:20:54 +01:00
Cristi Vîjdea 57d77cc48a Improve validator error handling 2018-01-11 21:20:07 +01:00
Cristi Vîjdea c225f66fb7 Bundle redoc-alpha renderer 2018-01-11 21:19:16 +01:00
Cristi Vîjdea 47de6f2f6f Add Django and djangorestframework to install requirements 2018-01-11 20:41:12 +01:00
Cristi Vîjdea 8dbf3fe984 Remove some inline scripts and styles 2018-01-10 22:54:56 +01:00
Cristi Vîjdea 1c3fba6e54 Add python_requires for pypi
Stolen from encode/django-rest-framework#5739
2018-01-10 21:59:16 +01:00
Cristi Vîjdea c4379dc6a7 Run testproj in a Heroku demo app (#38)
* Add Heroku configuration
* Add links in API description
* Read database connection string from DATABASE_URL environment variable
* Restructure settings files for production
* Run server using gunicorn and servce static files with whitenoise
* Install drf-yasg from source instead of pypi in testproj
* Add readme links to demo app
2018-01-10 10:18:22 +01:00
Cristi Vîjdea 6b38a3b6c1 Update swagger-ui to 3.9.0 2018-01-09 14:43:15 +01:00
Cristi Vîjdea 464a518ae5 Add bdist_wheel distribution 2018-01-09 13:43:22 +01:00
Cristi Vîjdea 15c67891c6 Optimize requirements (#35) 2018-01-03 23:11:27 +01:00
Cristi Vîjdea 917ccd1f56 Restructure travis & tox configuration (#34)
Also removed useless python 3.7-dev build
2018-01-03 20:30:17 +01:00
Cristi Vîjdea fd099998ea Add explicit dependency on uritemplate and six 2018-01-03 16:39:14 +01:00
Cristi Vîjdea a6e24e20c3 Improve README 2018-01-03 16:39:01 +01:00
Cristi Vîjdea 6608e0050c Improve swagger_auto_schema usage error reports 2018-01-03 05:44:53 +01:00
Cristi Vîjdea 68b0dda0b9 Clean up and release 1.1.3 2018-01-02 22:27:09 +01:00
Cristi Vîjdea f81795d745 Always vary cached schema on Cookie and Authorization
This is needed to play nice with session auth on the schema view and with CurrentUserDefault.
2018-01-02 16:14:00 +01:00
Jiri Puc caa397b906 Add requirements.txt and README instructions for test project setup
Closes #32.
2018-01-01 19:17:42 +01:00
Cristi Vîjdea faf81e7b6d Simplify should_filter and should_page 2018-01-01 19:17:37 +01:00
Cristi Vîjdea 74fcd47047 Refactor setup requirements 2018-01-01 19:16:28 +01:00
Cristi Vîjdea 02b72c466e Do not generate readOnly outside Schema properties 2018-01-01 19:16:20 +01:00
Cristi Vîjdea 10deea826d Update swagger-ui to 3.8.1 2017-12-30 14:21:01 +01:00
Cristi Vîjdea bbdc37a45e Remove unneeded files from swagger-ui dist 2017-12-28 16:14:47 +01:00
Cristi Vîjdea eba228a114 Prepare release 1.1.1 2017-12-27 21:05:07 +01:00
Cristi Vîjdea ee4789776a Fix isort inconsistency 2017-12-27 20:47:32 +01:00
Cristi Vîjdea 22853b87c7 Fix test_noop_inspectors 2017-12-27 20:23:21 +01:00
Cristi Vîjdea d39764d383 Add isort 2017-12-27 20:23:05 +01:00
Cristi Vîjdea 60e266fe99 Make sphinx document CamelCaseJSONFilter 2017-12-27 20:14:12 +01:00
Beau Gunderson 1f190744cd Add 'generate_swagger' management command (#31)
Closes #29.
2017-12-27 20:00:24 +01:00
Cristi Vîjdea 9f14114520 Disable travis publish stage for untagged commits 2017-12-27 16:44:15 +01:00
Cristi Vîjdea c85acee405 Convert regex '\Z' to '$'
See https://github.com/swagger-api/swagger-editor/issues/1601.
2017-12-27 03:00:47 +01:00
114 changed files with 3088 additions and 1159 deletions
+8 -1
View File
@@ -1,6 +1,8 @@
[run] [run]
source = drf_yasg source = drf_yasg
branch = True branch = True
parallel = true
disable_warnings = module-not-measured
[report] [report]
# Regexes for lines to exclude from consideration # Regexes for lines to exclude from consideration
@@ -14,10 +16,14 @@ 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
logger.debug
logger.info
logger.warning logger.warning
logger.error
return NotHandled return NotHandled
# Don't complain if non-runnable code isn't run: # Don't complain if non-runnable code isn't run:
@@ -28,7 +34,8 @@ exclude_lines =
raise SwaggerGenerationError raise SwaggerGenerationError
ignore_errors = True ignore_errors = True
precision = 0 precision = 2
show_missing = True
[paths] [paths]
source = source =
+3 -2
View File
@@ -1,4 +1,7 @@
node_modules/ node_modules/
testproj/db.sqlite3
testproj/staticfiles
.vscode/
# Created by .ignore support plugin (hsz.mobi) # Created by .ignore support plugin (hsz.mobi)
### Python template ### Python template
@@ -156,5 +159,3 @@ com_crashlytics_export_strings.xml
crashlytics.properties crashlytics.properties
crashlytics-build.properties crashlytics-build.properties
fabric.properties fabric.properties
testproj/db\.sqlite3
+4 -3
View File
@@ -4,18 +4,19 @@
<facet type="django" name="Django"> <facet type="django" name="Django">
<configuration> <configuration>
<option name="rootFolder" value="$MODULE_DIR$/testproj" /> <option name="rootFolder" value="$MODULE_DIR$/testproj" />
<option name="settingsModule" value="testproj/settings.py" /> <option name="settingsModule" value="testproj/settings/local.py" />
<option name="manageScript" value="manage.py" /> <option name="manageScript" value="manage.py" />
<option name="environment" value="&lt;map/&gt;" /> <option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" /> <option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration> </configuration>
</facet> </facet>
</component> </component>
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/testproj" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/testproj" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.6 (drf-yasg)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.6 (drf-yasg)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
@@ -24,7 +25,7 @@
<option name="TEMPLATE_CONFIGURATION" value="Django" /> <option name="TEMPLATE_CONFIGURATION" value="Django" />
<option name="TEMPLATE_FOLDERS"> <option name="TEMPLATE_FOLDERS">
<list> <list>
<option value="$MODULE_DIR$/drf_yasg/templates" /> <option value="$MODULE_DIR$/src/drf_yasg/templates" />
</list> </list>
</option> </option>
</component> </component>
+44 -35
View File
@@ -1,56 +1,31 @@
language: python language: python
cache: pip cache: pip
python: python:
- '2.7' - '2.7'
- '3.4' - '3.4'
- '3.5' - '3.5'
- '3.6' - '3.6'
- '3.7-dev'
env: env:
- DRF=3.7 - DRF=3.7
install:
- pip install -r requirements/ci.txt
before_script:
- coverage erase
script:
- tox
after_success:
- codecov
branches:
only:
- master
- /^release\/.*$/
- /^v?\d+\.\d+(\.\d+)?(-?\S+)?$/
notifications:
email:
on_success: always
on_failure: always
jobs: jobs:
fast_finish: true
include: include:
- stage: test - stage: test
python: '3.6'
env: DRF=master
-
python: '3.5' python: '3.5'
env: TOXENV=docs env: TOXENV=docs
- -
python: '2.7' python: '2.7'
env: TOXENV=flake8 env: TOXENV=lint
-
python: '3.6'
env: DRF=master
- stage: publish - stage: publish
python: '3.6' python: '3.6'
script: skip script: skip
env: env: PYPI_DEPLOY=true
deploy: &pypi deploy: &pypi
provider: pypi provider: pypi
user: cvijdea user: cvijdea
@@ -58,13 +33,47 @@ jobs:
secure: 54DvknusZ7uHlo9IJxgbNDVKYrwaScyuOZyAGZPb/PTUj8WroQZtp1bFOrAtzfcM4ctIIoLWVzmSwrxypmU4hNif2ZvJ8Vo2PnVh9G6wQ2fD2FN+kFBYczBNrzW5xhjJ53OiTYy/zzHgzxC/sp+hB4sibWl0v69PGU5v6oyBltOWZLXYpqMA6fINt62XDVwuNHVKAo1T/yoeJMQKeCKYAx8QOtve9/qcl5Td/OOM6z42hX5+q3N7RgkCFLl0KopwaPwBaL1Z3Bn+aUhiIUdrRrdigY329QtNXoa/VRBNvUSAwbvecShmgl3c9HigL2ZWmtmHaXda6YCdqmbVfSHSEDsn4AwhZ3A9WblbRtuBwP79YKiE+4BLmgLlGGA4IrAKr3woe+078q/bGqBzmeDd+jt72hhibzD5B96zo4tSNksSxSJGwMYH988fBN/ppynrzRvO0sR/THwpb0r42era8tRd3ZVBefloVas/nQZZs4+zMYoO0fbaLDXdkfaxsF5/X6WkwTYjbI3tdapUT6lYXwi1eKUM1ZGsfKpuq0lFa3qxevYBKveWStQwGyJz1KhVUHbo3OrA3U6q9yoqpzhcZBhzGAPSgumi+EkBUj69cymlYkmqXVBn4VnWsdeFefNYE6Kvh/HJEDPDaWSNgfVLN9xWVqJqM7QiWA351W9dRZA= secure: 54DvknusZ7uHlo9IJxgbNDVKYrwaScyuOZyAGZPb/PTUj8WroQZtp1bFOrAtzfcM4ctIIoLWVzmSwrxypmU4hNif2ZvJ8Vo2PnVh9G6wQ2fD2FN+kFBYczBNrzW5xhjJ53OiTYy/zzHgzxC/sp+hB4sibWl0v69PGU5v6oyBltOWZLXYpqMA6fINt62XDVwuNHVKAo1T/yoeJMQKeCKYAx8QOtve9/qcl5Td/OOM6z42hX5+q3N7RgkCFLl0KopwaPwBaL1Z3Bn+aUhiIUdrRrdigY329QtNXoa/VRBNvUSAwbvecShmgl3c9HigL2ZWmtmHaXda6YCdqmbVfSHSEDsn4AwhZ3A9WblbRtuBwP79YKiE+4BLmgLlGGA4IrAKr3woe+078q/bGqBzmeDd+jt72hhibzD5B96zo4tSNksSxSJGwMYH988fBN/ppynrzRvO0sR/THwpb0r42era8tRd3ZVBefloVas/nQZZs4+zMYoO0fbaLDXdkfaxsF5/X6WkwTYjbI3tdapUT6lYXwi1eKUM1ZGsfKpuq0lFa3qxevYBKveWStQwGyJz1KhVUHbo3OrA3U6q9yoqpzhcZBhzGAPSgumi+EkBUj69cymlYkmqXVBn4VnWsdeFefNYE6Kvh/HJEDPDaWSNgfVLN9xWVqJqM7QiWA351W9dRZA=
on: on:
tags: true tags: true
distributions: sdist distributions: "sdist bdist_wheel"
allow_failures: allow_failures:
- env: TOXENV=flake8 - env: TOXENV=lint
- env: DRF=master - env: DRF=master
- python: '3.7-dev'
fast_finish: true
install:
- pip install -r requirements/ci.txt
before_script:
- coverage erase
- |
[[ -z "$TOXENV" && -z "$PYPI_DEPLOY" ]] && REPORT_COVERAGE="yes" || REPORT_COVERAGE="no";
echo "Reporting coverage: ${REPORT_COVERAGE}"
- |
[[ -z "$TOXENV" && ! -z "$DRF" && "$DRF" != "master" ]] && USE_DETOX="yes" || USE_DETOX="no";
echo "Using detox: ${USE_DETOX}"
script:
- 'if [[ "$USE_DETOX" == "yes" ]]; then detox; else tox; fi'
after_success:
- coverage combine
- 'if [[ "$REPORT_COVERAGE" == "yes" ]]; then coverage report; fi'
- 'if [[ "$REPORT_COVERAGE" == "yes" ]]; then codecov; fi'
branches:
only:
- master
- /^release\/.*$/
- /^v?\d+\.\d+(\.\d+)?(-?\S+)?$/
stages: stages:
- test - test
- publish - name: publish
if: tag IS present
notifications:
email:
on_success: always
on_failure: always
+50 -17
View File
@@ -27,45 +27,55 @@ 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
$ virtualenv venv $ virtualenv venv
$ source venv/bin/activate $ source venv/bin/activate
(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 "Django>=1.11.7"
#. Make your changes and check them against the test project #. **Make your changes and check them against the test project**
.. code:: console .. code:: console
(venv) $ cd testproj (venv) $ cd testproj
(venv) $ python manage.py migrate (venv) $ python manage.py migrate
(venv) $ cat createsuperuser.py | python manage.py shell (venv) $ python manage.py shell -c "import createsuperuser"
(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,33 @@ 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.
******************
Maintainer's notes
******************
Release checklist
=================
* update ``docs/changelog.rst`` with changes since the last tagged version
* commit & tag the release - ``git tag x.x.x -m "Release version x.x.x"``
* push using ``git push --follow-tags``
* verify that `Travis`_ has built the tag and succesfully published the release to `PyPI`_
* publish release notes `on GitHub`_
* start the `ReadTheDocs build`_ if it has not already started
* deploy the live demo `on Heroku`_
.. _Travis: https://travis-ci.org/axnsan12/drf-yasg/builds
.. _PyPI: https://pypi.org/project/drf-yasg/
.. _on GitHub: https://github.com/axnsan12/drf-yasg/releases
.. _ReadTheDocs build: https://readthedocs.org/projects/drf-yasg/builds/
.. _on Heroku: https://dashboard.heroku.com/pipelines/412d1cae-6a95-4f5e-810b-94869133f36a
+1 -1
View File
@@ -10,7 +10,7 @@ License
BSD 3-Clause License BSD 3-Clause License
******************** ********************
Copyright (c) 2017, Cristian V. <cristi@cvjd.me> |br|\ All rights reserved. Copyright (c) 2018, Cristian V. <cristi@cvjd.me> |br|\ All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:
+2
View File
@@ -0,0 +1,2 @@
release: python testproj/manage.py migrate && python testproj/manage.py shell -c "import createsuperuser"
web: gunicorn --chdir testproj testproj.wsgi --log-file -
+84 -113
View File
@@ -11,30 +11,39 @@ Generate **real** Swagger/OpenAPI 2.0 specifications from a Django Rest Framewor
Compatible with Compatible with
- **Django Rest Framework**: 3.7 - **Django Rest Framework**: 3.7.7
- **Django**: 1.11, 2.0 - **Django**: 1.11.x, 2.0.x
- **Python**: 2.7, 3.4, 3.5, 3.6 - **Python**: 2.7, 3.4, 3.5, 3.6
**Source**: https://github.com/axnsan12/drf-yasg/ Resources:
**Documentation**: https://drf-yasg.readthedocs.io/en/latest/ * **Source**: https://github.com/axnsan12/drf-yasg/
* **Documentation**: https://drf-yasg.readthedocs.io/
* **Changelog**: https://drf-yasg.readthedocs.io/en/stable/changelog.html
* **Live demo**: https://drf-yasg-demo.herokuapp.com/
.. image:: https://www.herokucdn.com/deploy/button.svg
:target: https://heroku.com/deploy?template=https://github.com/axnsan12/drf-yasg
:alt: heroku deploy button
******** ********
Features Features
******** ********
- full support for nested Serializers and Schemas - full support for nested Serializers and Schemas
- response schemas and descriptions - response schemas and descriptions
- model definitions compatible with codegen tools - model definitions compatible with codegen tools
- customization hooks at all points in the spec generation process - customization hooks at all points in the spec generation process
- JSON and YAML format for spec - JSON and YAML format for spec
- bundles latest version of - bundles latest version of
`swagger-ui <https://github.com/swagger-api/swagger-ui>`__ and `swagger-ui <https://github.com/swagger-api/swagger-ui>`_ and
`redoc <https://github.com/Rebilly/ReDoc>`__ for viewing the generated documentation `redoc <https://github.com/Rebilly/ReDoc>`_ for viewing the generated documentation
- schema view is cacheable out of the box - schema view is cacheable out of the box
- generated Swagger schema can be automatically validated by - generated Swagger schema can be automatically validated by
`swagger-spec-validator <https://github.com/Yelp/swagger_spec_validator>`__ or `swagger-spec-validator <https://github.com/Yelp/swagger_spec_validator>`_ or
`flex <https://github.com/pipermerriam/flex>`__ `flex <https://github.com/pipermerriam/flex>`_
- supports Django REST Framework API versioning with ``URLPathVersioning`` and ``NamespaceVersioning``; other DRF
or custom versioning schemes are not currently supported
.. figure:: https://raw.githubusercontent.com/axnsan12/drf-yasg/1.0.2/screenshots/redoc-nested-response.png .. figure:: https://raw.githubusercontent.com/axnsan12/drf-yasg/1.0.2/screenshots/redoc-nested-response.png
:width: 100% :width: 100%
@@ -94,42 +103,42 @@ In ``settings.py``:
.. code:: python .. code:: python
INSTALLED_APPS = [ INSTALLED_APPS = [
... ...
'drf_yasg', 'drf_yasg',
... ...
] ]
In ``urls.py``: In ``urls.py``:
.. code:: python .. code:: python
... ...
from drf_yasg.views import get_schema_view from drf_yasg.views import get_schema_view
from drf_yasg import openapi from drf_yasg import openapi
... ...
schema_view = get_schema_view( schema_view = get_schema_view(
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"),
), ),
validators=['ssv', 'flex'], validators=['flex', 'ssv'],
public=True, public=True,
permission_classes=(permissions.AllowAny,), permission_classes=(permissions.AllowAny,),
) )
urlpatterns = [ urlpatterns = [
url(r'^swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'), url(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=None), name='schema-json'),
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'), url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=None), name='schema-swagger-ui'),
url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'), url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=None), name='schema-redoc'),
... ...
] ]
This exposes 4 cached, validated and publicly available endpoints: This exposes 4 cached, validated and publicly available endpoints:
@@ -145,12 +154,13 @@ This exposes 4 cached, validated and publicly available endpoints:
a. ``get_schema_view`` parameters a. ``get_schema_view`` parameters
--------------------------------- ---------------------------------
- ``info`` - Required. Swagger API Info object - ``info`` - Swagger API Info object; if omitted, defaults to ``DEFAULT_INFO``
- ``url`` - API base url; if left blank will be deduced from the location the view is served at - ``url`` - API base url; if left blank will be deduced from the location the view is served at
- ``patterns`` - passed to SchemaGenerator - ``patterns`` - passed to SchemaGenerator
- ``urlconf`` - passed to SchemaGenerator - ``urlconf`` - passed to SchemaGenerator
- ``public`` - if False, includes only endpoints the current user has access to - ``public`` - if False, includes only endpoints the current user has access to
- ``validators`` - a list of validator names to apply on the generated schema; allowed values are ``flex``, ``ssv`` - ``validators`` - a list of validator names to apply on the generated schema; allowed values are ``flex``, ``ssv``
- ``generator_class`` - schema generator class to use; should be a subclass of ``OpenAPISchemaGenerator``
- ``authentication_classes`` - authentication classes for the schema view itself - ``authentication_classes`` - authentication classes for the schema view itself
- ``permission_classes`` - permission classes for the schema view itself - ``permission_classes`` - permission classes for the schema view itself
@@ -166,76 +176,17 @@ b. ``SchemaView`` options
but with optional caching but with optional caching
- you can, of course, call :python:`as_view` as usual - you can, of course, call :python:`as_view` as usual
All of the first 3 methods take two optional arguments, All of the first 3 methods take two optional arguments, ``cache_timeout`` and ``cache_kwargs``; if present,
``cache_timeout`` and ``cache_kwargs``; if present, these are passed on these are passed on to Djangos :python:`cached_page` decorator in order to enable caching on the resulting view.
to Djangos :python:`cached_page` decorator in order to enable caching on the See `3. Caching`_.
resulting view. See `3. Caching`_.
---------------------------------------------- ----------------------------------------------
c. ``SWAGGER_SETTINGS`` and ``REDOC_SETTINGS`` c. ``SWAGGER_SETTINGS`` and ``REDOC_SETTINGS``
---------------------------------------------- ----------------------------------------------
Additionally, you can include some more settings in your ``settings.py`` file. Additionally, you can include some more settings in your ``settings.py`` file.
The possible settings and their default values are as follows: See https://drf-yasg.readthedocs.io/en/stable/settings.html for details.
.. code:: python
SWAGGER_SETTINGS = {
# default inspector classes, see advanced documentation
'DEFAULT_AUTO_SCHEMA_CLASS': 'drf_yasg.inspectors.SwaggerAutoSchema',
'DEFAULT_FIELD_INSPECTORS': [
'drf_yasg.inspectors.CamelCaseJSONFilter',
'drf_yasg.inspectors.ReferencingSerializerInspector',
'drf_yasg.inspectors.RelatedFieldInspector',
'drf_yasg.inspectors.ChoiceFieldInspector',
'drf_yasg.inspectors.FileFieldInspector',
'drf_yasg.inspectors.DictFieldInspector',
'drf_yasg.inspectors.SimpleFieldInspector',
'drf_yasg.inspectors.StringDefaultFieldInspector',
],
'DEFAULT_FILTER_INSPECTORS': [
'drf_yasg.inspectors.CoreAPICompatInspector',
],
'DEFAULT_PAGINATOR_INSPECTORS': [
'drf_yasg.inspectors.DjangoRestResponsePagination',
'drf_yasg.inspectors.CoreAPICompatInspector',
],
'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
'LOGOUT_URL': getattr(django.conf.settings, 'LOGOUT_URL', None), # URL for the logout button
# Swagger security definitions to include in the schema;
# see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-definitions-object
'SECURITY_DEFINITIONS': {
'basic': {
'type': 'basic'
}
},
# url to an external Swagger validation service; defaults to 'http://online.swagger.io/validator/'
# set to None to disable the schema validation badge in the UI
'VALIDATOR_URL': '',
# swagger-ui configuration settings, see https://github.com/swagger-api/swagger-ui/blob/112bca906553a937ac67adc2e500bdeed96d067b/docs/usage/configuration.md#parameters
'OPERATIONS_SORTER': None,
'TAGS_SORTER': None,
'DOC_EXPANSION': 'list',
'DEEP_LINKING': False,
'SHOW_EXTENSIONS': True,
'DEFAULT_MODEL_RENDERING': 'model',
'DEFAULT_MODEL_DEPTH': 3,
}
.. code:: python
REDOC_SETTINGS = {
# ReDoc UI configuration settings, see https://github.com/Rebilly/ReDoc#redoc-tag-attributes
'LAZY_RENDERING': True,
'HIDE_HOSTNAME': False,
'EXPAND_RESPONSES': 'all',
'PATH_IN_MIDDLE': False,
}
3. Caching 3. Caching
========== ==========
@@ -246,16 +197,16 @@ caching the schema view in-memory, with some sane defaults:
* caching is enabled by the `cache_page <https://docs.djangoproject.com/en/1.11/topics/cache/#the-per-view-cache>`__ * caching is enabled by the `cache_page <https://docs.djangoproject.com/en/1.11/topics/cache/#the-per-view-cache>`__
decorator, using the default Django cache backend, can be changed using the ``cache_kwargs`` argument decorator, using the default Django cache backend, can be changed using the ``cache_kwargs`` argument
* HTTP caching of the response is blocked to avoid confusing situations caused by being shown stale schemas * HTTP caching of the response is blocked to avoid confusing situations caused by being shown stale schemas
* if `public` is set to ``False`` on the SchemaView, the cached schema varies on the ``Cookie`` and ``Authorization`` * the cached schema varies on the ``Cookie`` and ``Authorization`` HTTP headers to enable filtering of visible endpoints
HTTP headers to enable filtering of visible endpoints according to the authentication credentials of each user; note according to the authentication credentials of each user; note that this means that every user accessing the schema
that this means that every user accessing the schema will have a separate schema cached in memory. will have a separate schema cached in memory.
4. Validation 4. Validation
============= =============
Given the numerous methods to manually customzie the generated schema, it makes sense to validate the result to ensure Given the numerous methods to manually customzie the generated schema, it makes sense to validate the result to ensure
it still conforms to OpenAPI 2.0. To this end, validation is provided at the generation point using python swagger it still conforms to OpenAPI 2.0. To this end, validation is provided at the generation point using python swagger
libraries, and can be activated by passing :python:`validators=['ssv', 'flex']` to ``get_schema_view``; if the generated libraries, and can be activated by passing :python:`validators=['flex', 'ssv']` to ``get_schema_view``; if the generated
schema is not valid, a :python:`SwaggerValidationError` is raised by the handling codec. schema is not valid, a :python:`SwaggerValidationError` is raised by the handling codec.
**Warning:** This internal validation can slow down your server. **Warning:** This internal validation can slow down your server.
@@ -329,6 +280,26 @@ You can use the specification outputted by this library together with
See the github page linked above for more details. See the github page linked above for more details.
.. _readme-testproj:
6. Example project
==================
For additional usage examples, you can take a look at the test project in the ``testproj`` directory:
.. code:: console
$ git clone https://github.com/axnsan12/drf-yasg.git
$ cd drf-yasg
$ virtualenv venv
$ source venv/bin/activate
(venv) $ cd testproj
(venv) $ pip install -r requirements.txt
(venv) $ python manage.py migrate
(venv) $ python manage.py shell -c "import createsuperuser"
(venv) $ python manage.py runserver
(venv) $ firefox localhost:8000/swagger/
********** **********
Background Background
********** **********
@@ -391,7 +362,7 @@ provided out of the box - if you have ``djangorestframework-camel-case`` install
:alt: Codecov :alt: Codecov
.. |pypi-version| image:: https://img.shields.io/pypi/v/drf-yasg.svg .. |pypi-version| image:: https://img.shields.io/pypi/v/drf-yasg.svg
:target: https://pypi.python.org/pypi/drf-yasg/ :target: https://pypi.org/project/drf-yasg/
:alt: PyPI :alt: PyPI
.. |rtd-badge| image:: https://img.shields.io/readthedocs/drf-yasg.svg .. |rtd-badge| image:: https://img.shields.io/readthedocs/drf-yasg.svg
+17
View File
@@ -0,0 +1,17 @@
{
"name": "drf-yasg Demo app",
"description": "A demonstrative app using https://github.com/axnsan12/drf-yasg",
"repository": "https://github.com/axnsan12/drf-yasg",
"logo": "https://swaggerhub.com/wp-content/uploads/2017/10/Swagger-Icon.svg",
"keywords": [
"django",
"django-rest-framework",
"swagger",
"openapi"
],
"env": {
"DJANGO_SETTINGS_MODULE": "testproj.settings.heroku",
"DJANGO_SECRET_KEY": "m76=^#=z7xv5^(o%4dv9w7+1_c)y2m6)1ogjx%s@9$1^nupry="
},
"success_url": "/"
}
+213
View File
@@ -3,10 +3,213 @@ Changelog
######### #########
*********
**1.6.1**
*********
*Release date: Apr 01, 2018*
- **ADDED:** added ``SUPPORTED_SUBMIT_METHODS`` ``swagger-ui`` setting
*********
**1.6.0**
*********
*Release date: Mar 24, 2018*
- **IMPROVED:** ``OAUTH2_REDIRECT_URL`` will now default to the built in ``oauth2-redirect.html`` file
*********
**1.5.1**
*********
*Release date: Mar 18, 2018*
- **IMPROVED:** updated ``swagger-ui`` to version 3.13.0
- **FIXED:** fixed a crash caused by ``serializers.OneToOneRel`` (:pr:`81`, thanks to :ghuser:`ko-pp`)
*********
**1.5.0**
*********
*Release date: Mar 12, 2018*
- **IMPROVED:** ``serializers.HiddenField`` are now hidden (:issue:`78`, :pr:`79`, thanks to :ghuser:`therefromhere`)
*NOTE:* in order for this to work, you will have to add the new ``drf_yasg.inspectors.HiddenFieldInspector`` to your
``DEFAULT_FIELD_INSPECTORS`` array if you changed it from the default value
- **IMPROVED:** type of model field is now detected for ``serializers.SlugRelatedField`` with ``read_only=True``
(:issue:`82`, :pr:`83`, thanks to :ghuser:`therefromhere`)
*********
**1.4.7**
*********
*Release date: Mar 05, 2018*
- **FIXED:** prevent crashes caused by attempting to delete object attributes which do not exist in the first place
(:issue:`76`)
*********
**1.4.6**
*********
*Release date: Mar 05, 2018*
- **IMPROVED:** updated ``swagger-ui`` to version 3.12.0
- **IMPROVED:** updated ``ReDoc`` to version 1.21.2
*********
**1.4.5**
*********
*Release date: Mar 05, 2018*
- **FIXED:** fixed an issue with modification of ``swagger_auto_schema`` arguments in-place during introspection, which
would sometimes cause an incomplete Swagger document to be generated after the first pass (:issue:`74`, :pr:`75`)
*********
**1.4.4**
*********
*Release date: Feb 26, 2018*
- **IMPROVED:** ``type`` for ``ChoiceField`` generated by a ``ModelSerializer`` from a model field with ``choices=...``
will now be set according to the associated model field (:issue:`69`)
- **FIXED:** ``lookup_field`` and ``lookup_value_regex`` on the same ``ViewSet`` will no longer trigger an exception
(:issue:`68`)
*********
**1.4.3**
*********
*Release date: Feb 22, 2018*
- **FIXED:** added a missing assignment that would cause the ``default`` argument to ``openapi.Parameter.__init__`` to
be ignored
*********
**1.4.2**
*********
*Release date: Feb 22, 2018*
- **FIXED:** fixed a bug that causes a ``ModelViewSet`` generated from models with nested ``ForeignKey`` to output
models named ``Nested`` into the ``definitions`` section (:issue:`59`, :pr:`65`)
- **FIXED:** ``Response`` objects without a ``schema`` are now properly handled when passed through
``swagger_auto_schema`` (:issue:`66`)
*********
**1.4.1**
*********
*Release date: Feb 21, 2018*
- **FIXED:** the ``coerce_to_string`` is now respected when setting the type, default value and min/max values of
``DecimalField`` in the OpenAPI schema (:issue:`62`)
- **FIXED:** error responses from web UI views are now rendered with ``TemplateHTMLRenderer`` instead of throwing
confusing errors (:issue:`58`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.10.0
- **IMPROVED:** updated ``ReDoc`` to version 1.21.0
*********
**1.4.0**
*********
*Release date: Feb 04, 2018*
- **ADDED:** added settings for OAuth2 client configuration in ``swagger-ui`` (:issue:`53`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.9.3
*********
**1.3.1**
*********
*Release date: Jan 24, 2018*
- **FIXED:** fixed a bug that would sometimes cause endpoints to wrongly be output as form operations (:issue:`50`)
- **IMPROVED:** added generation of ``produces`` based on renderer classes
- **IMPROVED:** added generation of top-level ``consumes`` and ``produces`` based on
``DEFAULT_PARSER_CLASSES`` and ``DEFAULT_RENDERER_CLASSES`` (:issue:`48`)
*********
**1.3.0**
*********
*Release date: Jan 23, 2018*
- **ADDED:** security requirements are now correctly set and can be customized; this should fix problems related
to authentication in ``swagger-ui`` Try it out! (:issue:`50`, :pr:`54`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.9.2
- **IMPROVED:** updated ``ReDoc`` to version 1.20.0
- **FIXED:** fixed an exception caused by a warning in get_path_from_regex (:pr:`49`, thanks to :ghuser:`blueyed`)
*********
**1.2.2**
*********
*Release date: Jan 12, 2018*
- **FIXED:** djangorestframework>=3.7.7 is now required because of breaking changes
(:issue:`44`, :pr:`45`, thanks to :ghuser:`h-hirokawa`)
*********
**1.2.1**
*********
*Release date: Jan 12, 2018*
- Fixed deployment issues
*********
**1.2.0**
*********
*Release date: Jan 12, 2018 (missing from PyPI due to deployment issues)*
- **ADDED:** ``basePath`` is now generated by taking into account the ``SCRIPT_NAME`` variable and the
longest common prefix of API urls (:issue:`37`, :pr:`42`)
- **IMPROVED:** removed inline scripts and styles from bundled HTML templates to increase CSP compatibility
- **IMPROVED:** improved validation errors and added more assertion sanity checks (:issue:`37`, :issue:`40`)
- **IMPROVED:** improved handling of NamespaceVersioning by excluding endpoints of differing versions
(i.e. when accesing the schema view for v1, v2 endpoints will not be included in swagger)
*********
**1.1.3**
*********
*Release date: Jan 02, 2018*
- **FIXED:** schema view cache will now always ``Vary`` on the ``Cookie`` and ``Authentication`` (the
``Vary`` header was previously only added if ``public`` was set to ``True``) - this fixes issues related to Django
authentication in ``swagger-ui`` and ``CurrentUserDefault`` values in the schema
*********
**1.1.2**
*********
*Release date: Jan 01, 2018*
- **IMPROVED:** updated ``swagger-ui`` to version 3.8.1
- **IMPROVED:** removed some unneeded static files
*********
**1.1.1**
*********
*Release date: Dec 27, 2017*
- **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**
********* *********
*Release date: Dec 27, 2017*
- **ADDED:** added support for APIs versioned with ``URLPathVersioning`` or ``NamespaceVersioning`` - **ADDED:** added support for APIs versioned with ``URLPathVersioning`` or ``NamespaceVersioning``
- **ADDED:** added ability to recursively customize schema generation - **ADDED:** added ability to recursively customize schema generation
:ref:`using pluggable inspector classes <custom-spec-inspectors>` :ref:`using pluggable inspector classes <custom-spec-inspectors>`
@@ -22,6 +225,8 @@ Changelog
**1.0.6** **1.0.6**
********* *********
*Release date: Dec 23, 2017*
- **FIXED:** Swagger UI "Try it out!" should now work with Django login - **FIXED:** Swagger UI "Try it out!" should now work with Django login
- **FIXED:** callable ``default`` values on serializer fields will now be properly called (:pr:`24`, :issue:`25`) - **FIXED:** callable ``default`` values on serializer fields will now be properly called (:pr:`24`, :issue:`25`)
- **IMPROVED:** updated ``swagger-ui`` to version 3.8.0 - **IMPROVED:** updated ``swagger-ui`` to version 3.8.0
@@ -33,6 +238,8 @@ Changelog
**1.0.5** **1.0.5**
********* *********
*Release date: Dec 18, 2017*
- **FIXED:** fixed a crash caused by having read-only Serializers nested by reference - **FIXED:** fixed a crash caused by having read-only Serializers nested by reference
- **FIXED:** removed erroneous backslashes in paths when routes are generated using Django 2 - **FIXED:** removed erroneous backslashes in paths when routes are generated using Django 2
`path() <https://docs.djangoproject.com/en/2.0/ref/urls/#django.urls.path>`_ `path() <https://docs.djangoproject.com/en/2.0/ref/urls/#django.urls.path>`_
@@ -44,6 +251,8 @@ Changelog
**1.0.4** **1.0.4**
********* *********
*Release date: Dec 16, 2017*
- **FIXED:** fixed improper generation of YAML references - **FIXED:** fixed improper generation of YAML references
- **ADDED:** added ``query_serializer`` parameter to - **ADDED:** added ``query_serializer`` parameter to
:func:`@swagger_auto_schema <.swagger_auto_schema>` (:issue:`16`, :pr:`17`) :func:`@swagger_auto_schema <.swagger_auto_schema>` (:issue:`16`, :pr:`17`)
@@ -52,6 +261,8 @@ Changelog
**1.0.3** **1.0.3**
********* *********
*Release date: Dec 15, 2017*
- **FIXED:** fixed bug that caused schema views returned from cache to fail (:issue:`14`) - **FIXED:** fixed bug that caused schema views returned from cache to fail (:issue:`14`)
- **FIXED:** disabled automatic generation of response schemas for form operations to avoid confusing errors caused by - **FIXED:** disabled automatic generation of response schemas for form operations to avoid confusing errors caused by
attempting to shove file parameters into Schema objects attempting to shove file parameters into Schema objects
@@ -60,4 +271,6 @@ Changelog
**1.0.2** **1.0.2**
********* *********
*Release date: Dec 13, 2017*
- First published version - First published version
+26 -28
View File
@@ -39,7 +39,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = 'drf-yasg' project = 'drf-yasg'
copyright = '2017, Cristi V.' copyright = '2018, Cristi V.'
author = 'Cristi V.' author = 'Cristi V.'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
@@ -162,6 +162,18 @@ autodoc_mock_imports = []
nitpick_ignore = [ nitpick_ignore = [
('py:class', 'object'), ('py:class', 'object'),
('py:class', 'bool'),
('py:class', 'dict'),
('py:class', 'list'),
('py:class', 'str'),
('py:class', 'int'),
('py:class', 'bytes'),
('py:class', 'tuple'),
('py:class', 'callable'),
('py:class', 'type'),
('py:class', 'OrderedDict'),
('py:class', 'None'),
('py:class', 'Exception'), ('py:class', 'Exception'),
('py:class', 'collections.OrderedDict'), ('py:class', 'collections.OrderedDict'),
@@ -174,40 +186,26 @@ nitpick_ignore = [
('py:class', 'OpenAPICodecJson'), ('py:class', 'OpenAPICodecJson'),
('py:class', 'OpenAPISchemaGenerator'), ('py:class', 'OpenAPISchemaGenerator'),
('py:obj', 'bool'), ('py:class', 'coreapi.Field'),
('py:obj', 'dict'), ('py:class', 'BaseFilterBackend'),
('py:obj', 'list'), ('py:class', 'BasePagination'),
('py:obj', 'str'), ('py:class', 'Request'),
('py:obj', 'int'), ('py:class', 'rest_framework.request.Request'),
('py:obj', 'bytes'), ('py:class', 'rest_framework.serializers.Field'),
('py:obj', 'tuple'), ('py:class', 'serializers.Field'),
('py:obj', 'callable'), ('py:class', 'serializers.BaseSerializer'),
('py:obj', 'type'), ('py:class', 'Serializer'),
('py:obj', 'OrderedDict'), ('py:class', 'BaseSerializer'),
('py:obj', 'None'), ('py:class', 'APIView'),
('py:obj', 'coreapi.Field'),
('py:obj', 'BaseFilterBackend'),
('py:obj', 'BasePagination'),
('py:obj', 'Request'),
('py:obj', 'rest_framework.request.Request'),
('py:obj', 'rest_framework.serializers.Field'),
('py:obj', 'serializers.Field'),
('py:obj', 'serializers.BaseSerializer'),
('py:obj', 'Serializer'),
('py:obj', 'BaseSerializer'),
('py:obj', 'APIView'),
] ]
# TODO: inheritance aliases in sphinx 1.7
# even though the package should be already installed, the sphinx build on RTD # even though the package should be already installed, the sphinx build on RTD
# for some reason needs the sources dir to be in the path in order for viewcode to work # for some reason needs the sources dir to be in the path in order for viewcode to work
sys.path.insert(0, os.path.abspath('../src')) sys.path.insert(0, os.path.abspath('../src'))
# activate the Django testproj to be able to succesfully import drf_yasg # activate the Django testproj to be able to succesfully import drf_yasg
sys.path.insert(0, os.path.abspath('../testproj')) sys.path.insert(0, os.path.abspath('../testproj'))
os.putenv('DJANGO_SETTINGS_MODULE', 'testproj.settings') os.putenv('DJANGO_SETTINGS_MODULE', 'testproj.settings.local')
from django.conf import settings # noqa: E402 from django.conf import settings # noqa: E402
@@ -275,7 +273,7 @@ def role_github_user(name, rawtext, text, lineno, inliner, options=None, content
options = options or {} options = options or {}
content = content or [] content = content or []
if not re.match(r"^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", text): if not re.match(r"^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$", text, re.IGNORECASE):
return sphinx_err(inliner, lineno, rawtext, '"%s" is not a valid GitHub username.' % text) return sphinx_err(inliner, lineno, rawtext, '"%s" is not a valid GitHub username.' % text)
ref = gh_user_uri.format(text) ref = gh_user_uri.format(text)
+55 -126
View File
@@ -9,125 +9,30 @@ Custom schema generation
If the default spec generation does not quite match what you were hoping to achieve, ``drf-yasg`` provides some If the default spec generation does not quite match what you were hoping to achieve, ``drf-yasg`` provides some
custom behavior hooks by default. custom behavior hooks by default.
********************* .. _custom-spec-excluding-endpoints:
Swagger spec overview
*********************
This library generates OpenAPI 2.0 documents. The authoritative specification for this document's structure will always *******************
be the official documentation over at `swagger.io <https://swagger.io/>`__ and the `OpenAPI 2.0 specification Excluding endpoints
page <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md>`__. *******************
Beause the above specifications are a bit heavy and convoluted, here is a general overview of how the specification You can prevent a view from being included in the Swagger view by setting its class-level ``swagger_schema``
is structured, starting from the root ``Swagger`` object. attribute to ``None``, or you can prevent an operation from being included by setting its ``auto_schema`` override
to none in :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`:
* :class:`.Swagger` object .. code-block:: python
+ ``info``, ``schemes``, ``securityDefinitions`` and other informative attributes
+ ``paths``: :class:`.Paths` object
A list of all the paths in the API in the form of a mapping
- ``{path}``: :class:`.PathItem` - each :class:`.PathItem` has multiple operations keyed by method class UserList(APIView):
* ``{http_method}``: :class:`.Operation` swagger_schema = None
Each operation is thus uniquely identified by its ``(path, http_method)`` combination,
e.g. ``GET /articles/``, ``POST /articles/``, etc.
* ``parameters``: [:class:`.Parameter`] - and a list of path parameters
+ ``definitions``: named Models
A list of all the named models in the API in the form of a mapping
- ``{ModelName}``: :class:`.Schema` # all methods of the UserList class will be excluded
...
* :class:`.Operation` contains the following information about each operation:
+ ``parameters``: [:class:`.Parameter`]
A list of all the *query*, *header* and *form* parameters accepted by the operation.
- there can also be **at most one** body parameter whose structure is represented by a
:class:`.Schema` or a reference to one (:class:`.SchemaRef`)
+ ``responses``: :class:`.Responses`
A list of all the possible responses the operation is expected to return. Each response can optionally have a
:class:`.Schema` which describes the structure of its body.
- ``{status_code}``: :class:`.Response` - mapping of status code to response definition
+ ``operationId`` - should be unique across all operations
+ ``tags`` - used to group operations in the listing
It is interesting to note the main differences between :class:`.Parameter` and :class:`.Schema` objects:
+----------------------------------------------------------+-----------------------------------------------------------+
| :class:`.Schema` | :class:`.Parameter` |
+==========================================================+===========================================================+
| Can nest other Schemas | Cannot nest other Parameters |br| |
| | Can only nest a Schema if the parameter is ``in: body`` |
+----------------------------------------------------------+-----------------------------------------------------------+
| Cannot describe file uploads |br| | Can describe file uploads via ``type`` = ``file``, |br| |
| - ``file`` is not permitted as a value for ``type`` | but only as part of a form :class:`.Operation` [#formop]_ |
+----------------------------------------------------------+-----------------------------------------------------------+
| Can be used in :class:`.Response`\ s | Cannot be used in :class:`.Response`\ s |
+----------------------------------------------------------+-----------------------------------------------------------+
| Cannot be used in form :class:`.Operation`\ s [#formop]_ | Can be used in form :class:`.Operation`\ s [#formop]_ |
+----------------------------------------------------------+-----------------------------------------------------------+
| Can only describe request or response bodies | Can describe ``query``, ``form``, ``header`` or ``path`` |
| | parameters |
+----------------------------------------------------------+-----------------------------------------------------------+
.. [#formop] a form Operation is an :class:`.Operation` that consumes ``multipart/form-data`` or
``application/x-www-form-urlencoded`` content
* a form Operation cannot have ``body`` parameters
* a non-form operation cannot have ``form`` parameters
****************
Default behavior
****************
This section describes where information is sourced from when using the default generation process.
* :class:`.Paths` are generated by exploring the patterns registered in your default ``urlconf``, or the ``patterns``
and ``urlconf`` you specified when constructing :class:`.OpenAPISchemaGenerator`; only views inheriting from Django
Rest Framework's ``APIView`` are looked at, all other views are ignored
* ``path`` :class:`.Parameter`\ s are generated by looking in the URL pattern for any template parameters; attempts are
made to guess their type from the views ``queryset`` and ``lookup_field``, if applicable. You can override path
parameters via ``manual_parameters`` in :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`.
* ``query`` :class:`.Parameter`\ s - i.e. parameters specified in the URL as ``/path/?query1=value&query2=value`` -
are generated from your view's ``filter_backends`` and ``paginator``, if any are declared. Additional parameters can
be specified via the ``query_serializer`` and ``manual_parameters`` arguments of
:ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`
* The request body is only generated for the HTTP ``POST``, ``PUT`` and ``PATCH`` methods, and is sourced from the
view's ``serializer_class``. You can also override the request body using the ``request_body`` argument of
:ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`.
- if the view represents a form request (that is, all its parsers are of the ``multipart/form-data`` or
``application/x-www-form-urlencoded`` media types), the request body will be output as ``form``
:class:`.Parameter`\ s
- if it is not a form request, the request body will be output as a single ``body`` :class:`.Parameter` wrapped
around a :class:`.Schema`
* ``header`` :class:`.Parameter`\ s are supported by the OpenAPI specification but are never generated by this library;
you can still add them using ``manual_parameters``.
* :class:`.Responses` are generated as follows:
+ if ``responses`` is provided to :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>` and contains at least
one success status code (i.e. any `2xx` status code), no automatic response is generated and the given response
is used as described in the :func:`@swagger_auto_schema documentation <.swagger_auto_schema>`
+ otherwise, an attempt is made to generate a default response:
- the success status code is assumed to be ``204` for ``DELETE`` requests, ``201`` for ``POST`` requests, and
``200`` for all other request methods
- if the view has a request body, the same ``Serializer`` or :class:`.Schema` as in the request body is used
in generating the :class:`.Response` schema; this is inline with the default ``GenericAPIView`` and
``GenericViewSet`` behavior
- if the view has no request body, its ``serializer_class`` is used to generate the :class:`.Response` schema
- if the view is a list view (as defined by :func:`.is_list_view`), the response schema is wrapped in an array
- if the view is also paginated, the response schema is then wrapped in the appropriate paging response structure
- the description of the response is left blank
* :class:`.Response` headers are supported by the OpenAPI specification but not currently supported by this library;
you can still add them manually by providing an `appropriately structured dictionary
<https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#headersObject>`_
to the ``headers`` property of a :class:`.Response` object
* *descriptions* for :class:`.Operation`\ s, :class:`.Parameter`\ s and :class:`.Schema`\ s are picked up from
docstrings and ``help_text`` attributes in the same manner as the `default DRF SchemaGenerator
<http://www.django-rest-framework.org/api-guide/schemas/#schemas-as-documentation>`_
# only the GET method will be shown in Swagger
@swagger_auto_schema(method='put', auto_schema=None)
@swagger_auto_schema(methods=['get'], ...)
@api_view(['GET', 'PUT'])
def user_detail(request, pk):
pass
.. _custom-spec-swagger-auto-schema: .. _custom-spec-swagger-auto-schema:
@@ -138,7 +43,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 +58,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 +74,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 +91,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 +119,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 +134,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 +161,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):
... ...
@@ -267,7 +172,11 @@ You can define some per-serializer options by adding a ``Meta`` class to your se
Currently, the only option you can add here is Currently, the only option you can add here is
* ``ref_name`` - a string which will be used as the model definition name for this serializer class; setting it to * ``ref_name`` - a string which will be used as the model definition name for this serializer class; setting it to
``None`` will force the serializer to be generated as an inline model everywhere it is used ``None`` will force the serializer to be generated as an inline model everywhere it is used. If two serializers
have the same ``ref_name``, both their usages will be replaced with a reference to the same definition.
If this option is not specified, all serializers have an implicit name derived from their class name, minus any
``Serializer`` suffix (e.g. ``UserSerializer`` -> ``User``, ``SerializerWithSuffix`` -> ``SerializerWithSuffix``)
************************* *************************
Subclassing and extending Subclassing and extending
@@ -288,7 +197,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
@@ -320,8 +229,6 @@ This custom generator can be put to use by setting it as the :attr:`.generator_c
``Inspector`` classes ``Inspector`` classes
--------------------- ---------------------
.. versionadded:: 1.1
For customizing behavior related to specific field, serializer, filter or paginator classes you can implement the For customizing behavior related to specific field, serializer, filter or paginator classes you can implement the
:class:`~.inspectors.FieldInspector`, :class:`~.inspectors.SerializerInspector`, :class:`~.inspectors.FilterInspector`, :class:`~.inspectors.FieldInspector`, :class:`~.inspectors.SerializerInspector`, :class:`~.inspectors.FilterInspector`,
:class:`~.inspectors.PaginatorInspector` classes and use them with :class:`~.inspectors.PaginatorInspector` classes and use them with
@@ -331,7 +238,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 +264,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):
@@ -398,3 +305,25 @@ A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``t
This means that you should generally avoid view or method-specific ``FieldInspector``\ s if you are dealing with This means that you should generally avoid view or method-specific ``FieldInspector``\ s if you are dealing with
references (a.k.a named models), because you can never know which view will be the first to generate the schema references (a.k.a named models), because you can never know which view will be the first to generate the schema
for a given serializer. for a given serializer.
**IMPORTANT:** nested fields on ``ModelSerializer``\ s that are generated from model ``ForeignKeys`` will always be
output by value. If you want the by-reference behaviour you have to explictly set the serializer class of nested
fields instead of letting ``ModelSerializer`` generate one automatically; for example:
.. code-block:: python
class OneSerializer(serializers.ModelSerializer):
class Meta:
model = SomeModel
fields = ('id',)
class AnotherSerializer(serializers.ModelSerializer):
chilf = OneSerializer()
class Meta:
model = SomeParentModel
fields = ('id', 'child')
Another caveat that stems from this is that any serializer named "``NestedSerializer``" will be forced inline
unless it has a ``ref_name`` set explicitly.
+2
View File
@@ -12,6 +12,8 @@ drf-yasg
readme.rst readme.rst
rendering.rst rendering.rst
openapi.rst
security.rst
custom_spec.rst custom_spec.rst
custom_ui.rst custom_ui.rst
settings.rst settings.rst
+179
View File
@@ -0,0 +1,179 @@
.. |br| raw:: html
<br />
**********************
Functional overview
**********************
------------------------------
OpenAPI specification overview
------------------------------
This library generates OpenAPI 2.0 documents. The authoritative specification for this document's structure will always
be the official documentation over at `swagger.io <https://swagger.io/>`__ and the `OpenAPI 2.0 specification
page <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md>`__.
Beause the above specifications are a bit heavy and convoluted, here is a general overview of how the specification
is structured, starting from the root ``Swagger`` object.
* :class:`.Swagger` object
+ ``info``, ``schemes``, ``securityDefinitions`` and other informative attributes
+ ``paths``: :class:`.Paths` object
A list of all the paths in the API in the form of a mapping
- ``{path}``: :class:`.PathItem` - each :class:`.PathItem` has multiple operations keyed by method
* ``{http_method}``: :class:`.Operation`
Each operation is thus uniquely identified by its ``(path, http_method)`` combination,
e.g. ``GET /articles/``, ``POST /articles/``, etc.
* ``parameters``: [:class:`.Parameter`] - and a list of path parameters
+ ``definitions``: named Models
A list of all the named models in the API in the form of a mapping
- ``{ModelName}``: :class:`.Schema`
* :class:`.Operation` contains the following information about each operation:
+ ``parameters``: [:class:`.Parameter`]
A list of all the *query*, *header* and *form* parameters accepted by the operation.
- there can also be **at most one** body parameter whose structure is represented by a
:class:`.Schema` or a reference to one (:class:`.SchemaRef`)
+ ``responses``: :class:`.Responses`
A list of all the possible responses the operation is expected to return. Each response can optionally have a
:class:`.Schema` which describes the structure of its body.
- ``{status_code}``: :class:`.Response` - mapping of status code to response definition
+ ``operationId`` - should be unique across all operations
+ ``tags`` - used to group operations in the listing
It is interesting to note the main differences between :class:`.Parameter` and :class:`.Schema` objects:
+----------------------------------------------------------+-----------------------------------------------------------+
| :class:`.Schema` | :class:`.Parameter` |
+==========================================================+===========================================================+
| Can nest other Schemas | Cannot nest other Parameters |br| |
| | Can only nest a Schema if the parameter is ``in: body`` |
+----------------------------------------------------------+-----------------------------------------------------------+
| Cannot describe file uploads |br| | Can describe file uploads via ``type`` = ``file``, |br| |
| - ``file`` is not permitted as a value for ``type`` | but only as part of a form :class:`.Operation` [#formop]_ |
+----------------------------------------------------------+-----------------------------------------------------------+
| Can be used in :class:`.Response`\ s | Cannot be used in :class:`.Response`\ s |
+----------------------------------------------------------+-----------------------------------------------------------+
| Cannot be used in form :class:`.Operation`\ s [#formop]_ | Can be used in form :class:`.Operation`\ s [#formop]_ |
+----------------------------------------------------------+-----------------------------------------------------------+
| Can only describe request or response bodies | Can describe ``query``, ``form``, ``header`` or ``path`` |
| | parameters |
+----------------------------------------------------------+-----------------------------------------------------------+
.. [#formop] a form Operation is an :class:`.Operation` that consumes ``multipart/form-data`` or
``application/x-www-form-urlencoded`` content
* a form Operation cannot have ``body`` parameters
* a non-form operation cannot have ``form`` parameters
----------------
Default behavior
----------------
This section describes where information is sourced from when using the default generation process.
* :class:`.Paths` are generated by exploring the patterns registered in your default ``urlconf``, or the ``patterns``
and ``urlconf`` you specified when constructing :class:`.OpenAPISchemaGenerator`; only views inheriting from Django
Rest Framework's ``APIView`` are looked at, all other views are ignored
* ``path`` :class:`.Parameter`\ s are generated by looking in the URL pattern for any template parameters; attempts are
made to guess their type from the views ``queryset`` and ``lookup_field``, if applicable. You can override path
parameters via ``manual_parameters`` in :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`.
* ``query`` :class:`.Parameter`\ s - i.e. parameters specified in the URL as ``/path/?query1=value&query2=value`` -
are generated from your view's ``filter_backends`` and ``paginator``, if any are declared. Additional parameters can
be specified via the ``query_serializer`` and ``manual_parameters`` arguments of
:ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`
* The request body is only generated for the HTTP ``POST``, ``PUT`` and ``PATCH`` methods, and is sourced from the
view's ``serializer_class``. You can also override the request body using the ``request_body`` argument of
:ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`.
- if the view represents a form request (that is, all its parsers are of the ``multipart/form-data`` or
``application/x-www-form-urlencoded`` media types), the request body will be output as ``form``
:class:`.Parameter`\ s
- if it is not a form request, the request body will be output as a single ``body`` :class:`.Parameter` wrapped
around a :class:`.Schema`
* ``header`` :class:`.Parameter`\ s are supported by the OpenAPI specification but are never generated by this library;
you can still add them using ``manual_parameters``.
* :class:`.Responses` are generated as follows:
+ if ``responses`` is provided to :ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>` and contains at least
one success status code (i.e. any `2xx` status code), no automatic response is generated and the given response
is used as described in the :func:`@swagger_auto_schema documentation <.swagger_auto_schema>`
+ otherwise, an attempt is made to generate a default response:
- the success status code is assumed to be ``204` for ``DELETE`` requests, ``201`` for ``POST`` requests, and
``200`` for all other request methods
- if the view has a request body, the same ``Serializer`` or :class:`.Schema` as in the request body is used
in generating the :class:`.Response` schema; this is inline with the default ``GenericAPIView`` and
``GenericViewSet`` behavior
- if the view has no request body, its ``serializer_class`` is used to generate the :class:`.Response` schema
- if the view is a list view (as defined by :func:`.is_list_view`), the response schema is wrapped in an array
- if the view is also paginated, the response schema is then wrapped in the appropriate paging response structure
- the description of the response is left blank
* :class:`.Response` headers are supported by the OpenAPI specification but not currently supported by this library;
you can still add them manually by providing an `appropriately structured dictionary
<https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#headersObject>`_
to the ``headers`` property of a :class:`.Response` object
* *descriptions* for :class:`.Operation`\ s, :class:`.Parameter`\ s and :class:`.Schema`\ s are picked up from
docstrings and ``help_text`` attributes in the same manner as the `default DRF SchemaGenerator
<http://www.django-rest-framework.org/api-guide/schemas/#schemas-as-documentation>`_
* .. _custom-spec-base-url:
The base URL for the API consists of three values - the ``host``, ``schemes`` and ``basePath`` attributes
* The host name and scheme are determined, in descending order of priority:
+ from the ``url`` argument passed to :func:`.get_schema_view` (more specifically, to the underlying
:class:`.OpenAPISchemaGenerator`)
+ from the :ref:`DEFAULT_API_URL setting <default-swagger-settings>`
+ inferred from the request made to the schema endpoint
For example, an url of ``https://www.example.com:8080/some/path`` will populate the ``host`` and ``schemes``
attributes with ``www.example.com:8080`` and ``['https']``, respectively. The path component will be ignored.
* The base path is determined as the concatenation of two variables:
#. the `SCRIPT_NAME`_ wsgi environment variable; this is set, for example, when serving the site from a
sub-path using web server url rewriting
.. Tip::
The Django `FORCE_SCRIPT_NAME`_ setting can be used to override the `SCRIPT_NAME`_ or set it when it's
missing from the environment.
#. the longest common path prefix of all the urls in your API - see :meth:`.determine_path_prefix`
* When using API versioning with ``NamespaceVersioning`` or ``URLPathVersioning``, versioned endpoints that do not
match the version used to access the ``SchemaView`` will be excluded from the endpoint list - for example,
``/api/v1.0/endpoint`` will be shown when viewing ``/api/v1.0/swagger/``, while ``/api/v2.0/endpoint`` will not
Other versioning schemes are not presently supported.
---------------------
A note on limitations
---------------------
When schema generation is requested, available endpoints are inspected by enumeration all the routes registered in
Django's urlconf. Each registered view is then artificially instantiated for introspection, and it is this step that
brings some limitations to what can be done:
* the ``request`` the view sees will always be the request made against the schema view endpoint
- e.g. ``GET /swagger.yaml``
* path parameters will not be filled
This means that you could get surprizing results if your ``get_serializer`` or ``get_serializer_class`` methods
depend on the incoming request, call ``get_object`` or in general depend on any stateful logic. You can prevent this
in a few ways:
* provide a fixed serializer for request and response body introspection using
:ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`, to prevent ``get_serializer`` from being called on
the view
* :ref:`exclude your endpoint from introspection <custom-spec-excluding-endpoints>`
.. _SCRIPT_NAME: https://www.python.org/dev/peps/pep-0333/#environ-variables
.. _FORCE_SCRIPT_NAME: https://docs.djangoproject.com/en/2.0/ref/settings/#force-script-name
+51 -1
View File
@@ -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,52 @@ 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
******************
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 ...
.. Note::
The :ref:`DEFAULT_INFO <default-swagger-settings>` setting must be defined when using the ``generate_swagger``
command. For example, the :ref:`README quickstart <readme-quickstart>` code could be modified as such:
In ``settings.py``:
.. code-block:: python
SWAGGER_SETTINGS = {
'DEFAULT_INFO': 'import.path.to.urls.api_info',
}
In ``urls.py``:
.. code-block:: python
api_info = openapi.Info(
title="Snippets API",
... other arguments ...
)
schema_view = get_schema_view(
# the info argument is no longer needed here as it will be picked up from DEFAULT_INFO
... other arguments ...
)
+99
View File
@@ -0,0 +1,99 @@
*********************************
Describing authentication schemes
*********************************
When using the `swagger-ui` frontend, it is possible to interact with the API described by your Swagger document.
This interaction might require authentication, which you will have to describe in order to make `swagger-ui` work
with it.
--------------------
Security definitions
--------------------
The first step that you have to do is add a :ref:`SECURITY_DEFINITIONS <security-definitions-settings>` setting
to declare all authentication schemes supported by your API.
For example, the definition for a simple API accepting HTTP basic auth and `Authorization` header API tokens would be:
.. code-block:: python
SWAGGER_SETTINGS = {
'SECURITY_DEFINITIONS': {
'Basic': {
'type': 'basic'
},
'Bearer': {
'type': 'apiKey',
'name': 'Authorization',
'in': 'header'
}
}
}
---------------------
Security requirements
---------------------
The second step is specifying, for each endpoint, which authentication mechanism can be used for interacting with it.
See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#security-requirement-object for details.
By default, a top-level `security` that accepts all the declared security definitions is generated.
For the example above, that would be :code:`[{'Basic': []}, {'Bearer': []}]`. This can be overriden using the
:ref:`SECURITY_REQUIREMENTS <security-definitions-settings>` setting.
Operation-level overrides can be added using the ``security`` parameter of
:ref:`@swagger_auto_schema <custom-spec-swagger-auto-schema>`.
-------------------------------
``swagger-ui`` as OAuth2 client
-------------------------------
It is possible to configure ``swagger-ui`` to authenticate against your (or a third party) OAuth2 service when sending
"Try it out" requests. This client-side configuration does not remove the requirement of a spec-side
:ref:`security definiiton <security-definitions-settings>`, but merely allows you to test OAuth2 APIs using
``swagger-ui`` as a client.
**DISCLAIMER**: this setup is very poorly tested as I do not currently implement OAuth in any of my projects. All
contributions relating to documentation, bugs, mistakes or anything else are welcome as an issue or pull request. The
settings described below were added as a result of discussion in issue :issue:`53`.
The settings of interest can be found on the :ref:`settings page <oauth2-settings>`. Configuration options are similar
to most OAuth client setups like web or mobile applications. Reading the relevant ``swagger-ui`` docmentation linked
will also probably help.
Example
^^^^^^^
A very simple working configuration was provided by :ghuser:`Vigrond`, originally at
`https://github.com/Vigrond/django_oauth2_example <https://github.com/Vigrond/django_oauth2_example>`_.
.. code-block:: python
SWAGGER_SETTINGS = {
'USE_SESSION_AUTH': False,
'SECURITY_DEFINITIONS': {
'Your App API - Swagger': {
'type': 'oauth2',
'authorizationUrl': '/yourapp/o/authorize',
'tokenUrl': '/yourapp/o/token/',
'flow": "accessCode',
'scopes': {
'read:groups': 'read groups',
}
}
},
'OAUTH2_CONFIG': {
'clientId': 'yourAppClientId',
'clientSecret': 'yourAppClientSecret',
'appName': 'your application name'
},
}
If the OAuth2 provider requires you to provide the full absolute redirect URL, the default value for most
``staticfiles`` configurations will be ``<origin>/static/drf-yasg/swagger-ui-dist/oauth2-redirect.html``. If this is
not suitable for some reason, you can override the ``OAUTH2_REDIRECT_URL`` setting as appropriate.
+72 -2
View File
@@ -15,7 +15,7 @@ Example:
**settings.py** **settings.py**
.. code:: python .. code-block:: python
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'SECURITY_DEFINITIONS': { 'SECURITY_DEFINITIONS': {
@@ -66,6 +66,7 @@ to this list.
:class:`'drf_yasg.inspectors.ChoiceFieldInspector' <.inspectors.ChoiceFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.ChoiceFieldInspector' <.inspectors.ChoiceFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.FileFieldInspector' <.inspectors.FileFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.FileFieldInspector' <.inspectors.FileFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.DictFieldInspector' <.inspectors.DictFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.DictFieldInspector' <.inspectors.DictFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.HiddenFieldInspector' <.inspectors.HiddenFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.SimpleFieldInspector' <.inspectors.SimpleFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.SimpleFieldInspector' <.inspectors.SimpleFieldInspector>`, |br| \
:class:`'drf_yasg.inspectors.StringDefaultFieldInspector' <.inspectors.StringDefaultFieldInspector>`, |br| \ :class:`'drf_yasg.inspectors.StringDefaultFieldInspector' <.inspectors.StringDefaultFieldInspector>`, |br| \
``]`` ``]``
@@ -91,6 +92,30 @@ 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-swagger-settings:
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`` and ``schemes`` attributes
of the Swagger document if no API URL is otherwise provided. The Django `FORCE_SCRIPT_NAME`_ setting can be used for
providing an API mount point prefix.
See also: :ref:`documentation on base URL construction <custom-spec-base-url>`
**Default**: :python:`None`
Authorization Authorization
============= =============
@@ -116,6 +141,9 @@ URL for the Django Logout action when using `USE_SESSION_AUTH`_.
**Default**: :python:`django.conf.settings.LOGOUT_URL` **Default**: :python:`django.conf.settings.LOGOUT_URL`
.. _security-definitions-settings:
SECURITY_DEFINITIONS SECURITY_DEFINITIONS
-------------------- --------------------
@@ -124,12 +152,20 @@ 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'
} }
SECURITY_REQUIREMENTS
---------------------
Global security requirements. If :python:`None`, all schemes in ``SECURITY_DEFINITIONS`` are accepted. |br|
See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#securityRequirementObject.
**Default**: :python:`None`
.. _swagger-ui-settings: .. _swagger-ui-settings:
Swagger UI settings Swagger UI settings
@@ -218,6 +254,37 @@ Controls how many levels are expaned by default when showing nested models.
**Default**: :python:`3` |br| **Default**: :python:`3` |br|
*Maps to parameter*: ``defaultModelExpandDepth`` *Maps to parameter*: ``defaultModelExpandDepth``
.. _oauth2-settings:
OAUTH2_REDIRECT_URL
-------------------
Used when OAuth2 authenitcation of API requests via swagger-ui is desired. If ``None`` is passed, the
``oauth2RedirectUrl`` parameter will be set to ``{% static 'drf-yasg/swagger-ui-dist/oauth2-redirect.html' %}``. This
is the default `https://github.com/swagger-api/swagger-ui/blob/master/dist/oauth2-redirect.html <oauth2-redirect>`_
file provided by ``swagger-ui``.
**Default**: :python:`None` |br|
*Maps to parameter*: ``oauth2RedirectUrl``
OAUTH2_CONFIG
-------------
Used when OAuth2 authenitcation of API requests via swagger-ui is desired. Provides OAuth2 configuration parameters
to the ``SwaggerUIBundle#initOAuth`` method, and must be a dictionary. See
`OAuth2 configuration <https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md>`_.
**Default**: :python:`{}`
SUPPORTED_SUBMIT_METHODS
------------------------
List of HTTP methods that have the Try it out feature enabled. An empty array disables Try it out for all operations.
This does not filter the operations from the display.
**Default**: :python:`['get','put','post','delete','options','head','patch','trace']` |br|
*Maps to parameter*: ``supportedSubmitMethods``
****************** ******************
``REDOC_SETTINGS`` ``REDOC_SETTINGS``
****************** ******************
@@ -253,3 +320,6 @@ PATH_IN_MIDDLE
**Default**: :python:`False` |br| **Default**: :python:`False` |br|
*Maps to attribute*: ``path-in-middle-panel`` *Maps to attribute*: ``path-in-middle-panel``
.. _FORCE_SCRIPT_NAME: https://docs.djangoproject.com/en/2.0/ref/settings/#force-script-name
+67 -64
View File
@@ -4,9 +4,9 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"dependencies": { "dependencies": {
"argparse": { "argparse": {
"version": "1.0.9", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": { "requires": {
"sprintf-js": "1.0.3" "sprintf-js": "1.0.3"
} }
@@ -38,9 +38,9 @@
} }
}, },
"commander": { "commander": {
"version": "2.12.2", "version": "2.14.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.12.2.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
"integrity": "sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==", "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
"optional": true "optional": true
}, },
"core-js": { "core-js": {
@@ -73,9 +73,9 @@
"integrity": "sha1-8TyUAhQdoJ50rfTmN5jXkiBEOPI=" "integrity": "sha1-8TyUAhQdoJ50rfTmN5jXkiBEOPI="
}, },
"es6-promise": { "es6-promise": {
"version": "4.1.1", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
"integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==" "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ=="
}, },
"esprima": { "esprima": {
"version": "4.0.0", "version": "4.0.0",
@@ -126,7 +126,7 @@
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz",
"integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==",
"requires": { "requires": {
"argparse": "1.0.9", "argparse": "1.0.10",
"esprima": "4.0.0" "esprima": "4.0.0"
} }
}, },
@@ -145,10 +145,10 @@
"requires": { "requires": {
"call-me-maybe": "1.0.1", "call-me-maybe": "1.0.1",
"debug": "3.1.0", "debug": "3.1.0",
"es6-promise": "4.1.1", "es6-promise": "4.2.4",
"js-yaml": "3.10.0", "js-yaml": "3.10.0",
"ono": "4.0.2", "ono": "4.0.3",
"z-schema": "3.19.0" "z-schema": "3.19.1"
} }
}, },
"lodash.get": { "lodash.get": {
@@ -166,23 +166,31 @@
"resolved": "https://registry.npmjs.org/lunr/-/lunr-1.0.0.tgz", "resolved": "https://registry.npmjs.org/lunr/-/lunr-1.0.0.tgz",
"integrity": "sha1-XJJ2ySyRrDWpJBtQGNRnI9kuL18=" "integrity": "sha1-XJJ2ySyRrDWpJBtQGNRnI9kuL18="
}, },
"mark.js": {
"version": "8.11.1",
"resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz",
"integrity": "sha1-GA8fnr74sOY45BZq1S24eb6y/8U="
},
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}, },
"ono": { "ono": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/ono/-/ono-4.0.2.tgz", "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.3.tgz",
"integrity": "sha512-EFXJFoeF+KkZW4lwmcPMKHp2ZU7o6CM+ccX2nPbEJKiJIdyqbIcS1v6pmNgeNJ6x4/vEYn0/8oz66qXSPnnmSQ==", "integrity": "sha512-7QIxG4UB00H7CR7fhXC/U7VhB5DK9wsYLwaYBui1JmQoXtLkhIBn3fbuk6FgAP+ctWeBsWVTM+R/bThvUZN+ww==",
"requires": { "requires": {
"format-util": "1.0.3" "format-util": "1.0.3"
} }
}, },
"openapi-sampler": { "openapi-sampler": {
"version": "0.4.3", "version": "1.0.0-beta.8",
"resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-0.4.3.tgz", "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.0.0-beta.8.tgz",
"integrity": "sha512-Ml6o1gt++ZQ4JKL344YRo/fX05yuM6C+l/mGVX2yjhu1BRKyrRK4Z46uBTKSVaag1xINBFwYG7dZdz/10AmPzA==" "integrity": "sha1-v0P/R3N/xOH5iNDiCC1JeI9B3q0=",
"requires": {
"json-pointer": "0.6.0"
}
}, },
"perfect-scrollbar": { "perfect-scrollbar": {
"version": "0.8.1", "version": "0.8.1",
@@ -190,36 +198,36 @@
"integrity": "sha512-RNC5tX/JMRYR+qVdJTEAWnRxw0Yf9lvbO8lTuAOvgDODkiA8lveTSkvrNMhmaGKEyimJpJl+myb/syVS9YyPuw==" "integrity": "sha512-RNC5tX/JMRYR+qVdJTEAWnRxw0Yf9lvbO8lTuAOvgDODkiA8lveTSkvrNMhmaGKEyimJpJl+myb/syVS9YyPuw=="
}, },
"prismjs": { "prismjs": {
"version": "1.9.0", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.9.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.11.0.tgz",
"integrity": "sha1-+j4tntw8OIfB8fMJXUHx+bQgDw8=", "integrity": "sha1-KXrvM+t5Qhv9sZJzpQkspRWXDSk=",
"requires": { "requires": {
"clipboard": "1.7.1" "clipboard": "1.7.1"
} }
}, },
"process-nextick-args": { "process-nextick-args": {
"version": "1.0.7", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
}, },
"readable-stream": { "readable-stream": {
"version": "2.3.3", "version": "2.3.5",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz",
"integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==",
"requires": { "requires": {
"core-util-is": "1.0.2", "core-util-is": "1.0.2",
"inherits": "2.0.3", "inherits": "2.0.3",
"isarray": "1.0.0", "isarray": "1.0.0",
"process-nextick-args": "1.0.7", "process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1", "safe-buffer": "5.1.1",
"string_decoder": "1.0.3", "string_decoder": "1.0.3",
"util-deprecate": "1.0.2" "util-deprecate": "1.0.2"
} }
}, },
"redoc": { "redoc": {
"version": "1.19.3", "version": "1.21.2",
"resolved": "https://registry.npmjs.org/redoc/-/redoc-1.19.3.tgz", "resolved": "https://registry.npmjs.org/redoc/-/redoc-1.21.2.tgz",
"integrity": "sha1-DfPx+6S92G/+nGIAEzBxUjytVec=", "integrity": "sha1-uMeLfDxtwFjmeZniwvYbqFjewaY=",
"requires": { "requires": {
"core-js": "2.5.3", "core-js": "2.5.3",
"dropkickjs": "2.1.10", "dropkickjs": "2.1.10",
@@ -228,21 +236,16 @@
"json-pointer": "0.6.0", "json-pointer": "0.6.0",
"json-schema-ref-parser": "3.3.1", "json-schema-ref-parser": "3.3.1",
"lunr": "1.0.0", "lunr": "1.0.0",
"mark.js": "github:julmot/mark.js#714c9523feca999267f1758da8cfd92d077105d0", "mark.js": "8.11.1",
"openapi-sampler": "0.4.3", "openapi-sampler": "1.0.0-beta.8",
"perfect-scrollbar": "0.8.1", "perfect-scrollbar": "0.8.1",
"prismjs": "1.9.0", "prismjs": "1.11.0",
"remarkable": "1.7.1", "remarkable": "1.7.1",
"scrollparent": "2.0.1", "scrollparent": "2.0.1",
"slugify": "1.2.6", "slugify": "1.2.9",
"stream-http": "2.7.2", "stream-http": "2.8.0",
"ts-helpers": "1.1.2", "ts-helpers": "1.1.2",
"zone.js": "0.8.18" "zone.js": "0.8.20"
},
"dependencies": {
"mark.js": {
"version": "github:julmot/mark.js#714c9523feca999267f1758da8cfd92d077105d0"
}
} }
}, },
"remarkable": { "remarkable": {
@@ -282,9 +285,9 @@
"optional": true "optional": true
}, },
"slugify": { "slugify": {
"version": "1.2.6", "version": "1.2.9",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.2.6.tgz", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.2.9.tgz",
"integrity": "sha512-796YAGnzEnLKQHAFf7H2q1nsjY/9qywSnF9ZkMUbs9he4aZaXO/zFUow0LZ95sBAiQjOX1EmGl23gTYaswiNaQ==" "integrity": "sha512-n0cdJ+kN3slJu8SbZXt/EHjljBqF6MxvMGSg/NPpBzoY7yyXoH38wp/ox20a1JaG1KgmdTN5Lf3aS9+xB2Y2aQ=="
}, },
"sprintf-js": { "sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
@@ -292,13 +295,13 @@
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
}, },
"stream-http": { "stream-http": {
"version": "2.7.2", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.7.2.tgz", "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.0.tgz",
"integrity": "sha512-c0yTD2rbQzXtSsFSVhtpvY/vS6u066PcXOX9kBB3mSO76RiUQzL340uJkGBWnlBg4/HZzqiUXtaVA7wcRcJgEw==", "integrity": "sha512-sZOFxI/5xw058XIRHl4dU3dZ+TTOIGJR78Dvo0oEAejIt4ou27k+3ne1zYmCV+v7UucbxIFQuOgnkTVHh8YPnw==",
"requires": { "requires": {
"builtin-status-codes": "3.0.0", "builtin-status-codes": "3.0.0",
"inherits": "2.0.3", "inherits": "2.0.3",
"readable-stream": "2.3.3", "readable-stream": "2.3.5",
"to-arraybuffer": "1.0.1", "to-arraybuffer": "1.0.1",
"xtend": "4.0.1" "xtend": "4.0.1"
} }
@@ -312,9 +315,9 @@
} }
}, },
"swagger-ui-dist": { "swagger-ui-dist": {
"version": "3.8.0", "version": "3.13.0",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.8.0.tgz", "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.13.0.tgz",
"integrity": "sha1-BHfLOagE7a6Wx+COskDNWagEt3U=" "integrity": "sha1-NPpclKDx3EAWVBmIEV38rhOKirE="
}, },
"tiny-emitter": { "tiny-emitter": {
"version": "2.0.2", "version": "2.0.2",
@@ -348,9 +351,9 @@
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
}, },
"validator": { "validator": {
"version": "9.2.0", "version": "9.4.1",
"resolved": "https://registry.npmjs.org/validator/-/validator-9.2.0.tgz", "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz",
"integrity": "sha512-6Ij4Eo0KM4LkR0d0IegOwluG5453uqT5QyF5SV5Ezvm8/zmkKI/L4eoraafZGlZPC9guLkwKzgypcw8VGWWnGA==" "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA=="
}, },
"xtend": { "xtend": {
"version": "4.0.1", "version": "4.0.1",
@@ -358,20 +361,20 @@
"integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
}, },
"z-schema": { "z-schema": {
"version": "3.19.0", "version": "3.19.1",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.19.0.tgz", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.19.1.tgz",
"integrity": "sha512-V94f3ODuluBS4kQLLjNhwoMek0dyIXCsvNu/A17dAyJ6sMhT5KkJQwSn07R0naByLIXJWMDk+ruMfI/3G3hS4Q==", "integrity": "sha512-jPNzqmOu3+AGbb4krDODqo4QBzwUGDVzyfGyy1HtWaUnafltQotatSpxxWd6Mp0iSZOUwHU5sqKYi+U8HsHMkg==",
"requires": { "requires": {
"commander": "2.12.2", "commander": "2.14.1",
"lodash.get": "4.4.2", "lodash.get": "4.4.2",
"lodash.isequal": "4.5.0", "lodash.isequal": "4.5.0",
"validator": "9.2.0" "validator": "9.4.1"
} }
}, },
"zone.js": { "zone.js": {
"version": "0.8.18", "version": "0.8.20",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.18.tgz", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.8.20.tgz",
"integrity": "sha512-knKOBQM0oea3/x9pdyDuDi7RhxDlJhOIkeixXSiTKWLgs4LpK37iBc+1HaHwzlciHUKT172CymJFKo8Xgh+44Q==" "integrity": "sha512-FXlA37ErSXCMy5RNBcGFgCI/Zivqzr0D19GuvDxhcYIJc7xkFp6c29DKyODJu0Zo+EMyur/WPPgcBh1EHjB9jA=="
} }
} }
} }
+8 -3
View File
@@ -1,7 +1,12 @@
{ {
"name": "drf-yasg", "name": "drf-yasg",
"dependencies": { "dependencies": {
"redoc": "^1.19.3", "redoc": "^1.21.2",
"swagger-ui-dist": "^3.8.0" "swagger-ui-dist": "^3.13.0"
} },
"repository": {
"type": "git",
"url": "https://github.com/axnsan12/drf-yasg.git"
},
"private": true
} }
+2
View File
@@ -0,0 +1,2 @@
.[validation]
-r requirements/heroku.txt
+6
View File
@@ -4,3 +4,9 @@ openapi_codec>=1.3.2
ruamel.yaml>=0.15.34 ruamel.yaml>=0.15.34
inflection>=0.3.1 inflection>=0.3.1
future>=0.16.0 future>=0.16.0
six>=1.10.0
uritemplate>=3.0.0
djangorestframework>=3.7.7
Django>=1.11.7,<2.0; python_version <= "2.7"
Django>=1.11.7; python_version >= "3.4"
+3 -2
View File
@@ -1,4 +1,5 @@
# requirements for CI test suite # requirements for the CI test runner
-r dev.txt
tox-travis>=0.10 tox-travis>=0.10
codecov>=2.0.9 codecov>=2.0.9
-r tox.txt
+5 -5
View File
@@ -1,6 +1,6 @@
# requirements for local development # requirements for local development to be installed via pip install -r requirements/dev.txt
tox>=2.9.1 -r tox.txt
tox-battery>=0.5 -r test.txt
-r lint.txt
# do not unpin this (see setup.py) tox-battery>=0.5
setuptools_scm==1.15.6
+7 -5
View File
@@ -1,6 +1,8 @@
Sphinx==1.6.5 # used by the 'docs' tox env for building the documentation
sphinx_rtd_theme==0.2.4 Sphinx>=1.7.0
Pillow==4.3.0 sphinx_rtd_theme>=0.2.4
readme_renderer==17.2 Pillow>=4.3.0
readme_renderer>=17.2
Django==2.0 Django>=2.0,<2.1
djangorestframework_camel_case>=0.2.0
+5
View File
@@ -0,0 +1,5 @@
# requirements necessary when deploying the test project to heroku
-r testproj.txt
psycopg2>=2.7.3
gunicorn>=19.7.1
whitenoise>=3.3.1
+4
View File
@@ -0,0 +1,4 @@
# used by the 'lint' tox env for linting via flake8
isort>=4.2
flake8>=3.5.0
flake8-isort>=2.3
+4
View File
@@ -0,0 +1,4 @@
# needed to build the package setup_requires in setup.py
# do not unpin this (see setup.py)
setuptools_scm==1.15.6
+3 -8
View File
@@ -1,15 +1,10 @@
# pytest runner + plugins # requirements for running the tests via pytest
pytest>=2.9 pytest>=2.9
pytest-pythonpath>=0.7.1 pytest-pythonpath>=0.7.1
pytest-cov>=2.5.1 pytest-cov>=2.5.1
pytest-xdist>=1.22.0
# latest pip version of pytest-django is more than a year old and does not support Django 2.0 # latest pip version of pytest-django is more than a year old and does not support Django 2.0
git+https://github.com/pytest-dev/pytest-django.git@94cccb956435dd7a719606744ee7608397e1eafb git+https://github.com/pytest-dev/pytest-django.git@94cccb956435dd7a719606744ee7608397e1eafb
datadiff==2.0.0 datadiff==2.0.0
# test project requirements -r testproj.txt
Pillow>=4.3.0
pygments>=2.2.0
django-cors-headers>=2.1.0
django-filter>=1.1.0,<2.0; python_version == "2.7"
django-filter>=1.1.0; python_version >= "3.4"
djangorestframework-camel-case>=0.2.0
+9
View File
@@ -0,0 +1,9 @@
# test project requirements
Pillow>=4.3.0
pygments>=2.2.0
django-cors-headers>=2.1.0
django-filter>=1.1.0,<2.0; python_version == "2.7"
django-filter>=1.1.0; python_version >= "3.4"
djangorestframework-camel-case>=0.2.0
dj-database-url>=0.4.2
user_agents>=1.1.0
+5
View File
@@ -0,0 +1,5 @@
# requirements for building and running tox
tox>=2.9.1
detox>=0.11
-r setup.txt
+1
View File
@@ -0,0 +1 @@
python-3.6.4
+2
View File
@@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1
+36 -21
View File
@@ -1,13 +1,25 @@
#!/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 random
import string
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']
def read_req(req_file):
with open(os.path.join('requirements', req_file)) as req:
return [line.strip() for line in req.readlines() if line.strip() and not line.strip().startswith('#')]
with io.open('README.rst', encoding='utf-8') as readme:
description = readme.read()
requirements = read_req('base.txt')
requirements_setup = read_req('setup.txt')
requirements_validation = read_req('validation.txt')
def _install_setup_requires(attrs): def _install_setup_requires(attrs):
@@ -22,14 +34,14 @@ def _install_setup_requires(attrs):
dist.fetch_build_eggs(dist.setup_requires) dist.fetch_build_eggs(dist.setup_requires)
if 'sdist' in sys.argv: try:
try: # try to install setuptools_scm before setuptools does it, otherwise our monkey patch below will come too early
# try to install setuptools_scm before setuptools does it, otherwise our monkey patch below will come too early # (setuptools_scm adds find_files hooks into setuptools on install)
# (setuptools_scm adds find_files hooks into setuptools on install) _install_setup_requires({'setup_requires': requirements_setup})
_install_setup_requires({'setup_requires': requirements_setup}) except Exception:
except Exception: pass
pass
if 'sdist' in sys.argv:
try: try:
# see https://github.com/pypa/setuptools_scm/issues/190, setuptools_scm includes ALL versioned files from # see https://github.com/pypa/setuptools_scm/issues/190, setuptools_scm includes ALL versioned files from
# the git repo into the sdist by default, and there is no easy way to provide an opt-out; # the git repo into the sdist by default, and there is no easy way to provide an opt-out;
@@ -41,21 +53,22 @@ if 'sdist' in sys.argv:
except ImportError: except ImportError:
pass pass
try:
# this is a workaround for being able to install the package from source without working from a git checkout
# it is needed for building succesfully on Heroku
from setuptools_scm import get_version
def read_req(req_file): version = get_version()
with open(os.path.join('requirements', req_file)) as req: version_kwargs = {'use_scm_version': True}
return [line for line in req.readlines() if line and not line.isspace()] except LookupError:
if 'sdist' in sys.argv or 'bdist_wheel' in sys.argv:
raise
rnd = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
with io.open('README.rst', encoding='utf-8') as readme: version_kwargs = {'version': '0.0.0.dummy+' + rnd}
description = readme.read()
requirements = ['djangorestframework>=3.7.0'] + read_req('base.txt')
requirements_validation = read_req('validation.txt')
setup( setup(
name='drf-yasg', name='drf-yasg',
use_scm_version=True,
packages=find_packages('src'), packages=find_packages('src'),
package_dir={'': 'src'}, package_dir={'': 'src'},
include_package_data=True, include_package_data=True,
@@ -72,6 +85,7 @@ setup(
author_email='cristi@cvjd.me', author_email='cristi@cvjd.me',
keywords='drf django django-rest-framework schema swagger openapi codegen swagger-codegen ' keywords='drf django django-rest-framework schema swagger openapi codegen swagger-codegen '
'documentation drf-yasg django-rest-swagger drf-openapi', 'documentation drf-yasg django-rest-swagger drf-openapi',
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
classifiers=[ classifiers=[
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
@@ -91,4 +105,5 @@ setup(
'Topic :: Documentation', 'Topic :: Documentation',
'Topic :: Software Development :: Code Generators', 'Topic :: Software Development :: Code Generators',
], ],
**version_kwargs
) )
+1 -1
View File
@@ -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'
+18
View File
@@ -11,6 +11,7 @@ SWAGGER_DEFAULTS = {
'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.HiddenFieldInspector',
'drf_yasg.inspectors.SimpleFieldInspector', 'drf_yasg.inspectors.SimpleFieldInspector',
'drf_yasg.inspectors.StringDefaultFieldInspector', 'drf_yasg.inspectors.StringDefaultFieldInspector',
], ],
@@ -22,12 +23,16 @@ SWAGGER_DEFAULTS = {
'drf_yasg.inspectors.CoreAPICompatInspector', 'drf_yasg.inspectors.CoreAPICompatInspector',
], ],
'DEFAULT_INFO': None,
'DEFAULT_API_URL': None,
'USE_SESSION_AUTH': True, 'USE_SESSION_AUTH': True,
'SECURITY_DEFINITIONS': { 'SECURITY_DEFINITIONS': {
'basic': { 'basic': {
'type': 'basic' 'type': 'basic'
} }
}, },
'SECURITY_REQUIREMENTS': None,
'LOGIN_URL': getattr(settings, 'LOGIN_URL', None), 'LOGIN_URL': getattr(settings, 'LOGIN_URL', None),
'LOGOUT_URL': getattr(settings, 'LOGOUT_URL', None), 'LOGOUT_URL': getattr(settings, 'LOGOUT_URL', None),
'VALIDATOR_URL': '', 'VALIDATOR_URL': '',
@@ -39,6 +44,18 @@ SWAGGER_DEFAULTS = {
'SHOW_EXTENSIONS': True, 'SHOW_EXTENSIONS': True,
'DEFAULT_MODEL_RENDERING': 'model', 'DEFAULT_MODEL_RENDERING': 'model',
'DEFAULT_MODEL_DEPTH': 3, 'DEFAULT_MODEL_DEPTH': 3,
'OAUTH2_REDIRECT_URL': None,
'OAUTH2_CONFIG': {},
'SUPPORTED_SUBMIT_METHODS': [
'get',
'put',
'post',
'delete',
'options',
'head',
'patch',
'trace'
],
} }
REDOC_DEFAULTS = { REDOC_DEFAULTS = {
@@ -53,6 +70,7 @@ IMPORT_STRINGS = [
'DEFAULT_FIELD_INSPECTORS', 'DEFAULT_FIELD_INSPECTORS',
'DEFAULT_FILTER_INSPECTORS', 'DEFAULT_FILTER_INSPECTORS',
'DEFAULT_PAGINATOR_INSPECTORS', 'DEFAULT_PAGINATOR_INSPECTORS',
'DEFAULT_INFO',
] ]
+16 -10
View File
@@ -1,32 +1,32 @@
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
from .app_settings import swagger_settings
from .errors import SwaggerValidationError from .errors import SwaggerValidationError
def _validate_flex(spec, codec): def _validate_flex(spec):
from flex.core import parse as validate_flex from flex.core import parse as validate_flex
from flex.exceptions import ValidationError from flex.exceptions import ValidationError
try: try:
validate_flex(spec) validate_flex(spec)
except ValidationError as ex: except ValidationError as ex:
raise_from(SwaggerValidationError(str(ex), 'flex', spec, codec), ex) raise_from(SwaggerValidationError(str(ex)), ex)
def _validate_swagger_spec_validator(spec, codec): def _validate_swagger_spec_validator(spec):
from swagger_spec_validator.validator20 import validate_spec as validate_ssv from swagger_spec_validator.validator20 import validate_spec as validate_ssv
from swagger_spec_validator.common import SwaggerValidationError as SSVErr from swagger_spec_validator.common import SwaggerValidationError as SSVErr
try: try:
validate_ssv(spec) validate_ssv(spec)
except SSVErr as ex: except SSVErr as ex:
raise_from(SwaggerValidationError(str(ex), 'swagger_spec_validator', spec, codec), ex) raise_from(SwaggerValidationError(str(ex)), ex)
#: #:
@@ -60,10 +60,17 @@ class _OpenAPICodec(object):
raise TypeError('Expected a `openapi.Swagger` instance') raise TypeError('Expected a `openapi.Swagger` instance')
spec = self.generate_swagger_object(document) spec = self.generate_swagger_object(document)
errors = {}
for validator in self.validators: for validator in self.validators:
# validate a deepcopy of the spec to prevent the validator from messing with it try:
# for example, swagger_spec_validator adds an x-scope property to all references # validate a deepcopy of the spec to prevent the validator from messing with it
VALIDATORS[validator](copy.deepcopy(spec), self) # for example, swagger_spec_validator adds an x-scope property to all references
VALIDATORS[validator](copy.deepcopy(spec))
except SwaggerValidationError as e:
errors[validator] = str(e)
if errors:
raise SwaggerValidationError("spec validation failed", errors, spec, self)
return force_bytes(self._dump_dict(spec)) return force_bytes(self._dump_dict(spec))
def encode_error(self, err): def encode_error(self, err):
@@ -86,7 +93,6 @@ class _OpenAPICodec(object):
:return: swagger spec as dict :return: swagger spec as dict
:rtype: OrderedDict :rtype: OrderedDict
""" """
swagger.security_definitions = swagger_settings.SECURITY_DEFINITIONS
return swagger.as_odict() return swagger.as_odict()
+2 -2
View File
@@ -3,9 +3,9 @@ class SwaggerError(Exception):
class SwaggerValidationError(SwaggerError): class SwaggerValidationError(SwaggerError):
def __init__(self, msg, validator_name, spec, source_codec, *args): def __init__(self, msg, errors=None, spec=None, source_codec=None, *args):
super(SwaggerValidationError, self).__init__(msg, *args) super(SwaggerValidationError, self).__init__(msg, *args)
self.validator_name = validator_name self.errors = errors
self.spec = spec self.spec = spec
self.source_codec = source_codec self.source_codec = source_codec
+158 -51
View File
@@ -1,24 +1,115 @@
import copy
import logging
import re import re
from collections import defaultdict, OrderedDict from collections import OrderedDict, defaultdict
import uritemplate import uritemplate
from coreapi.compat import urlparse
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.compat import URLPattern, URLResolver, get_original_route
from rest_framework.schemas.generators import EndpointEnumerator as _EndpointEnumerator
from rest_framework.schemas.generators import SchemaGenerator, endpoint_ordering
from rest_framework.schemas.inspectors import get_pk_description from rest_framework.schemas.inspectors import get_pk_description
from rest_framework.settings import api_settings as rest_framework_settings
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 .errors import SwaggerGenerationError
from .inspectors.field import get_basic_type_info, get_queryset_field
from .openapi import ReferenceResolver from .openapi import ReferenceResolver
from .utils import get_consumes, get_produces
logger = logging.getLogger(__name__)
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}') PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
class EndpointEnumerator(_EndpointEnumerator): class EndpointEnumerator(_EndpointEnumerator):
def __init__(self, patterns=None, urlconf=None, request=None):
super(EndpointEnumerator, self).__init__(patterns, urlconf)
self.request = request
def get_path_from_regex(self, path_regex): def get_path_from_regex(self, path_regex):
if path_regex.endswith(')'):
logger.warning("url pattern does not end in $ ('%s') - unexpected things might happen",
path_regex)
return self.unescape_path(super(EndpointEnumerator, self).get_path_from_regex(path_regex)) return self.unescape_path(super(EndpointEnumerator, self).get_path_from_regex(path_regex))
def should_include_endpoint(self, path, callback, app_name='', namespace='', url_name=None):
if not super(EndpointEnumerator, self).should_include_endpoint(path, callback):
return False
version = getattr(self.request, 'version', None)
versioning_class = getattr(callback.cls, 'versioning_class', None)
if versioning_class is not None and issubclass(versioning_class, versioning.NamespaceVersioning):
if version and version not in namespace.split(':'):
return False
if getattr(callback.cls, 'swagger_schema', object()) is None:
return False
return True
def replace_version(self, path, callback):
"""If ``request.version`` is not ``None`` and `callback` uses ``URLPathVersioning``, this function replaces
the ``version`` parameter in `path` with the actual version.
:param str path: the templated path
:param callback: the view callback
:rtype: str
"""
versioning_class = getattr(callback.cls, 'versioning_class', None)
if versioning_class is not None and issubclass(versioning_class, versioning.URLPathVersioning):
version = getattr(self.request, 'version', None)
if version:
version_param = getattr(versioning_class, 'version_param', 'version')
version_param = '{%s}' % version_param
if version_param not in path:
logger.info("view %s uses URLPathVersioning but URL %s has no param %s"
% (callback.cls, path, version_param))
path = path.replace(version_param, version)
return path
def get_api_endpoints(self, patterns=None, prefix='', app_name=None, namespace=None):
"""
Return a list of all available API endpoints by inspecting the URL conf.
Copied entirely from super.
"""
if patterns is None:
patterns = self.patterns
api_endpoints = []
for pattern in patterns:
path_regex = prefix + get_original_route(pattern)
if isinstance(pattern, URLPattern):
path = self.get_path_from_regex(path_regex)
callback = pattern.callback
url_name = pattern.name
if self.should_include_endpoint(path, callback, app_name or '', namespace or '', url_name):
path = self.replace_version(path, callback)
for method in self.get_allowed_methods(callback):
endpoint = (path, method, callback)
api_endpoints.append(endpoint)
elif isinstance(pattern, URLResolver):
nested_endpoints = self.get_api_endpoints(
patterns=pattern.url_patterns,
prefix=path_regex,
app_name="%s:%s" % (app_name, pattern.app_name) if app_name else pattern.app_name,
namespace="%s:%s" % (namespace, pattern.namespace) if namespace else pattern.namespace
)
api_endpoints.extend(nested_endpoints)
else:
logger.warning("unknown pattern type {}".format(type(pattern)))
api_endpoints = sorted(api_endpoints, key=endpoint_ordering)
return api_endpoints
def unescape(self, s): def unescape(self, s):
"""Unescape all backslash escapes from `s`. """Unescape all backslash escapes from `s`.
@@ -29,8 +120,8 @@ class EndpointEnumerator(_EndpointEnumerator):
return re.sub(r'\\(.)', r'\1', s) return re.sub(r'\\(.)', r'\1', s)
def unescape_path(self, path): def unescape_path(self, path):
"""Remove backslashes from all path components outside {parameters}. This is needed because """Remove backslashe escapes from all path components outside {parameters}. This is needed because
Django>=2.0 ``path()``/``RoutePattern`` aggresively escapes all non-parameter path components. ``simplify_regex`` does not handle this correctly - note however that this implementation is
**NOTE:** this might destructively affect some url regex patterns that contain metacharacters (e.g. \w, \d) **NOTE:** this might destructively affect some url regex patterns that contain metacharacters (e.g. \w, \d)
outside path parameter groups; if you are in this category, God help you outside path parameter groups; if you are in this category, God help you
@@ -59,12 +150,19 @@ 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=None, 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; if omitted, `info.default_version` will be used
:param str url: API :param str url: API scheme, host and port; if ``None`` is passed and ``DEFAULT_API_URL`` is not set, the url
will be inferred from the request made against the schema view, so you should generally not need to set
this parameter explicitly; if the empty string is passed, no host and scheme will be emitted
If `url` is not ``None`` or the empty string, it must be a scheme-absolute uri (i.e. starting with http://
or https://), and any path component is ignored;
See also: :ref:`documentation on base URL construction <custom-spec-base-url>`
: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
@@ -72,6 +170,18 @@ class OpenAPISchemaGenerator(object):
self._gen = SchemaGenerator(info.title, url, info.get('description', ''), patterns, urlconf) self._gen = SchemaGenerator(info.title, url, info.get('description', ''), patterns, urlconf)
self.info = info self.info = info
self.version = version self.version = version
self.consumes = []
self.produces = []
if url is None and swagger_settings.DEFAULT_API_URL is not None:
url = swagger_settings.DEFAULT_API_URL
if url:
parsed_url = urlparse.urlparse(url)
if parsed_url.scheme not in ('http', 'https') or not parsed_url.netloc:
raise SwaggerGenerationError("`url` must be an absolute HTTP(S) url")
if parsed_url.path:
logger.warning("path component of api base URL %s is ignored; use FORCE_SCRIPT_NAME instead" % url)
@property @property
def url(self): def url(self):
@@ -88,17 +198,24 @@ class OpenAPISchemaGenerator(object):
:rtype: openapi.Swagger :rtype: openapi.Swagger
""" """
endpoints = self.get_endpoints(request) endpoints = self.get_endpoints(request)
endpoints = self.replace_version(endpoints, request)
components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS) components = ReferenceResolver(openapi.SCHEMA_DEFINITIONS)
paths = self.get_paths(endpoints, components, request, public) self.consumes = get_consumes(rest_framework_settings.DEFAULT_PARSER_CLASSES)
self.produces = get_produces(rest_framework_settings.DEFAULT_RENDERER_CLASSES)
paths, prefix = self.get_paths(endpoints, components, request, public)
security_definitions = swagger_settings.SECURITY_DEFINITIONS
security_requirements = swagger_settings.SECURITY_REQUIREMENTS
if security_requirements is None:
security_requirements = [{security_scheme: [] for security_scheme in swagger_settings.SECURITY_DEFINITIONS}]
url = self.url url = self.url
if not url and request is not None: if url is None and request is not None:
url = request.build_absolute_uri() url = request.build_absolute_uri()
return openapi.Swagger( return openapi.Swagger(
info=self.info, paths=paths, info=self.info, paths=paths, consumes=self.consumes or None, produces=self.produces or None,
_url=url, _version=self.version, **dict(components) security_definitions=security_definitions, security=security_requirements,
_url=url, _prefix=prefix, _version=self.version, **dict(components)
) )
def create_view(self, callback, method, request=None): def create_view(self, callback, method, request=None):
@@ -119,30 +236,6 @@ class OpenAPISchemaGenerator(object):
setattr(view_method.__func__, '_swagger_auto_schema', overrides) setattr(view_method.__func__, '_swagger_auto_schema', overrides)
return view return view
def replace_version(self, endpoints, request):
"""If ``request.version`` is not ``None``, replace the version parameter in the path of any endpoints using
``URLPathVersioning`` as a versioning class.
:param dict endpoints: endpoints as returned by :meth:`.get_endpoints`
:param Request request: the request made against the schema view
:return: endpoints with modified paths
"""
version = getattr(request, 'version', None)
if version is None:
return endpoints
new_endpoints = {}
for path, endpoint in endpoints.items():
view_cls = endpoint[0]
versioning_class = getattr(view_cls, 'versioning_class', None)
version_param = getattr(versioning_class, 'version_param', 'version')
if versioning_class is not None and issubclass(versioning_class, versioning.URLPathVersioning):
path = path.replace('{%s}' % version_param, version)
new_endpoints[path] = endpoint
return new_endpoints
def get_endpoints(self, request): def get_endpoints(self, request):
"""Iterate over all the registered endpoints in the API and return a fake view with the right parameters. """Iterate over all the registered endpoints in the API and return a fake view with the right parameters.
@@ -150,7 +243,7 @@ class OpenAPISchemaGenerator(object):
:return: {path: (view_class, list[(http_method, view_instance)]) :return: {path: (view_class, list[(http_method, view_instance)])
:rtype: dict :rtype: dict
""" """
enumerator = self.endpoint_enumerator_class(self._gen.patterns, self._gen.urlconf) enumerator = self.endpoint_enumerator_class(self._gen.patterns, self._gen.urlconf, request=request)
endpoints = enumerator.get_api_endpoints() endpoints = enumerator.get_api_endpoints()
view_paths = defaultdict(list) view_paths = defaultdict(list)
@@ -206,14 +299,16 @@ class OpenAPISchemaGenerator(object):
:param ReferenceResolver components: resolver/container for Swagger References :param ReferenceResolver components: resolver/container for Swagger References
:param Request request: the request made against the schema view; can be None :param Request request: the request made against the schema view; can be None
:param bool public: if True, all endpoints are included regardless of access through `request` :param bool public: if True, all endpoints are included regardless of access through `request`
:rtype: openapi.Paths :returns: the :class:`.Paths` object and the longest common path prefix, as a 2-tuple
:rtype: tuple[openapi.Paths,str]
""" """
if not endpoints: if not endpoints:
return openapi.Paths(paths={}) return openapi.Paths(paths={}), ''
prefix = self.determine_path_prefix(list(endpoints.keys())) or ''
assert '{' not in prefix, "base path cannot be templated in swagger 2.0"
prefix = self.determine_path_prefix(list(endpoints.keys()))
paths = OrderedDict() paths = OrderedDict()
for path, (view_cls, methods) in sorted(endpoints.items()): for path, (view_cls, methods) in sorted(endpoints.items()):
operations = {} operations = {}
for method, view in methods: for method, view in methods:
@@ -223,9 +318,14 @@ class OpenAPISchemaGenerator(object):
operations[method.lower()] = self.get_operation(view, path, prefix, method, components, request) operations[method.lower()] = self.get_operation(view, path, prefix, method, components, request)
if operations: if operations:
paths[path] = self.get_path_item(path, view_cls, operations) # since the common prefix is used as the API basePath, it must be stripped
# from individual paths when writing them into the swagger document
path_suffix = path[len(prefix):]
if not path_suffix.startswith('/'):
path_suffix = '/' + path_suffix
paths[path_suffix] = self.get_path_item(path, view_cls, operations)
return openapi.Paths(paths=paths) return openapi.Paths(paths=paths), prefix
def get_operation(self, view, path, prefix, method, components, request): def get_operation(self, view, path, prefix, method, components, request):
"""Get an :class:`.Operation` for the given API endpoint (path, method). This method delegates to """Get an :class:`.Operation` for the given API endpoint (path, method). This method delegates to
@@ -240,7 +340,6 @@ class OpenAPISchemaGenerator(object):
:param Request request: the request made against the schema view; can be None :param Request request: the request made against the schema view; can be None
:rtype: openapi.Operation :rtype: openapi.Operation
""" """
operation_keys = self.get_operation_keys(path[len(prefix):], method, view) operation_keys = self.get_operation_keys(path[len(prefix):], method, view)
overrides = self.get_overrides(view, method) overrides = self.get_overrides(view, method)
@@ -252,8 +351,16 @@ class OpenAPISchemaGenerator(object):
# 3. on the swagger_auto_schema decorator # 3. on the swagger_auto_schema decorator
view_inspector_cls = overrides.get('auto_schema', view_inspector_cls) view_inspector_cls = overrides.get('auto_schema', view_inspector_cls)
if view_inspector_cls is None:
return None
view_inspector = view_inspector_cls(view, path, method, components, request, overrides) view_inspector = view_inspector_cls(view, path, method, components, request, overrides)
return view_inspector.get_operation(operation_keys) operation = view_inspector.get_operation(operation_keys)
if 'consumes' in operation and set(operation.consumes) == set(self.consumes):
del operation.consumes
if 'produces' in operation and set(operation.produces) == set(self.produces):
del operation.produces
return operation
def get_path_item(self, path, view_cls, operations): def get_path_item(self, path, view_cls, operations):
"""Get a :class:`.PathItem` object that describes the parameters and operations related to a single path in the """Get a :class:`.PathItem` object that describes the parameters and operations related to a single path in the
@@ -282,7 +389,7 @@ class OpenAPISchemaGenerator(object):
if method in overrides: if method in overrides:
overrides = overrides[method] overrides = overrides[method]
return overrides return copy.deepcopy(overrides)
def get_path_parameters(self, path, view_cls): def get_path_parameters(self, path, view_cls):
"""Return a list of Parameter instances corresponding to any templated path variables. """Return a list of Parameter instances corresponding to any templated path variables.
@@ -299,12 +406,12 @@ class OpenAPISchemaGenerator(object):
for variable in uritemplate.variables(path): for variable in uritemplate.variables(path):
model, model_field = get_queryset_field(queryset, variable) model, model_field = get_queryset_field(queryset, variable)
attrs = get_basic_type_info(model_field) or {'type': openapi.TYPE_STRING} attrs = get_basic_type_info(model_field) or {'type': openapi.TYPE_STRING}
if hasattr(view_cls, 'lookup_value_regex') and getattr(view_cls, 'lookup_field', None) == variable: if getattr(view_cls, 'lookup_field', None) == variable and attrs['type'] == openapi.TYPE_STRING:
attrs['pattern'] = view_cls.lookup_value_regex attrs['pattern'] = getattr(view_cls, 'lookup_value_regex', attrs.get('pattern', None))
if model_field and model_field.help_text: if model_field and getattr(model_field, 'help_text', False):
description = force_text(model_field.help_text) description = force_text(model_field.help_text)
elif model_field and model_field.primary_key: elif model_field and getattr(model_field, 'primary_key', False):
description = get_pk_description(model, model_field) description = get_pk_description(model, model_field)
else: else:
description = None description = None
+6 -9
View File
@@ -1,17 +1,14 @@
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 (
CamelCaseJSONFilter, ChoiceFieldInspector, DictFieldInspector, FileFieldInspector, HiddenFieldInspector,
InlineSerializerInspector, ReferencingSerializerInspector, RelatedFieldInspector, SimpleFieldInspector, InlineSerializerInspector, ReferencingSerializerInspector, RelatedFieldInspector, SimpleFieldInspector,
FileFieldInspector, ChoiceFieldInspector, DictFieldInspector, StringDefaultFieldInspector, 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
@@ -28,7 +25,7 @@ __all__ = [
# field inspectors # field inspectors
'InlineSerializerInspector', 'ReferencingSerializerInspector', 'RelatedFieldInspector', 'SimpleFieldInspector', 'InlineSerializerInspector', 'ReferencingSerializerInspector', 'RelatedFieldInspector', 'SimpleFieldInspector',
'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector', 'StringDefaultFieldInspector', 'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector', 'StringDefaultFieldInspector',
'CamelCaseJSONFilter', 'CamelCaseJSONFilter', 'HiddenFieldInspector',
# view inspectors # view inspectors
'SwaggerAutoSchema', 'SwaggerAutoSchema',
+8 -17
View File
@@ -3,11 +3,10 @@ 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 .. import openapi from .. import openapi
from ..utils import is_list_view from ..utils import decimal_as_float, is_list_view
#: Sentinel value that inspectors must return to signal that they do not know how to handle an object #: Sentinel value that inspectors must return to signal that they do not know how to handle an object
NotHandled = object() NotHandled = object()
@@ -182,7 +181,7 @@ class FieldInspector(BaseInspector):
- arguments specified by the ``kwargs`` parameter of :meth:`._get_partial_types` - arguments specified by the ``kwargs`` parameter of :meth:`._get_partial_types`
- ``instance_kwargs`` passed to the constructor function - ``instance_kwargs`` passed to the constructor function
- ``title``, ``description``, ``required``, ``default`` and ``read_only`` inferred from the field, - ``title``, ``description``, ``required`` and ``default`` inferred from the field,
where appropriate where appropriate
If ``existing_object`` is not ``None``, it is updated instead of creating a new object. If ``existing_object`` is not ``None``, it is updated instead of creating a new object.
@@ -225,6 +224,8 @@ class FieldInspector(BaseInspector):
# JSON roundtrip ensures that the value is valid JSON; # JSON roundtrip ensures that the value is valid JSON;
# for example, sets and tuples get transformed into lists # for example, sets and tuples get transformed into lists
default = json.loads(json.dumps(default, cls=encoders.JSONEncoder)) default = json.loads(json.dumps(default, cls=encoders.JSONEncoder))
if decimal_as_float(field):
default = float(default)
except Exception: # pragma: no cover except Exception: # pragma: no cover
logger.warning("'default' on schema for %s will not be set because " logger.warning("'default' on schema for %s will not be set because "
"to_representation raised an exception", field, exc_info=True) "to_representation raised an exception", field, exc_info=True)
@@ -233,12 +234,8 @@ class FieldInspector(BaseInspector):
if default is not None: if default is not None:
instance_kwargs['default'] = default instance_kwargs['default'] = default
if 'read_only' not in instance_kwargs and swagger_object_type == openapi.Schema: if instance_kwargs.get('type', None) != openapi.TYPE_ARRAY:
# TODO: read_only is only relevant for schema `properties` - should not be generated in other cases instance_kwargs.setdefault('title', title)
if field.read_only:
instance_kwargs['read_only'] = True
instance_kwargs.setdefault('title', title)
instance_kwargs.setdefault('description', description) instance_kwargs.setdefault('description', description)
instance_kwargs.update(kwargs) instance_kwargs.update(kwargs)
@@ -327,9 +324,6 @@ class ViewInspector(BaseInspector):
if self.method.lower() not in ["get", "delete"]: if self.method.lower() not in ["get", "delete"]:
return False return False
if not isinstance(self.view, GenericViewSet):
return True
return is_list_view(self.path, self.method, self.view) return is_list_view(self.path, self.method, self.view)
def get_filter_parameters(self): def get_filter_parameters(self):
@@ -351,10 +345,7 @@ class ViewInspector(BaseInspector):
:rtype: bool :rtype: bool
""" """
if not hasattr(self.view, 'paginator'): if not getattr(self.view, 'paginator', None):
return False
if self.view.paginator is None:
return False return False
if self.method.lower() != 'get': if self.method.lower() != 'get':
+105 -29
View File
@@ -1,15 +1,19 @@
import logging
import operator import operator
from collections import OrderedDict from collections import OrderedDict
from decimal import Decimal
from django.core import validators from django.core import validators
from django.db import models 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 decimal_as_float, filter_none
from .base import FieldInspector, NotHandled, SerializerInspector
logger = logging.getLogger(__name__)
class InlineSerializerInspector(SerializerInspector): class InlineSerializerInspector(SerializerInspector):
@@ -53,27 +57,44 @@ class InlineSerializerInspector(SerializerInspector):
serializer = field serializer = field
serializer_meta = getattr(serializer, 'Meta', None) serializer_meta = getattr(serializer, 'Meta', None)
serializer_name = type(serializer).__name__
if hasattr(serializer_meta, 'ref_name'): if hasattr(serializer_meta, 'ref_name'):
ref_name = serializer_meta.ref_name ref_name = serializer_meta.ref_name
elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
logger.debug("Forcing inline output for ModelSerializer named 'NestedSerializer': " + str(serializer))
ref_name = None
else: else:
ref_name = type(serializer).__name__ ref_name = serializer_name
if ref_name.endswith('Serializer'): if ref_name.endswith('Serializer'):
ref_name = ref_name[:-len('Serializer')] ref_name = ref_name[:-len('Serializer')]
def make_schema_definition(): def make_schema_definition():
properties = OrderedDict() properties = OrderedDict()
required = [] required = []
for key, value in serializer.fields.items(): for property_name, child in serializer.fields.items():
key = self.get_property_name(key) property_name = self.get_property_name(property_name)
properties[key] = self.probe_field_inspectors(value, ChildSwaggerType, use_references) prop_kwargs = {
if value.required: 'read_only': child.read_only or None
required.append(key) }
prop_kwargs = filter_none(prop_kwargs)
return SwaggerType( properties[property_name] = self.probe_field_inspectors(
child, ChildSwaggerType, use_references, **prop_kwargs
)
if child.required:
required.append(property_name)
result = SwaggerType(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
properties=properties, properties=properties,
required=required or None, required=required or None,
) )
if not ref_name and 'title' in result:
# on an inline model, the title is derived from the field name
# but is visually displayed like the model name, which is confusing
# it is better to just remove title from inline models
del result.title
return result
if not ref_name or not use_references: if not ref_name or not use_references:
return make_schema_definition() return make_schema_definition()
@@ -140,7 +161,11 @@ def get_related_model(model, source):
:return: related model or ``None`` :return: related model or ``None``
""" """
try: try:
return getattr(model, source).rel.related_model descriptor = getattr(model, source)
try:
return descriptor.rel.related_model
except Exception:
return descriptor.field.remote_field.model
except Exception: # pragma: no cover except Exception: # pragma: no cover
return None return None
@@ -211,7 +236,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)
@@ -244,18 +276,29 @@ def find_limits(field):
if isinstance(field, field_class) if isinstance(field, field_class)
] ]
if isinstance(field, serializers.DecimalField) and not decimal_as_float(field):
return limits
for validator in field.validators: for validator in field.validators:
if not hasattr(validator, 'limit_value'): if not hasattr(validator, 'limit_value'):
continue continue
limit_value = validator.limit_value
if isinstance(limit_value, Decimal) and decimal_as_float(field):
limit_value = float(limit_value)
for validator_class, attr, improves in applicable_limits: for validator_class, attr, improves in applicable_limits:
if isinstance(validator, validator_class): if isinstance(validator, validator_class):
if attr not in limits or improves(validator.limit_value, limits[attr]): if attr not in limits or improves(limit_value, limits[attr]):
limits[attr] = validator.limit_value limits[attr] = limit_value
return OrderedDict(sorted(limits.items())) return OrderedDict(sorted(limits.items()))
def decimal_field_type(field):
return openapi.TYPE_NUMBER if decimal_as_float(field) else openapi.TYPE_STRING
model_field_to_basic_type = [ model_field_to_basic_type = [
(models.AutoField, (openapi.TYPE_INTEGER, None)), (models.AutoField, (openapi.TYPE_INTEGER, None)),
(models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)), (models.BinaryField, (openapi.TYPE_STRING, openapi.FORMAT_BINARY)),
@@ -263,7 +306,7 @@ model_field_to_basic_type = [
(models.NullBooleanField, (openapi.TYPE_BOOLEAN, None)), (models.NullBooleanField, (openapi.TYPE_BOOLEAN, None)),
(models.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)), (models.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)),
(models.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)), (models.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
(models.DecimalField, (openapi.TYPE_NUMBER, None)), (models.DecimalField, (decimal_field_type, openapi.FORMAT_DECIMAL)),
(models.DurationField, (openapi.TYPE_INTEGER, None)), (models.DurationField, (openapi.TYPE_INTEGER, None)),
(models.FloatField, (openapi.TYPE_NUMBER, None)), (models.FloatField, (openapi.TYPE_NUMBER, None)),
(models.IntegerField, (openapi.TYPE_INTEGER, None)), (models.IntegerField, (openapi.TYPE_INTEGER, None)),
@@ -286,9 +329,11 @@ serializer_field_to_basic_type = [
(serializers.UUIDField, (openapi.TYPE_STRING, openapi.FORMAT_UUID)), (serializers.UUIDField, (openapi.TYPE_STRING, openapi.FORMAT_UUID)),
(serializers.RegexField, (openapi.TYPE_STRING, None)), (serializers.RegexField, (openapi.TYPE_STRING, None)),
(serializers.CharField, (openapi.TYPE_STRING, None)), (serializers.CharField, (openapi.TYPE_STRING, None)),
((serializers.BooleanField, serializers.NullBooleanField), (openapi.TYPE_BOOLEAN, None)), (serializers.BooleanField, (openapi.TYPE_BOOLEAN, None)),
(serializers.NullBooleanField, (openapi.TYPE_BOOLEAN, None)),
(serializers.IntegerField, (openapi.TYPE_INTEGER, None)), (serializers.IntegerField, (openapi.TYPE_INTEGER, None)),
((serializers.FloatField, serializers.DecimalField), (openapi.TYPE_NUMBER, None)), (serializers.FloatField, (openapi.TYPE_NUMBER, None)),
(serializers.DecimalField, (decimal_field_type, openapi.FORMAT_DECIMAL)),
(serializers.DurationField, (openapi.TYPE_NUMBER, None)), # ? (serializers.DurationField, (openapi.TYPE_NUMBER, None)), # ?
(serializers.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)), (serializers.DateField, (openapi.TYPE_STRING, openapi.FORMAT_DATE)),
(serializers.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)), (serializers.DateTimeField, (openapi.TYPE_STRING, openapi.FORMAT_DATETIME)),
@@ -312,13 +357,18 @@ def get_basic_type_info(field):
for field_class, type_format in basic_type_info: for field_class, type_format in basic_type_info:
if isinstance(field, field_class): if isinstance(field, field_class):
swagger_type, format = type_format swagger_type, format = type_format
if callable(swagger_type):
swagger_type = swagger_type(field)
if callable(format): if callable(format):
format = format(field) format = format(field)
break break
else: # pragma: no cover else: # pragma: no cover
return None return None
pattern = find_regex(field) if format in (None, openapi.FORMAT_SLUG) else None pattern = None
if swagger_type == openapi.TYPE_STRING and format in (None, openapi.FORMAT_SLUG):
pattern = find_regex(field)
limits = find_limits(field) limits = find_limits(field)
result = OrderedDict([ result = OrderedDict([
@@ -351,16 +401,27 @@ class ChoiceFieldInspector(FieldInspector):
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
if isinstance(field, serializers.MultipleChoiceField): if isinstance(field, serializers.ChoiceField):
return SwaggerType( enum_type = openapi.TYPE_STRING
type=openapi.TYPE_ARRAY,
items=ChildSwaggerType( # for ModelSerializer, try to infer the type from the associated model field
type=openapi.TYPE_STRING, serializer = get_parent_serializer(field)
enum=list(field.choices.keys()) if isinstance(serializer, serializers.ModelSerializer):
model = getattr(getattr(serializer, 'Meta'), 'model')
model_field = get_model_field(model, field.source)
if model_field:
enum_type = get_basic_type_info(model_field).get('type', enum_type)
if isinstance(field, serializers.MultipleChoiceField):
return SwaggerType(
type=openapi.TYPE_ARRAY,
items=ChildSwaggerType(
type=enum_type,
enum=list(field.choices.keys())
)
) )
)
elif isinstance(field, serializers.ChoiceField): return SwaggerType(type=enum_type, enum=list(field.choices.keys()))
return SwaggerType(type=openapi.TYPE_STRING, enum=list(field.choices.keys()))
return NotHandled return NotHandled
@@ -408,11 +469,21 @@ class DictFieldInspector(FieldInspector):
return NotHandled return NotHandled
class HiddenFieldInspector(FieldInspector):
"""Hide ``HiddenField``."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
if isinstance(field, serializers.HiddenField):
return None
return NotHandled
class StringDefaultFieldInspector(FieldInspector): class StringDefaultFieldInspector(FieldInspector):
"""For otherwise unhandled fields, return them as plain :data:`.TYPE_STRING` objects.""" """For otherwise unhandled fields, return them as plain :data:`.TYPE_STRING` objects."""
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): # pragma: no cover def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): # pragma: no cover
# TODO unhandled fields: TimeField HiddenField JSONField # TODO unhandled fields: TimeField JSONField
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs) SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
return SwaggerType(type=openapi.TYPE_STRING) return SwaggerType(type=openapi.TYPE_STRING)
@@ -423,6 +494,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,9 +516,13 @@ 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 (
or any(issubclass(renderer, CamelCaseJSONRenderer) for renderer in self.view.renderer_classes) any(issubclass(parser, CamelCaseJSONParser) for parser in self.view.parser_classes) or
any(issubclass(renderer, CamelCaseJSONRenderer) for renderer in self.view.renderer_classes)
)
def process_result(self, result, method_name, obj, **kwargs): def process_result(self, result, method_name, obj, **kwargs):
if isinstance(result, openapi.Schema.OR_REF) and self.is_camel_case(): if isinstance(result, openapi.Schema.OR_REF) and self.is_camel_case():
+2 -2
View File
@@ -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):
+32 -12
View File
@@ -4,10 +4,13 @@ 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, get_consumes, get_produces, guess_response_status, is_list_view, no_body,
param_list_to_odict
)
from .base import ViewInspector
class SwaggerAutoSchema(ViewInspector): class SwaggerAutoSchema(ViewInspector):
@@ -18,6 +21,7 @@ class SwaggerAutoSchema(ViewInspector):
def get_operation(self, operation_keys): def get_operation(self, operation_keys):
consumes = self.get_consumes() consumes = self.get_consumes()
produces = self.get_produces()
body = self.get_request_body_parameters(consumes) body = self.get_request_body_parameters(consumes)
query = self.get_query_parameters() query = self.get_query_parameters()
@@ -27,6 +31,8 @@ class SwaggerAutoSchema(ViewInspector):
operation_id = self.get_operation_id(operation_keys) operation_id = self.get_operation_id(operation_keys)
description = self.get_description() description = self.get_description()
security = self.get_security()
assert security is None or isinstance(security, list), "security must be a list of securiy requirement objects"
tags = self.get_tags(operation_keys) tags = self.get_tags(operation_keys)
responses = self.get_responses() responses = self.get_responses()
@@ -37,7 +43,9 @@ class SwaggerAutoSchema(ViewInspector):
responses=responses, responses=responses,
parameters=parameters, parameters=parameters,
consumes=consumes, consumes=consumes,
produces=produces,
tags=tags, tags=tags,
security=security
) )
def get_request_body_parameters(self, consumes): def get_request_body_parameters(self, consumes):
@@ -88,8 +96,8 @@ class SwaggerAutoSchema(ViewInspector):
if body_override is no_body: if body_override is no_body:
return None return None
if self.method not in self.body_methods: if self.method not in self.body_methods:
raise SwaggerGenerationError("request_body can only be applied to PUT, PATCH or POST views; " raise SwaggerGenerationError("request_body can only be applied to (" + ','.join(self.body_methods) +
"are you looking for query_serializer or manual_parameters?") "); are you looking for query_serializer or manual_parameters?")
if isinstance(body_override, openapi.Schema.OR_REF): if isinstance(body_override, openapi.Schema.OR_REF):
return body_override return body_override
return force_serializer_instance(body_override) return force_serializer_instance(body_override)
@@ -162,9 +170,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 ''
@@ -215,7 +221,7 @@ class SwaggerAutoSchema(ViewInspector):
) )
elif isinstance(serializer, openapi.Response): elif isinstance(serializer, openapi.Response):
response = serializer response = serializer
if not isinstance(response.schema, openapi.Schema.OR_REF): if hasattr(response, 'schema') and not isinstance(response.schema, openapi.Schema.OR_REF):
serializer = force_serializer_instance(response.schema) serializer = force_serializer_instance(response.schema)
response.schema = self.serializer_to_schema(serializer) response.schema = self.serializer_to_schema(serializer)
elif isinstance(serializer, openapi.Schema.OR_REF): elif isinstance(serializer, openapi.Schema.OR_REF):
@@ -288,6 +294,16 @@ class SwaggerAutoSchema(ViewInspector):
description = self._sch.get_description(self.path, self.method) description = self._sch.get_description(self.path, self.method)
return description return description
def get_security(self):
"""Return a list of security requirements for this operation.
Returning an empty list marks the endpoint as unauthenticated (i.e. removes all accepted
authentication schemes). Returning ``None`` will inherit the top-level secuirty requirements.
:return: security requirements
:rtype: list[dict[str,list[str]]]"""
return self.overrides.get('security', None)
def get_tags(self, operation_keys): def get_tags(self, operation_keys):
"""Get a list of tags for this operation. Tags determine how operations relate with each other, and in the UI """Get a list of tags for this operation. Tags determine how operations relate with each other, and in the UI
each tag will show as a group containing the operations that use it. each tag will show as a group containing the operations that use it.
@@ -303,7 +319,11 @@ class SwaggerAutoSchema(ViewInspector):
:rtype: list[str] :rtype: list[str]
""" """
media_types = [parser.media_type for parser in getattr(self.view, 'parser_classes', [])] return get_consumes(getattr(self.view, 'parser_classes', []))
if all(is_form_media_type(encoding) for encoding in media_types):
return media_types def get_produces(self):
return media_types[:1] """Return the MIME types this endpoint can produce.
:rtype: list[str]
"""
return get_produces(getattr(self.view, 'renderer_classes', []))
View File
@@ -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 and scheme 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)
+1 -1
View File
@@ -13,7 +13,7 @@ class SwaggerExceptionMiddleware(object):
def process_exception(self, request, exception): def process_exception(self, request, exception):
if isinstance(exception, SwaggerValidationError): if isinstance(exception, SwaggerValidationError):
err = {'errors': {exception.validator_name: str(exception)}} err = {'errors': exception.errors, 'message': str(exception)}
codec = exception.source_codec codec = exception.source_codec
if isinstance(codec, _OpenAPICodec): if isinstance(codec, _OpenAPICodec):
err = codec.encode_error(err) err = codec.encode_error(err)
+78 -14
View File
@@ -2,6 +2,7 @@ import re
from collections import OrderedDict from collections import OrderedDict
from coreapi.compat import urlparse from coreapi.compat import urlparse
from django.urls import get_script_prefix
from inflection import camelize from inflection import camelize
from .utils import filter_none from .utils import filter_none
@@ -34,6 +35,7 @@ FORMAT_URI = "uri" #:
# pulled out of my ass # pulled out of my ass
FORMAT_UUID = "uuid" #: FORMAT_UUID = "uuid" #:
FORMAT_SLUG = "slug" #: FORMAT_SLUG = "slug" #:
FORMAT_DECIMAL = "decimal"
IN_BODY = 'body' #: IN_BODY = 'body' #:
IN_PATH = 'path' #: IN_PATH = 'path' #:
@@ -210,12 +212,19 @@ class Info(SwaggerDict):
class Swagger(SwaggerDict): class Swagger(SwaggerDict):
def __init__(self, info=None, _url=None, _version=None, paths=None, definitions=None, **extra): def __init__(self, info=None, _url=None, _prefix=None, _version=None, consumes=None, produces=None,
security_definitions=None, security=None, paths=None, definitions=None, **extra):
"""Root Swagger object. """Root Swagger object.
:param .Info info: info object :param .Info info: info object
:param str _url: URL used for guessing the API host, scheme and basepath :param str _url: URL used for setting the API host and scheme
:param str _prefix: api path prefix to use in setting basePath; this will be appended to the wsgi
SCRIPT_NAME prefix or Django's FORCE_SCRIPT_NAME if applicable
:param str _version: version string to override Info :param str _version: version string to override Info
:param list[dict] security_definitions: list of supported authentication mechanisms
:param list[dict] security: authentication mechanisms accepted by default; can be overriden in Operation
:param list[str] consumes: consumed MIME types; can be overriden in Operation
:param list[str] produces: produced MIME types; can be overriden in Operation
:param .Paths paths: paths object :param .Paths paths: paths object
:param dict[str,.Schema] definitions: named models :param dict[str,.Schema] definitions: named models
""" """
@@ -226,16 +235,43 @@ class Swagger(SwaggerDict):
if _url: if _url:
url = urlparse.urlparse(_url) url = urlparse.urlparse(_url)
if url.netloc: assert url.netloc and url.scheme, "if given, url must have both schema and netloc"
self.host = url.netloc self.host = url.netloc
if url.scheme: self.schemes = [url.scheme]
self.schemes = [url.scheme]
self.base_path = '/'
self.base_path = self.get_base_path(get_script_prefix(), _prefix)
self.consumes = consumes
self.produces = produces
self.security_definitions = filter_none(security_definitions)
self.security = filter_none(security)
self.paths = paths self.paths = paths
self.definitions = filter_none(definitions) self.definitions = filter_none(definitions)
self._insert_extras__() self._insert_extras__()
@classmethod
def get_base_path(cls, script_prefix, api_prefix):
"""Determine an appropriate value for ``basePath`` based on the SCRIPT_NAME and the api common prefix.
:param str script_prefix: script prefix as defined by django ``get_script_prefix``
:param str api_prefix: api common prefix
:return: joined base path
"""
# avoid double slash when joining script_name with api_prefix
if script_prefix and script_prefix.endswith('/'):
script_prefix = script_prefix[:-1]
if not api_prefix.startswith('/'):
api_prefix = '/' + api_prefix
base_path = script_prefix + api_prefix
# ensure that the base path has a leading slash and no trailing slash
if base_path and base_path.endswith('/'):
base_path = base_path[:-1]
if not base_path.startswith('/'):
base_path = '/' + base_path
return base_path
class Paths(SwaggerDict): class Paths(SwaggerDict):
def __init__(self, paths, **extra): def __init__(self, paths, **extra):
@@ -278,8 +314,8 @@ class PathItem(SwaggerDict):
class Operation(SwaggerDict): class Operation(SwaggerDict):
def __init__(self, operation_id, responses, parameters=None, consumes=None, def __init__(self, operation_id, responses, parameters=None, consumes=None, produces=None, summary=None,
produces=None, summary=None, description=None, tags=None, **extra): description=None, tags=None, security=None, **extra):
"""Information about an API operation (path + http method combination) """Information about an API operation (path + http method combination)
:param str operation_id: operation ID, should be unique across all operations :param str operation_id: operation ID, should be unique across all operations
@@ -290,6 +326,7 @@ class Operation(SwaggerDict):
:param str summary: operation summary; should be < 120 characters :param str summary: operation summary; should be < 120 characters
:param str description: operation description; can be of any length and supports markdown :param str description: operation description; can be of any length and supports markdown
:param list[str] tags: operation tags :param list[str] tags: operation tags
:param list[dict[str,list[str]]] security: list of security requirements
""" """
super(Operation, self).__init__(**extra) super(Operation, self).__init__(**extra)
self.operation_id = operation_id self.operation_id = operation_id
@@ -300,6 +337,7 @@ class Operation(SwaggerDict):
self.consumes = filter_none(consumes) self.consumes = filter_none(consumes)
self.produces = filter_none(produces) self.produces = filter_none(produces)
self.tags = filter_none(tags) self.tags = filter_none(tags)
self.security = filter_none(security)
self._insert_extras__() self._insert_extras__()
@@ -321,11 +359,15 @@ class Items(SwaggerDict):
self.pattern = pattern self.pattern = pattern
self.items = items self.items = items
self._insert_extras__() self._insert_extras__()
if items and type != TYPE_ARRAY:
raise AssertionError("items can only be used when type is array")
if pattern and type != TYPE_STRING:
raise AssertionError("pattern can only be used when type is string")
class Parameter(SwaggerDict): class Parameter(SwaggerDict):
def __init__(self, name, in_, description=None, required=None, schema=None, def __init__(self, name, in_, description=None, required=None, schema=None,
type=None, format=None, enum=None, pattern=None, items=None, **extra): type=None, format=None, enum=None, pattern=None, items=None, default=None, **extra):
"""Describe parameters accepted by an :class:`.Operation`. Each parameter should be a unique combination of """Describe parameters accepted by an :class:`.Operation`. Each parameter should be a unique combination of
(`name`, `in_`). ``body`` and ``form`` parameters in the same operation are mutually exclusive. (`name`, `in_`). ``body`` and ``form`` parameters in the same operation are mutually exclusive.
@@ -339,6 +381,7 @@ class Parameter(SwaggerDict):
:param list enum: restrict possible values :param list enum: restrict possible values
:param str pattern: pattern if type is ``string`` :param str pattern: pattern if type is ``string``
:param .Items items: only valid if `type` is ``array`` :param .Items items: only valid if `type` is ``array``
:param default: default value if the parameter is not provided; must conform to parameter type
""" """
super(Parameter, self).__init__(**extra) super(Parameter, self).__init__(**extra)
if (not schema and not type) or (schema and type): if (not schema and not type) or (schema and type):
@@ -353,7 +396,20 @@ class Parameter(SwaggerDict):
self.enum = enum self.enum = enum
self.pattern = pattern self.pattern = pattern
self.items = items self.items = items
self.default = default
self._insert_extras__() self._insert_extras__()
if self['in'] == IN_PATH:
# path parameters must always be required
assert required is not False, "path parameter cannot be optional"
self.required = True
if self['in'] != IN_BODY and schema is not None:
raise AssertionError("schema can only be applied to a body Parameter, not %s" % type)
if (format or enum or pattern or default) and not type:
raise AssertionError("[format, enum, pattern, default] can only be applied to non-body Parameter")
if items and type != TYPE_ARRAY:
raise AssertionError("items can only be used when type is array")
if pattern and type != TYPE_STRING:
raise AssertionError("pattern can only be used when type is string")
class Schema(SwaggerDict): class Schema(SwaggerDict):
@@ -369,7 +425,7 @@ class Schema(SwaggerDict):
:param str format: value format, see OpenAPI spec :param str format: value format, see OpenAPI spec
:param list enum: restrict possible values :param list enum: restrict possible values
:param str pattern: pattern if type is ``string`` :param str pattern: pattern if type is ``string``
:param list[.Schema,.SchemaRef] properties: object properties; required if `type` is ``object`` :param dict[str,(.Schema,.SchemaRef)] properties: object properties; required if `type` is ``object``
:param bool,.Schema,.SchemaRef additional_properties: allow wildcard properties not listed in `properties` :param bool,.Schema,.SchemaRef additional_properties: allow wildcard properties not listed in `properties`
:param list[str] required: list of requried property names :param list[str] required: list of requried property names
:param .Schema,.SchemaRef items: type of array items, only valid if `type` is ``array`` :param .Schema,.SchemaRef items: type of array items, only valid if `type` is ``array``
@@ -381,9 +437,9 @@ class Schema(SwaggerDict):
super(Schema, self).__init__(**extra) super(Schema, self).__init__(**extra)
if required is True or required is False: if required is True or required is False:
# common error # common error
raise AssertionError( raise AssertionError("the `requires` attribute of schema must be an "
"the `requires` attribute of schema must be an array of required properties, not a boolean!") "array of required property names, not a boolean!")
assert type is not None, "type is required!" assert type, "type is required!"
self.title = title self.title = title
self.description = description self.description = description
self.required = filter_none(required) self.required = filter_none(required)
@@ -397,6 +453,14 @@ class Schema(SwaggerDict):
self.read_only = read_only self.read_only = read_only
self.default = default self.default = default
self._insert_extras__() self._insert_extras__()
if (properties or (additional_properties is not None)) and type != TYPE_OBJECT:
raise AssertionError("only object Schema can have properties")
if (format or enum or pattern) and type in (TYPE_OBJECT, TYPE_ARRAY):
raise AssertionError("[format, enum, pattern] can only be applied to primitive Schema")
if items and type != TYPE_ARRAY:
raise AssertionError("items can only be used when type is array")
if pattern and type != TYPE_STRING:
raise AssertionError("pattern can only be used when type is string")
class _Ref(SwaggerDict): class _Ref(SwaggerDict):
+19 -4
View File
@@ -1,15 +1,17 @@
from django.shortcuts import render, resolve_url from django.shortcuts import render, resolve_url
from rest_framework.renderers import BaseRenderer from rest_framework.renderers import BaseRenderer, TemplateHTMLRenderer
from rest_framework.utils import json from rest_framework.utils import json
from .app_settings import swagger_settings, redoc_settings from drf_yasg.openapi import Swagger
from .codecs import OpenAPICodecJson, VALIDATORS, OpenAPICodecYaml
from .app_settings import redoc_settings, swagger_settings
from .codecs import VALIDATORS, OpenAPICodecJson, OpenAPICodecYaml
class _SpecRenderer(BaseRenderer): class _SpecRenderer(BaseRenderer):
"""Base class for text renderers. Handles encoding and validation.""" """Base class for text renderers. Handles encoding and validation."""
charset = None charset = None
validators = ['ssv', 'flex'] validators = []
codec_class = None codec_class = None
@classmethod @classmethod
@@ -51,6 +53,11 @@ class _UIRenderer(BaseRenderer):
template = '' template = ''
def render(self, swagger, accepted_media_type=None, renderer_context=None): def render(self, swagger, accepted_media_type=None, renderer_context=None):
if not isinstance(swagger, Swagger):
# if `swagger` is not a ``Swagger`` object, it means we somehow got a non-success ``Response``
# in that case, it's probably better to let the default ``TemplateHTMLRenderer`` render it
# see https://github.com/axnsan12/drf-yasg/issues/58
return TemplateHTMLRenderer().render(swagger, accepted_media_type, renderer_context)
self.set_context(renderer_context, swagger) self.set_context(renderer_context, swagger)
return render( return render(
renderer_context['request'], renderer_context['request'],
@@ -63,6 +70,7 @@ class _UIRenderer(BaseRenderer):
renderer_context['version'] = swagger.info.version renderer_context['version'] = swagger.info.version
renderer_context['swagger_settings'] = json.dumps(self.get_swagger_ui_settings()) renderer_context['swagger_settings'] = json.dumps(self.get_swagger_ui_settings())
renderer_context['redoc_settings'] = json.dumps(self.get_redoc_settings()) renderer_context['redoc_settings'] = json.dumps(self.get_redoc_settings())
renderer_context['oauth2_config'] = json.dumps(self.get_oauth2_config())
renderer_context['USE_SESSION_AUTH'] = swagger_settings.USE_SESSION_AUTH renderer_context['USE_SESSION_AUTH'] = swagger_settings.USE_SESSION_AUTH
renderer_context.update(self.get_auth_urls()) renderer_context.update(self.get_auth_urls())
@@ -85,6 +93,8 @@ class _UIRenderer(BaseRenderer):
'defaultModelRendering': swagger_settings.DEFAULT_MODEL_RENDERING, 'defaultModelRendering': swagger_settings.DEFAULT_MODEL_RENDERING,
'defaultModelExpandDepth': swagger_settings.DEFAULT_MODEL_DEPTH, 'defaultModelExpandDepth': swagger_settings.DEFAULT_MODEL_DEPTH,
'defaultModelsExpandDepth': swagger_settings.DEFAULT_MODEL_DEPTH, 'defaultModelsExpandDepth': swagger_settings.DEFAULT_MODEL_DEPTH,
'oauth2RedirectUrl': swagger_settings.OAUTH2_REDIRECT_URL,
'supportedSubmitMethods': swagger_settings.SUPPORTED_SUBMIT_METHODS,
} }
data = {k: v for k, v in data.items() if v is not None} data = {k: v for k, v in data.items() if v is not None}
if swagger_settings.VALIDATOR_URL != '': if swagger_settings.VALIDATOR_URL != '':
@@ -102,6 +112,11 @@ class _UIRenderer(BaseRenderer):
return data return data
def get_oauth2_config(self):
data = swagger_settings.OAUTH2_CONFIG
assert isinstance(data, dict), "OAUTH2_CONFIG must be a dict"
return data
class SwaggerUIRenderer(_UIRenderer): class SwaggerUIRenderer(_UIRenderer):
"""Renders a swagger-ui web interface for schema browisng. """Renders a swagger-ui web interface for schema browisng.
@@ -0,0 +1,48 @@
"use strict";
var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname;
var specURL = currentPath + '?format=openapi';
var redoc = document.createElement("redoc");
redoc.setAttribute("spec-url", specURL);
var redocSettings = JSON.parse(document.getElementById('redoc-settings').innerHTML);
if (redocSettings.lazyRendering) {
redoc.setAttribute("lazy-rendering", '');
}
if (redocSettings.pathInMiddle) {
redoc.setAttribute("path-in-middle-panel", '');
}
if (redocSettings.hideHostname) {
redoc.setAttribute("hide-hostname", '');
}
redoc.setAttribute("expand-responses", redocSettings.expandResponses);
document.body.appendChild(redoc);
function hideEmptyVersion() {
// 'span.api-info-version' is for redoc 1.x, 'div.api-info span' is for redoc 2-alpha
var apiVersion = document.querySelector('span.api-info-version') || document.querySelector('div.api-info span');
if (!apiVersion) {
console.log("WARNING: could not find API versionString element (span.api-info-version)");
return;
}
var versionString = apiVersion.innerText;
if (versionString) {
// trim spaces and surrounding ()
versionString = versionString.replace(/ /g,'');
versionString = versionString.replace(/(^\()|(\)$)/g,'');
}
if (!versionString) {
// hide version element if empty
apiVersion.classList.add("hidden");
}
}
if (document.querySelector('span.api-info-version') || document.querySelector('div.api-info span')) {
hideEmptyVersion();
}
else {
insertionQ('span.api-info-version').every(hideEmptyVersion);
insertionQ('div.api-info span').every(hideEmptyVersion);
}
File diff suppressed because one or more lines are too long
+77
View File
@@ -0,0 +1,77 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
padding: 0;
}
body.swagger-body {
background: #fafafa;
}
#django-session-auth {
margin-right: 8px;
}
.hidden {
display: none;
}
#django-session-auth > div {
display: inline-block;
}
#django-session-auth .btn.authorize {
padding: 10px 23px;
}
#django-session-auth .btn.authorize a {
color: #49cc90;
text-decoration: none;
}
#django-session-auth .hello {
margin-right: 5px;
}
#django-session-auth .hello .django-session {
font-weight: bold;
}
.label {
display: inline;
padding: .2em .6em .3em;
font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: .25em;
}
.label-primary {
background-color: #337ab7;
}
.divider {
margin-right: 8px;
background: #16222c44;
width: 2px;
}
svg.swagger-defs {
position: absolute;
width: 0;
height: 0;
}
@@ -1,22 +0,0 @@
# Swagger UI Dist
[![NPM version](https://badge.fury.io/js/swagger-ui-dist.svg)](http://badge.fury.io/js/swagger-ui-dist)
# API
This module, `swagger-ui-dist`, exposes Swagger-UI's entire dist folder as a dependency-free npm module.
Use `swagger-ui` instead, if you'd like to have npm install dependencies for you.
`SwaggerUIBundle` and `SwaggerUIStandalonePreset` can be imported:
```javascript
import { SwaggerUIBundle, SwaggerUIStandalonePreset } from "swagger-ui-dist"
```
To get an absolute path to this directory for static file serving, use the exported `getAbsoluteFSPath` method:
```javascript
const swaggerUiAssetPath = require("swagger-ui-dist").getAbsoluteFSPath()
// then instantiate server that serves files from the swaggerUiAssetPath
```
For anything else, check the [Swagger-UI](https://github.com/swagger-api/swagger-ui) repository.
@@ -1,95 +0,0 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
<defs>
<symbol viewBox="0 0 20 20" id="unlocked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
</symbol>
<symbol viewBox="0 0 20 20" id="locked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="close">
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow">
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow-down">
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="jump-to">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="expand">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
</symbol>
</defs>
</svg>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js"> </script>
<script src="./swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Build a system
const ui = SwaggerUIBundle({
url: "http://petstore.swagger.io/v2/swagger.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
window.ui = ui
}
</script>
</body>
</html>
@@ -45,11 +45,18 @@
oauth2.auth.code = qp.code; oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl}); oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else { } else {
let oauthErrorMsg
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({ oauth2.errCb({
authId: oauth2.auth.name, authId: oauth2.auth.name,
source: "auth", source: "auth",
level: "error", level: "error",
message: "Authorization failed: no accessCode received from the server" message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
}); });
} }
} else { } else {
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"swagger-ui.css","sourceRoot":""}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,82 @@
"use strict";
var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname;
var specURL = currentPath + '?format=openapi';
function patchSwaggerUi() {
var authWrapper = document.querySelector('.auth-wrapper');
var authorizeButton = document.querySelector('.auth-wrapper .authorize');
var djangoSessionAuth = document.querySelector('#django-session-auth');
if (document.querySelector('.auth-wrapper #django-session-auth')) {
console.log("WARNING: session auth already patched; skipping patchSwaggerUi()");
return;
}
authWrapper.insertBefore(djangoSessionAuth, authorizeButton);
djangoSessionAuth.classList.remove("hidden");
var divider = document.createElement("div");
divider.classList.add("divider");
authWrapper.insertBefore(divider, authorizeButton);
}
function initSwaggerUi() {
if (window.ui) {
console.log("WARNING: skipping initSwaggerUi() because window.ui is already defined");
return;
}
var swaggerConfig = {
url: specURL,
dom_id: '#swagger-ui',
displayOperationId: true,
displayRequestDuration: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
filter: true,
requestInterceptor: function (request) {
var headers = request.headers || {};
var csrftoken = document.querySelector("[name=csrfmiddlewaretoken]");
if (csrftoken) {
headers["X-CSRFToken"] = csrftoken.value;
}
return request;
}
};
var swaggerSettings = JSON.parse(document.getElementById('swagger-settings').innerHTML);
if (!('oauth2RedirectUrl' in swaggerSettings)) {
var oauth2RedirectUrl = document.getElementById('oauth2-redirect-url');
if (oauth2RedirectUrl) {
swaggerSettings['oauth2RedirectUrl'] = oauth2RedirectUrl.href;
oauth2RedirectUrl.parentNode.removeChild(oauth2RedirectUrl);
}
}
console.log(swaggerSettings);
for (var p in swaggerSettings) {
if (swaggerSettings.hasOwnProperty(p)) {
swaggerConfig[p] = swaggerSettings[p];
}
}
window.ui = SwaggerUIBundle(swaggerConfig);
var oauth2Config = JSON.parse(document.getElementById('oauth2-config').innerHTML);
console.log(oauth2Config);
window.ui.initOAuth(oauth2Config);
}
window.onload = function () {
initSwaggerUi();
};
if (document.querySelector('.auth-wrapper .authorize')) {
patchSwaggerUi();
}
else {
insertionQ('.auth-wrapper .authorize').every(patchSwaggerUi);
}
@@ -0,0 +1,18 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="{% static 'drf-yasg/style.css' %}"/>
</head>
<body>
<script id="redoc-settings" type="application/json">{{ redoc_settings | safe }}</script>
<script src="{% static 'drf-yasg/insQ.min.js' %}"></script>
<script src="{% static 'drf-yasg/redoc-init.js' %}"> </script>
<script src="{% static 'drf-yasg/redoc-alpha/redoc.standalone.js' %}"> </script>
</body>
</html>
+6 -31
View File
@@ -5,39 +5,14 @@
<title>{{ title }}</title> <title>{{ title }}</title>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { <link rel="stylesheet" type="text/css" href="{% static 'drf-yasg/style.css' %}"/>
margin: 0;
padding: 0;
}
{% if not request.version %}
span.api-info-version {
display: none;
}
{% endif %}
</style>
</head> </head>
<body> <body>
<script> <script id="redoc-settings" type="application/json">{{ redoc_settings | safe }}</script>
var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname;
var specURL = currentPath + '?format=openapi';
var redoc = document.createElement("redoc");
redoc.setAttribute("spec-url", specURL);
var redocSettings = {}; <script src="{% static 'drf-yasg/insQ.min.js' %}"></script>
redocSettings = {{ redoc_settings | safe }}; <script src="{% static 'drf-yasg/redoc-init.js' %}"> </script>
if (redocSettings.lazyRendering) { <script src="{% static 'drf-yasg/redoc/redoc.min.js' %}"></script>
redoc.setAttribute("lazy-rendering", '');
}
if (redocSettings.pathInMiddle) {
redoc.setAttribute("path-in-middle-panel", '');
}
if (redocSettings.hideHostname) {
redoc.setAttribute("hide-hostname", '');
}
redoc.setAttribute("expand-responses", redocSettings.expandResponses);
document.body.appendChild(redoc);
</script>
<script src="{% static 'drf-yasg/redoc/redoc.min.js' %}"> </script>
</body> </body>
</html> </html>
+12 -139
View File
@@ -5,88 +5,18 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ title }}</title> <title>{{ title }}</title>
<link
href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700">
rel="stylesheet"> <link rel="stylesheet" type="text/css" href="{% static 'drf-yasg/style.css' %}"/>
<link rel="stylesheet" type="text/css" href="{% static 'drf-yasg/swagger-ui-dist/swagger-ui.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'drf-yasg/swagger-ui-dist/swagger-ui.css' %}">
<link rel="icon" type="image/png" href="{% static 'drf-yasg/swagger-ui-dist/favicon-32x32.png' %}" <link rel="icon" type="image/png" href="{% static 'drf-yasg/swagger-ui-dist/favicon-32x32.png' %}" sizes="32x32"/>
sizes="32x32"/> <link rel="icon" type="image/png" href="{% static 'drf-yasg/swagger-ui-dist/favicon-16x16.png' %}" sizes="16x16"/>
<link rel="icon" type="image/png" href="{% static 'drf-yasg/swagger-ui-dist/favicon-16x16.png' %}"
sizes="16x16"/>
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
#django-session-auth {
margin-right: 8px;
}
.hidden {
display: none;
}
#django-session-auth > div {
display: inline-block;
}
#django-session-auth .btn.authorize {
padding: 10px 23px;
}
#django-session-auth .btn.authorize a {
color: #49cc90;
text-decoration: none;
}
#django-session-auth .hello {
margin-right: 5px;
}
#django-session-auth .hello .django-session {
font-weight: bold;
}
.label {
display: inline;
padding: .2em .6em .3em;
font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: .25em;
}
.label-primary {
background-color: #337ab7;
}
.divider {
margin-right: 8px;
background: #16222c44;
width: 2px;
}
</style>
</head> </head>
<body> <body class="swagger-body">
<svg xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0"> <svg class="swagger-defs" xmlns="http://www.w3.org/2000/svg">
<defs> <defs>
<symbol viewBox="0 0 20 20" id="unlocked"> <symbol viewBox="0 0 20 20" id="unlocked">
<path <path
@@ -128,73 +58,16 @@
<div id="swagger-ui"></div> <div id="swagger-ui"></div>
<div id="spec-error" class="hidden alert alert-danger"></div> <div id="spec-error" class="hidden alert alert-danger"></div>
<script>
"use strict";
var currentPath = window.location.protocol + "//" + window.location.host + window.location.pathname;
var specURL = currentPath + '?format=openapi';
function patchSwaggerUi() { <script id="swagger-settings" type="application/json">{{ swagger_settings | safe }}</script>
var authWrapper = document.querySelector('.auth-wrapper'); <script id="oauth2-config" type="application/json">{{ oauth2_config | safe }}</script>
var authorizeButton = document.querySelector('.auth-wrapper .authorize');
var djangoSessionAuth = document.querySelector('#django-session-auth');
if (document.querySelector('.auth-wrapper #django-session-auth')) {
console.log("session auth already patched");
return;
}
authWrapper.insertBefore(djangoSessionAuth, authorizeButton);
djangoSessionAuth.classList.remove("hidden");
var divider = document.createElement("div");
divider.classList.add("divider");
authWrapper.insertBefore(divider, authorizeButton);
}
function initSwaggerUi() {
var swaggerConfig = {
url: specURL,
dom_id: '#swagger-ui',
displayOperationId: true,
displayRequestDuration: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
filter: true,
requestInterceptor: function(request) {
var headers = request.headers || {};
var csrftoken = document.querySelector("[name=csrfmiddlewaretoken]");
if (csrftoken) {
headers["X-CSRFToken"] = csrftoken.value;
}
return request;
}
};
var swaggerSettings = {};
swaggerSettings = {{ swagger_settings | safe }};
console.log(swaggerSettings);
for (var p in swaggerSettings) {
if (swaggerSettings.hasOwnProperty(p)) {
swaggerConfig[p] = swaggerSettings[p];
}
}
window.ui = SwaggerUIBundle(swaggerConfig);
}
window.onload = function () {
insertionQ('.auth-wrapper .authorize').every(patchSwaggerUi);
initSwaggerUi();
};
</script>
<script src="{% static 'drf-yasg/swagger-ui-dist/swagger-ui-bundle.js' %}"></script> <script src="{% static 'drf-yasg/swagger-ui-dist/swagger-ui-bundle.js' %}"></script>
<script src="{% static 'drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js' %}"></script> <script src="{% static 'drf-yasg/swagger-ui-dist/swagger-ui-standalone-preset.js' %}"></script>
<script src="{% static 'drf-yasg/insQ.min.js' %}"></script> <script src="{% static 'drf-yasg/insQ.min.js' %}"></script>
<script src="{% static 'drf-yasg/swagger-ui-init.js' %}"></script>
<a id="oauth2-redirect-url" href="{% static 'drf-yasg/swagger-ui-dist/oauth2-redirect.html' %}" class="hidden"></a>
<div id="django-session-auth" class="hidden"> <div id="django-session-auth" class="hidden">
{% if USE_SESSION_AUTH %} {% if USE_SESSION_AUTH %}
+104 -45
View File
@@ -2,18 +2,29 @@ import inspect
import logging import logging
from collections import OrderedDict from collections import OrderedDict
from rest_framework import status, serializers from django.db import models
from rest_framework.mixins import RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin from rest_framework import serializers, status
from rest_framework.mixins import DestroyModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.request import is_form_media_type
from rest_framework.settings import api_settings as rest_framework_settings
from rest_framework.views import APIView
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
#: used to forcibly remove the body of a request via :func:`.swagger_auto_schema`
no_body = object() class no_body(object):
"""Used as a sentinel value to forcibly remove the body of a request via :func:`.swagger_auto_schema`."""
pass
def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_body=None, query_serializer=None, class unset(object):
manual_parameters=None, operation_id=None, operation_description=None, responses=None, """Used as a sentinel value for function parameters not set by the caller where ``None`` would be a valid value."""
field_inspectors=None, filter_inspectors=None, paginator_inspectors=None, pass
def swagger_auto_schema(method=None, methods=None, auto_schema=unset, request_body=None, query_serializer=None,
manual_parameters=None, operation_id=None, operation_description=None, security=None,
responses=None, field_inspectors=None, filter_inspectors=None, paginator_inspectors=None,
**extra_overrides): **extra_overrides):
"""Decorate a view method to customize the :class:`.Operation` object generated from it. """Decorate a view method to customize the :class:`.Operation` object generated from it.
@@ -22,18 +33,12 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
The `auto_schema` and `operation_description` arguments take precendence over view- or method-level values. The `auto_schema` and `operation_description` arguments take precendence over view- or method-level values.
.. versionchanged:: 1.1
Added the ``extra_overrides`` and ``operatiod_id`` parameters.
.. versionchanged:: 1.1
Added the ``field_inspectors``, ``filter_inspectors`` and ``paginator_inspectors`` parameters.
:param str method: for multi-method views, the http method the options should apply to :param str method: for multi-method views, the http method the options should apply to
:param list[str] methods: for multi-method views, the http methods the options should apply to :param list[str] methods: for multi-method views, the http methods the options should apply to
:param .inspectors.SwaggerAutoSchema auto_schema: custom class to use for generating the Operation object; :param .inspectors.SwaggerAutoSchema auto_schema: custom class to use for generating the Operation object;
this overrides both the class-level ``swagger_schema`` attribute and the ``DEFAULT_AUTO_SCHEMA_CLASS`` this overrides both the class-level ``swagger_schema`` attribute and the ``DEFAULT_AUTO_SCHEMA_CLASS``
setting setting, and can be set to ``None`` to prevent this operation from being generated
:param .Schema,.SchemaRef,.Serializer request_body: custom request body, or :data:`.no_body`. The value given here :param .Schema,.SchemaRef,.Serializer request_body: custom request body, or :class:`.no_body`. The value given here
will be used as the ``schema`` property of a :class:`.Parameter` with ``in: 'body'``. will be used as the ``schema`` property of a :class:`.Parameter` with ``in: 'body'``.
A Schema or SchemaRef is not valid if this request consumes form-data, because ``form`` and ``body`` parameters A Schema or SchemaRef is not valid if this request consumes form-data, because ``form`` and ``body`` parameters
@@ -41,7 +46,7 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
the `manual_parameters` argument. the `manual_parameters` argument.
If a ``Serializer`` class or instance is given, it will be automatically converted into a :class:`.Schema` If a ``Serializer`` class or instance is given, it will be automatically converted into a :class:`.Schema`
used as a ``body`` :class:`.Parameter`, or into a list of ``form`` :class:`.Parameter`\ s, as appropriate. used as a ``body`` :class:`.Parameter`, or into a list of ``form`` :class:`.Parameter`\ s, as appropriate.
:param .Serializer query_serializer: if you use a ``Serializer`` to parse query parameters, you can pass it here :param .Serializer query_serializer: if you use a ``Serializer`` to parse query parameters, you can pass it here
and have :class:`.Parameter` objects be generated automatically from it. and have :class:`.Parameter` objects be generated automatically from it.
@@ -61,6 +66,9 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
:param str operation_id: operation ID override; the operation ID must be unique accross the whole API :param str operation_id: operation ID override; the operation ID must be unique accross the whole API
:param str operation_description: operation description override :param str operation_description: operation description override
:param str security: security requirements override; used to specify which authetication mechanism
is requried to call this API; an empty list marks the endpoint as unauthenticated (i.e. removes all accepted
authentication schemes), and ``None`` will inherit the top-level secuirty requirements
:param dict[str,(.Schema,.SchemaRef,.Response,str,Serializer)] responses: a dict of documented manual responses :param dict[str,(.Schema,.SchemaRef,.Response,str,Serializer)] responses: a dict of documented manual responses
keyed on response status code. If no success (``2xx``) response is given, one will automatically be keyed on response status code. If no success (``2xx``) response is given, one will automatically be
generated from the request body and http method. If any ``2xx`` response is given the automatic response is generated from the request body and http method. If any ``2xx`` response is given the automatic response is
@@ -85,60 +93,72 @@ def swagger_auto_schema(method=None, methods=None, auto_schema=None, request_bod
""" """
def decorator(view_method): def decorator(view_method):
assert not any(hm in extra_overrides for hm in APIView.http_method_names), "HTTP method names not allowed here"
data = { data = {
'auto_schema': auto_schema,
'request_body': request_body, 'request_body': request_body,
'query_serializer': query_serializer, 'query_serializer': query_serializer,
'manual_parameters': manual_parameters, 'manual_parameters': manual_parameters,
'operation_id': operation_id, 'operation_id': operation_id,
'operation_description': operation_description, 'operation_description': operation_description,
'security': security,
'responses': responses, 'responses': responses,
'filter_inspectors': list(filter_inspectors) if filter_inspectors else None, 'filter_inspectors': list(filter_inspectors) if filter_inspectors else None,
'paginator_inspectors': list(paginator_inspectors) if paginator_inspectors else None, 'paginator_inspectors': list(paginator_inspectors) if paginator_inspectors else None,
'field_inspectors': list(field_inspectors) if field_inspectors else None, 'field_inspectors': list(field_inspectors) if field_inspectors else None,
} }
data = {k: v for k, v in data.items() if v is not None} data = filter_none(data)
if auto_schema is not unset:
data['auto_schema'] = auto_schema
data.update(extra_overrides) data.update(extra_overrides)
if not data: # pragma: no cover
# no overrides to set, no use in doing more work
return
# if the method is a detail_route or list_route, it will have a bind_to_methods attribute # if the method is a detail_route or list_route, it will have a bind_to_methods attribute
bind_to_methods = getattr(view_method, 'bind_to_methods', []) bind_to_methods = getattr(view_method, 'bind_to_methods', [])
# if the method is actually a function based view (@api_view), it will have a 'cls' attribute # if the method is actually a function based view (@api_view), it will have a 'cls' attribute
view_cls = getattr(view_method, 'cls', None) view_cls = getattr(view_method, 'cls', None)
http_method_names = getattr(view_cls, 'http_method_names', []) http_method_names = [m for m in getattr(view_cls, 'http_method_names', []) if hasattr(view_cls, m)]
if bind_to_methods or http_method_names:
available_methods = http_method_names + bind_to_methods
existing_data = getattr(view_method, '_swagger_auto_schema', {})
_methods = methods
if methods or method:
assert available_methods or http_method_names, "`method` or `methods` can only be specified " \
"on @detail_route or @api_view views"
assert bool(methods) != bool(method), "specify either method or methods"
assert not isinstance(methods, str), "`methods` expects to receive a list of methods;" \
" use `method` for a single argument"
if method:
_methods = [method.lower()]
else:
_methods = [mth.lower() for mth in methods]
assert all(mth in available_methods for mth in _methods), "http method not bound to view"
assert not any(mth in existing_data for mth in _methods), "http method defined multiple times"
if available_methods:
# detail_route, list_route or api_view # detail_route, list_route or api_view
assert bool(http_method_names) != bool(bind_to_methods), "this should never happen" assert bool(http_method_names) != bool(bind_to_methods), "this should never happen"
available_methods = http_method_names + bind_to_methods
existing_data = getattr(view_method, '_swagger_auto_schema', {})
if http_method_names:
_route = "api_view"
else:
_route = "detail_route" if view_method.detail else "list_route"
_methods = methods
if len(available_methods) > 1: if len(available_methods) > 1:
assert methods or method, \ assert _methods, \
"on multi-method %s, you must specify swagger_auto_schema on a per-method basis " \ "on multi-method api_view, detail_route or list_route, you must specify swagger_auto_schema on " \
"using one of the `method` or `methods` arguments" % _route "a per-method basis using one of the `method` or `methods` arguments"
assert bool(methods) != bool(method), "specify either method or methods"
assert not isinstance(methods, str), "`methods` expects to receive a list of methods;" \
" use `method` for a single argument"
if method:
_methods = [method.lower()]
else:
_methods = [mth.lower() for mth in methods]
assert not any(mth in existing_data for mth in _methods), "method defined multiple times"
assert all(mth in available_methods for mth in _methods), "method not bound to %s" % _route
existing_data.update((mth.lower(), data) for mth in _methods)
else: else:
existing_data[available_methods[0]] = data # for a single-method view we assume that single method as the decorator target
_methods = _methods or available_methods
assert not any(hasattr(getattr(view_cls, mth, None), '_swagger_auto_schema') for mth in _methods), \
"swagger_auto_schema applied twice to method"
assert not any(mth in existing_data for mth in _methods), "swagger_auto_schema applied twice to method"
existing_data.update((mth.lower(), data) for mth in _methods)
view_method._swagger_auto_schema = existing_data view_method._swagger_auto_schema = existing_data
else: else:
assert method is None and methods is None, \ assert not _methods, \
"the methods argument should only be specified when decorating a detail_route or list_route; you " \ "the methods argument should only be specified when decorating a detail_route or list_route; you " \
"should also ensure that you put the swagger_auto_schema decorator AFTER (above) the _route decorator" "should also ensure that you put the swagger_auto_schema decorator AFTER (above) the _route decorator"
assert not existing_data, "swagger_auto_schema applied twice to method"
view_method._swagger_auto_schema = data view_method._swagger_auto_schema = data
return view_method return view_method
@@ -166,7 +186,7 @@ def is_list_view(path, method, view):
# a detail_route is surely not a list route # a detail_route is surely not a list route
return False return False
# for APIView, if it's a detail view it can't also be a list view # for GenericAPIView, if it's a detail view it can't also be a list view
if isinstance(view, (RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin)): if isinstance(view, (RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin)):
return False return False
@@ -235,3 +255,42 @@ def force_serializer_instance(serializer):
assert isinstance(serializer, serializers.BaseSerializer), \ assert isinstance(serializer, serializers.BaseSerializer), \
"Serializer class or instance required, not %s" % type(serializer).__name__ "Serializer class or instance required, not %s" % type(serializer).__name__
return serializer return serializer
def get_consumes(parser_classes):
"""Extract ``consumes`` MIME types from a list of parser classes.
:param list parser_classes: parser classes
:return: MIME types for ``consumes``
:rtype: list[str]
"""
media_types = [parser.media_type for parser in parser_classes or []]
if all(is_form_media_type(encoding) for encoding in media_types):
return media_types
else:
media_types = [encoding for encoding in media_types if not is_form_media_type(encoding)]
return media_types
def get_produces(renderer_classes):
"""Extract ``produces`` MIME types from a list of renderer classes.
:param list renderer_classes: renderer classes
:return: MIME types for ``produces``
:rtype: list[str]
"""
media_types = [renderer.media_type for renderer in renderer_classes or []]
media_types = [encoding for encoding in media_types if 'html' not in encoding]
return media_types
def decimal_as_float(field):
"""
Returns true if ``field`` is a django-rest-framework DecimalField and its ``coerce_to_string`` attribute or the
``COERCE_DECIMAL_TO_STRING`` setting is set to ``False``.
:rtype: bool
"""
if isinstance(field, serializers.DecimalField) or isinstance(field, models.DecimalField):
return not getattr(field, 'coerce_to_string', rest_framework_settings.COERCE_DECIMAL_TO_STRING)
return False
+11 -13
View File
@@ -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,18 +45,17 @@ 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: information about the API; if omitted, defaults to :ref:`DEFAULT_INFO <default-swagger-settings>`
:param str url: API base url; if left blank will be deduced from the location the view is served at :param str url: same as :class:`.OpenAPISchemaGenerator`
:param patterns: passed to SchemaGenerator :param patterns: same as :class:`.OpenAPISchemaGenerator`
:param urlconf: passed to SchemaGenerator :param urlconf: same as :class:`.OpenAPISchemaGenerator`
:param bool public: if False, includes only endpoints the current user has access to :param bool public: if False, includes only the endpoints that are accesible by the user viewing the schema
:param list validators: a list of validator names to apply; allowed values are ``flex``, ``ssv`` :param list validators: a list of validator names to apply; allowed values are ``flex``, ``ssv``
:param type generator_class: schema generator class to use; should be a subclass of :class:`.OpenAPISchemaGenerator` :param type generator_class: schema generator class to use; should be a subclass of :class:`.OpenAPISchemaGenerator`
:param tuple authentication_classes: authentication classes for the schema view itself :param tuple authentication_classes: authentication classes for the schema view itself
@@ -69,6 +67,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)
@@ -94,8 +93,7 @@ def get_schema_view(info, url=None, patterns=None, urlconf=None, public=False, v
Arguments described in :meth:`.as_cached_view`. Arguments described in :meth:`.as_cached_view`.
""" """
if not cls.public: view = vary_on_headers('Cookie', 'Authorization')(view)
view = vary_on_headers('Cookie', 'Authorization')(view)
view = cache_page(cache_timeout, **cache_kwargs)(view) view = cache_page(cache_timeout, **cache_kwargs)(view)
view = deferred_never_cache(view) # disable in-browser caching view = deferred_never_cache(view) # disable in-browser caching
return view return view
+22 -1
View File
@@ -1,8 +1,9 @@
# Generated by Django 2.0 on 2017-12-23 09:07 # Generated by Django 2.0.1 on 2018-03-18 18:32
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import uuid
class Migration(migrations.Migration): class Migration(migrations.Migration):
@@ -23,8 +24,28 @@ class Migration(migrations.Migration):
('slug', models.SlugField(blank=True, help_text='slug model help_text', unique=True)), ('slug', models.SlugField(blank=True, help_text='slug model help_text', unique=True)),
('date_created', models.DateTimeField(auto_now_add=True)), ('date_created', models.DateTimeField(auto_now_add=True)),
('date_modified', models.DateTimeField(auto_now=True)), ('date_modified', models.DateTimeField(auto_now=True)),
('article_type', models.PositiveSmallIntegerField(choices=[(1, 'first'), (2, 'second'), (3, 'third'), (7, 'seven'), (8, 'eight')], help_text='IntegerField declared on model with choices=(...) and exposed via ModelSerializer', null=True)),
('cover', models.ImageField(blank=True, upload_to='article/original/')), ('cover', models.ImageField(blank=True, upload_to='article/original/')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to=settings.AUTH_USER_MODEL)), ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='articles', to=settings.AUTH_USER_MODEL)),
], ],
), ),
migrations.CreateModel(
name='ArticleGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('title', models.CharField(help_text='title model help_text', max_length=255, unique=True)),
('slug', models.SlugField(blank=True, help_text='slug model help_text', unique=True)),
],
),
migrations.AddField(
model_name='article',
name='group',
field=models.ForeignKey(blank=True, default=None, on_delete=django.db.models.deletion.PROTECT, related_name='articles_as_main', to='articles.ArticleGroup'),
),
migrations.AddField(
model_name='article',
name='original_group',
field=models.ForeignKey(blank=True, default=None, on_delete=django.db.models.deletion.PROTECT, related_name='articles_as_original', to='articles.ArticleGroup'),
),
] ]
+17
View File
@@ -1,3 +1,5 @@
import uuid
from django.db import models from django.db import models
@@ -8,5 +10,20 @@ class Article(models.Model):
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
date_modified = models.DateTimeField(auto_now=True) date_modified = models.DateTimeField(auto_now=True)
author = models.ForeignKey('auth.User', related_name='articles', on_delete=models.CASCADE) author = models.ForeignKey('auth.User', related_name='articles', on_delete=models.CASCADE)
article_type = models.PositiveSmallIntegerField(
help_text="IntegerField declared on model with choices=(...) and exposed via ModelSerializer",
choices=((1, "first"), (2, "second"), (3, "third"), (7, "seven"), (8, "eight")), null=True
)
cover = models.ImageField(upload_to='article/original/', blank=True) cover = models.ImageField(upload_to='article/original/', blank=True)
group = models.ForeignKey('ArticleGroup', related_name='articles_as_main', blank=True, default=None,
on_delete=models.PROTECT)
original_group = models.ForeignKey('ArticleGroup', related_name='articles_as_original', blank=True, default=None,
on_delete=models.PROTECT)
class ArticleGroup(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
title = models.CharField(help_text="title model help_text", max_length=255, blank=False, unique=True)
slug = models.SlugField(help_text="slug model help_text", unique=True, blank=True)
+6 -4
View File
@@ -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, ArticleGroup
from django.utils.translation import ugettext_lazy as _
class ArticleSerializer(serializers.ModelSerializer): class ArticleSerializer(serializers.ModelSerializer):
@@ -12,11 +12,13 @@ class ArticleSerializer(serializers.ModelSerializer):
) )
uuid = serializers.UUIDField(help_text="should articles have UUIDs?", read_only=True) uuid = serializers.UUIDField(help_text="should articles have UUIDs?", read_only=True)
cover_name = serializers.FileField(use_url=False, source='cover', read_only=True) cover_name = serializers.FileField(use_url=False, source='cover', read_only=True)
group = serializers.SlugRelatedField(slug_field='uuid', queryset=ArticleGroup.objects.all())
original_group = serializers.SlugRelatedField(slug_field='uuid', read_only=True)
class Meta: class Meta:
model = Article model = Article
fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified', fields = ('title', 'author', 'body', 'slug', 'date_created', 'date_modified',
'references', 'uuid', 'cover', 'cover_name') 'references', 'uuid', 'cover', 'cover_name', 'article_type', 'group', 'original_group', )
read_only_fields = ('date_created', 'date_modified', read_only_fields = ('date_created', 'date_modified',
'references', 'uuid', 'cover_name') 'references', 'uuid', 'cover_name')
lookup_field = 'slug' lookup_field = 'slug'
@@ -31,7 +33,7 @@ class ArticleSerializer(serializers.ModelSerializer):
class ImageUploadSerializer(serializers.Serializer): class ImageUploadSerializer(serializers.Serializer):
what_am_i_doing = serializers.RegexField(regex=r"^69$", help_text="test") what_am_i_doing = serializers.RegexField(regex=r"^69$", help_text="test", default="69")
image_styles = serializers.ListSerializer( image_styles = serializers.ListSerializer(
child=serializers.ChoiceField(choices=['wide', 'tall', 'thumb', 'social']), child=serializers.ChoiceField(choices=['wide', 'tall', 'thumb', 'social']),
help_text="Parameter with Items" help_text="Parameter with Items"
+1 -1
View File
@@ -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
+14 -3
View File
@@ -1,4 +1,15 @@
from django.contrib.auth.models import User from __future__ import print_function
User.objects.filter(username='admin').delete() from django.contrib.auth.models import User
User.objects.create_superuser('admin', 'admin@admin.admin', 'passwordadmin') from django.db.utils import IntegrityError
username = 'admin'
email = 'admin@admin.admin'
password = 'passwordadmin'
try:
User.objects.create_superuser(username, email, password)
except IntegrityError:
print("User '%s <%s>' already exists" % (username, email))
else:
print("Created superuser '%s <%s>' with password '%s'" % (username, email, password))
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import sys import sys
if __name__ == "__main__": if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings.local")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError: except ImportError:
View File
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class PeopleConfig(AppConfig):
name = 'people'
@@ -0,0 +1,30 @@
# Generated by Django 2.0.1 on 2018-03-18 18:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Identity',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('firstName', models.CharField(max_length=30, null=True)),
('lastName', models.CharField(max_length=30, null=True)),
],
),
migrations.CreateModel(
name='Person',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('identity', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='person', to='people.Identity')),
],
),
]
+10
View File
@@ -0,0 +1,10 @@
from django.db import models
class Identity(models.Model):
firstName = models.CharField(max_length=30, null=True)
lastName = models.CharField(max_length=30, null=True)
class Person(models.Model):
identity = models.OneToOneField(Identity, related_name='person', on_delete=models.PROTECT)
+23
View File
@@ -0,0 +1,23 @@
from rest_framework import serializers
from .models import Identity, Person
class IdentitySerializer(serializers.ModelSerializer):
class Meta:
model = Identity
fields = '__all__'
class PersonSerializer(serializers.ModelSerializer):
identity = IdentitySerializer()
class Meta:
model = Person
fields = '__all__'
def create(self, validated_data):
identity = Identity(**validated_data['identity'])
identity.save()
validated_data['identity'] = identity
return super().create(validated_data)
+26
View File
@@ -0,0 +1,26 @@
from django.conf.urls import url
from .views import IdentityViewSet, PersonViewSet
person_list = PersonViewSet.as_view({
'get': 'list',
'post': 'create'
})
person_detail = PersonViewSet.as_view({
'get': 'retrieve',
'patch': 'partial_update',
'delete': 'destroy'
})
identity_detail = IdentityViewSet.as_view({
'get': 'retrieve',
'patch': 'partial_update',
})
urlpatterns = (
url(r'^$', person_list, name='people-list'),
url(r'^(?P<pk>[0-9]+)$', person_detail, name='person-detail'),
url(r'^(?P<person>[0-9]+)/identity$', identity_detail,
name='person-identity'),
)
+16
View File
@@ -0,0 +1,16 @@
from rest_framework import viewsets
from .models import Identity, Person
from .serializers import IdentitySerializer, PersonSerializer
class PersonViewSet(viewsets.ModelViewSet):
model = Person
queryset = Person.objects
serializer_class = PersonSerializer
class IdentityViewSet(viewsets.ModelViewSet):
model = Identity
queryset = Identity.objects
serializer_class = IdentitySerializer
+2
View File
@@ -0,0 +1,2 @@
..[validation]
-r ../requirements/testproj.txt
+1 -1
View File
@@ -1,4 +1,4 @@
# Generated by Django 2.0 on 2017-12-23 09:07 # Generated by Django 2.0.1 on 2018-03-18 18:32
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
+8 -3
View File
@@ -1,11 +1,13 @@
from decimal import Decimal
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 rest_framework.compat import MinValueValidator
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):
name = serializers.ChoiceField( name = serializers.ChoiceField(
choices=LANGUAGE_CHOICES, default='python', help_text='The name of the programming language') choices=LANGUAGE_CHOICES, default='python', help_text='The name of the programming language')
@@ -14,7 +16,6 @@ class LanguageSerializer(serializers.Serializer):
class ExampleProjectSerializer(serializers.Serializer): class ExampleProjectSerializer(serializers.Serializer):
project_name = serializers.CharField(help_text='Name of the project') project_name = serializers.CharField(help_text='Name of the project')
github_repo = serializers.CharField(required=True, help_text='Github repository of the project') github_repo = serializers.CharField(required=True, help_text='Github repository of the project')
@@ -49,6 +50,10 @@ class SnippetSerializer(serializers.Serializer):
example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True) example_projects = serializers.ListSerializer(child=ExampleProjectSerializer(), read_only=True)
difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField", difficulty_factor = serializers.FloatField(help_text="this is here just to test FloatField",
read_only=True, default=lambda: 6.9) read_only=True, default=lambda: 6.9)
rate_as_string = serializers.DecimalField(max_digits=6, decimal_places=3, default=Decimal('0.0'),
validators=[MinValueValidator(Decimal('0.0'))])
rate = serializers.DecimalField(max_digits=6, decimal_places=3, default=Decimal('0.0'), coerce_to_string=False,
validators=[MinValueValidator(Decimal('0.0'))])
def create(self, validated_data): def create(self, validated_data):
""" """
+17 -2
View File
@@ -1,9 +1,12 @@
from djangorestframework_camel_case.parser import CamelCaseJSONParser from djangorestframework_camel_case.parser import CamelCaseJSONParser
from djangorestframework_camel_case.render import CamelCaseJSONRenderer from djangorestframework_camel_case.render import CamelCaseJSONRenderer
from inflection import camelize from inflection import camelize
from rest_framework import generics from rest_framework import generics, status
from rest_framework.parsers import FormParser
from drf_yasg import openapi
from drf_yasg.inspectors import SwaggerAutoSchema from drf_yasg.inspectors import SwaggerAutoSchema
from drf_yasg.utils import swagger_auto_schema
from snippets.models import Snippet from snippets.models import Snippet
from snippets.serializers import SnippetSerializer from snippets.serializers import SnippetSerializer
@@ -19,7 +22,7 @@ class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all() queryset = Snippet.objects.all()
serializer_class = SnippetSerializer serializer_class = SnippetSerializer
parser_classes = (CamelCaseJSONParser,) parser_classes = (FormParser, CamelCaseJSONParser,)
renderer_classes = (CamelCaseJSONRenderer,) renderer_classes = (CamelCaseJSONRenderer,)
swagger_schema = CamelCaseOperationIDAutoSchema swagger_schema = CamelCaseOperationIDAutoSchema
@@ -53,6 +56,18 @@ class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
"""patch method docstring""" """patch method docstring"""
return super(SnippetDetail, self).patch(request, *args, **kwargs) return super(SnippetDetail, self).patch(request, *args, **kwargs)
@swagger_auto_schema(manual_parameters=[
openapi.Parameter(
name='id', in_=openapi.IN_PATH,
type=openapi.TYPE_INTEGER,
description="path parameter override",
required=True
),
], responses={
status.HTTP_204_NO_CONTENT: openapi.Response(
description="This should not crash"
)
})
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
"""delete method docstring""" """delete method docstring"""
return super(SnippetDetail, self).patch(request, *args, **kwargs) return super(SnippetDetail, self).patch(request, *args, **kwargs)
@@ -1,16 +1,7 @@
import os import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '!z1yj(9uz)zk0gg@5--j)bc4h^i!8))r^dezco8glf190e0&#p'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = [ ALLOWED_HOSTS = [
'127.0.0.1', '127.0.0.1',
@@ -35,6 +26,8 @@ INSTALLED_APPS = [
'snippets', 'snippets',
'users', 'users',
'articles', 'articles',
'todo',
'people'
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -69,16 +62,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'testproj.wsgi.application' WSGI_APPLICATION = 'testproj.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
@@ -97,16 +80,21 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
# Django Rest Framework
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ( 'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated', 'rest_framework.permissions.IsAuthenticated',
) )
} }
# drf-yasg
SWAGGER_SETTINGS = { SWAGGER_SETTINGS = {
'LOGIN_URL': '/admin/login', 'LOGIN_URL': '/admin/login',
'LOGOUT_URL': '/admin/logout', 'LOGOUT_URL': '/admin/logout',
'VALIDATOR_URL': 'http://localhost:8189',
'DEFAULT_INFO': 'testproj.urls.swagger_info'
} }
# Internationalization # Internationalization
@@ -126,9 +114,14 @@ USE_TZ = True
# https://docs.djangoproject.com/en/1.11/howto/static-files/ # https://docs.djangoproject.com/en/1.11/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Testing
TEST_RUNNER = 'testproj.runner.PytestTestRunner' TEST_RUNNER = 'testproj.runner.PytestTestRunner'
# Logging configuration
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': True, 'disable_existing_loggers': True,
+35
View File
@@ -0,0 +1,35 @@
import dj_database_url
from .base import * # noqa: F403
DEBUG = True
ALLOWED_HOSTS.append('.herokuapp.com')
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
assert SECRET_KEY, 'DJANGO_SECRET_KEY environment variable must be set'
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
# Simplified static file serving.
# https://warehouse.python.org/project/whitenoise/
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
MIDDLEWARE.insert(0, 'whitenoise.middleware.WhiteNoiseMiddleware')
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': dj_database_url.config(conn_max_age=600)
}
SILENCED_SYSTEM_CHECKS = [
'security.W004', # SECURE_HSTS_SECONDS
'security.W008', # SECURE_SSL_REDIRECT
]
+24
View File
@@ -0,0 +1,24 @@
import os
import dj_database_url
from .base import * # noqa: F403
SWAGGER_SETTINGS.update({'VALIDATOR_URL': 'http://localhost:8189'})
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
db_path = os.path.join(BASE_DIR, 'db.sqlite3')
DATABASES = {
'default': dj_database_url.parse('sqlite:///' + db_path)
}
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '!z1yj(9uz)zk0gg@5--j)bc4h^i!8))r^dezco8glf190e0&#p'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
+35 -9
View File
@@ -1,20 +1,29 @@
from django.conf.urls import url, include import user_agents
from django.conf.urls import include, url
from django.contrib import admin from django.contrib import admin
from django.shortcuts import redirect
from rest_framework import permissions from rest_framework import permissions
from rest_framework.decorators import api_view 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="""This is a demo project for the [drf-yasg](https://github.com/axnsan12/drf-yasg) Django Rest Framework library.
The `swagger-ui` view can be found [here](/cached/swagger).
The `ReDoc` view can be found [here](/cached/redoc).
The swagger YAML document can be found [here](/cached/swagger.yaml).
You can log in using the pre-existing `admin` user with password `passwordadmin`.""", # noqa
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,),
@@ -26,17 +35,34 @@ def plain_view(request):
pass pass
def root_redirect(request):
user_agent_string = request.META.get('HTTP_USER_AGENT', '')
user_agent = user_agents.parse(user_agent_string)
if user_agent.is_mobile:
schema_view = 'cschema-redoc'
else:
schema_view = 'cschema-swagger-ui'
return redirect(schema_view, permanent=True)
urlpatterns = [ urlpatterns = [
url(r'^swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=0), name='schema-json'), url(r'^swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=0), name='schema-json'),
url(r'^swagger/$', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), url(r'^swagger/$', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
url(r'^redoc/$', SchemaView.with_ui('redoc', cache_timeout=0), name='schema-redoc'), url(r'^redoc/$', SchemaView.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
url(r'^cached/swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=None), name='cschema-json'), url(r'^cached/swagger(?P<format>.json|.yaml)$', SchemaView.without_ui(cache_timeout=None), name='cschema-json'),
url(r'^cached/swagger/$', SchemaView.with_ui('swagger', cache_timeout=None), name='cschema-swagger-ui'), url(r'^cached/swagger/$', SchemaView.with_ui('swagger', cache_timeout=None), name='cschema-swagger-ui'),
url(r'^cached/redoc/$', SchemaView.with_ui('redoc', cache_timeout=None), name='cschema-redoc'), url(r'^cached/redoc/$', SchemaView.with_ui('redoc', cache_timeout=None), name='cschema-redoc'),
url(r'^$', root_redirect),
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^snippets/', include('snippets.urls')), url(r'^snippets/', include('snippets.urls')),
url(r'^articles/', include('articles.urls')), url(r'^articles/', include('articles.urls')),
url(r'^users/', include('users.urls')), url(r'^users/', include('users.urls')),
url(r'^todo/', include('todo.urls')),
url(r'^people/', include('people.urls')),
url(r'^plain/', plain_view), url(r'^plain/', plain_view),
] ]
+1 -1
View File
@@ -2,6 +2,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproj.settings.local")
application = get_wsgi_application() application = get_wsgi_application()
View File
+38
View File
@@ -0,0 +1,38 @@
# Generated by Django 2.0.1 on 2018-03-18 18:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Todo',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='TodoAnother',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50)),
('todo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='todo.Todo')),
],
),
migrations.CreateModel(
name='TodoYetAnother',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50)),
('todo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='todo.TodoAnother')),
],
),
]
+15
View File
@@ -0,0 +1,15 @@
from django.db import models
class Todo(models.Model):
title = models.CharField(max_length=50)
class TodoAnother(models.Model):
todo = models.ForeignKey(Todo, on_delete=models.CASCADE)
title = models.CharField(max_length=50)
class TodoYetAnother(models.Model):
todo = models.ForeignKey(TodoAnother, on_delete=models.CASCADE)
title = models.CharField(max_length=50)
+27
View File
@@ -0,0 +1,27 @@
from django.utils import timezone
from rest_framework import serializers
from .models import Todo, TodoAnother, TodoYetAnother
class TodoSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = ('title', 'a_hidden_field',)
a_hidden_field = serializers.HiddenField(default=timezone.now)
class TodoAnotherSerializer(serializers.ModelSerializer):
todo = TodoSerializer()
class Meta:
model = TodoAnother
fields = ('title', 'todo')
class TodoYetAnotherSerializer(serializers.ModelSerializer):
class Meta:
model = TodoYetAnother
fields = ('title', 'todo')
depth = 2
+10
View File
@@ -0,0 +1,10 @@
from rest_framework import routers
from todo import views
router = routers.DefaultRouter()
router.register(r'', views.TodoViewSet)
router.register(r'another', views.TodoAnotherViewSet)
router.register(r'yetanother', views.TodoYetAnotherViewSet)
urlpatterns = router.urls
+22
View File
@@ -0,0 +1,22 @@
from rest_framework import viewsets
from .models import Todo, TodoAnother, TodoYetAnother
from .serializer import TodoAnotherSerializer, TodoSerializer, TodoYetAnotherSerializer
class TodoViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Todo.objects.all()
serializer_class = TodoSerializer
lookup_field = 'id'
lookup_value_regex = '[0-9]+'
class TodoAnotherViewSet(viewsets.ReadOnlyModelViewSet):
queryset = TodoAnother.objects.all()
serializer_class = TodoAnotherSerializer
class TodoYetAnotherViewSet(viewsets.ReadOnlyModelViewSet):
queryset = TodoYetAnother.objects.all()
serializer_class = TodoYetAnotherSerializer

Some files were not shown because too many files have changed in this diff Show More