├── .flake8 ├── .github └── workflows │ ├── lint-and-pytest.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── examples ├── aiohttp_test.py ├── bottle_test.py ├── chalice_test │ ├── .chalice │ │ └── config.json │ ├── README.md │ └── app.py ├── conf │ ├── test.yaml │ └── test3.yaml ├── falcon_asgi_test.py ├── falcon_test.py ├── flask_test.py ├── quart_test.py ├── sanic_test.py ├── starlette_test.py └── tornado_test.py ├── setup.py ├── swagger_ui ├── __init__.py ├── core.py ├── handlers │ ├── __init__.py │ ├── aiohttp.py │ ├── bottle.py │ ├── chalice.py │ ├── falcon.py │ ├── flask.py │ ├── quart.py │ ├── sanic.py │ ├── starlette.py │ └── tornado.py ├── static │ ├── LICENSE │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── index.css │ ├── oauth2-redirect.html │ ├── swagger-editor-bundle.js │ ├── swagger-editor-bundle.js.map │ ├── swagger-editor-es-bundle-core.js │ ├── swagger-editor-es-bundle-core.js.map │ ├── swagger-editor-es-bundle.js │ ├── swagger-editor-es-bundle.js.map │ ├── swagger-editor-standalone-preset.js │ ├── swagger-editor-standalone-preset.js.map │ ├── swagger-editor.css │ ├── swagger-editor.css.map │ ├── swagger-editor.js │ ├── swagger-editor.js.map │ ├── swagger-initializer.js │ ├── swagger-ui-bundle.js │ ├── swagger-ui-bundle.js.map │ ├── swagger-ui-es-bundle-core.js │ ├── swagger-ui-es-bundle-core.js.map │ ├── swagger-ui-es-bundle.js │ ├── swagger-ui-es-bundle.js.map │ ├── swagger-ui-standalone-preset.js │ ├── swagger-ui-standalone-preset.js.map │ ├── swagger-ui.css │ ├── swagger-ui.css.map │ ├── swagger-ui.js │ └── swagger-ui.js.map ├── templates │ ├── doc.html │ └── editor.html └── utils.py ├── test ├── __init__.py ├── aiohttp_test.py ├── bottle_test.py ├── chalice_test.py ├── common.py ├── conf │ └── test3.yaml ├── conftest.py ├── falcon_test.py ├── flask_test.py ├── quart_test.py ├── requirements.txt ├── sanic_test.py ├── starlette_test.py └── tornado_test.py ├── tools └── update.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | ignore = E402,W504 4 | -------------------------------------------------------------------------------- /.github/workflows/lint-and-pytest.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - README.md 9 | - 'tools/**' 10 | pull_request: 11 | paths-ignore: 12 | - README.md 13 | - 'tools/**' 14 | 15 | jobs: 16 | lint-and-pytest: 17 | runs-on: ubuntu-20.04 18 | strategy: 19 | matrix: 20 | python-version: ['3.7', '3.8', '3.9', '3.11'] 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - run: python setup.py install 28 | - run: pip install flake8 isort -r test/requirements.txt 29 | - run: isort --line-width=100 --check-only --force-single-line-imports . 30 | - run: flake8 31 | - run: pytest -s test/ 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Releases 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Setup Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: 3.11 20 | - run: | 21 | python -m pip install wheel setuptools 22 | python setup.py sdist 23 | python setup.py bdist_wheel 24 | - name: Generate a token 25 | id: generate_token 26 | uses: actions/create-github-app-token@v1 27 | with: 28 | app_id: ${{ secrets.APP_ID }} 29 | private_key: ${{ secrets.APP_PRIVATE_KEY }} 30 | - uses: ncipollo/release-action@v1 31 | with: 32 | token: ${{ steps.generate_token.outputs.token }} 33 | artifacts: "dist/*.whl,dist/*.tar.gz" 34 | generateReleaseNotes: true 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project 2 | tools/*/ 3 | tools/*.tar.gz 4 | .vscode 5 | .envrc 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | 112 | # Mac 113 | .DS_Store 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 PWZER 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![tests](https://github.com/PWZER/swagger-ui-py/actions/workflows/lint-and-pytest.yml/badge.svg)](https://github.com/PWZER/swagger-ui-py/actions/workflows/lint-and-pytest.yml) 2 | [![Version](https://badge.fury.io/gh/PWZER%2Fswagger-ui-py.svg)](https://github.com/PWZER/swagger-ui-py/tags) 3 | [![PyPi Version](https://img.shields.io/pypi/v/swagger-ui-py.svg)](https://pypi.org/project/swagger-ui-py/) 4 | [![PyPi Downloads](https://pepy.tech/badge/swagger-ui-py)](https://pepy.tech/project/swagger-ui-py) 5 | 6 | [Project Page](https://pwzer.github.io/swagger-ui-py/) 7 | 8 | # swagger-ui-py 9 | Swagger UI for Python web framework, such Tornado, Flask, Quart, aiohttp, Sanic and Falcon. 10 | 11 | Only support Python3. 12 | 13 | ## Supported 14 | 15 | - [Tornado](https://www.tornadoweb.org/en/stable/) 16 | - [Flask](https://flask.palletsprojects.com/) 17 | - [Sanic](https://sanicframework.org/en/) 18 | - [AIOHTTP](https://docs.aiohttp.org/en/stable/) 19 | - [Quart](https://pgjones.gitlab.io/quart/) 20 | - [Starlette](https://www.starlette.io/) 21 | - [Falcon](https://falcon.readthedocs.io/en/stable/) 22 | - [Bottle](https://bottlepy.org/docs/dev/) 23 | - [Chalice](https://aws.github.io/chalice/index.html) 24 | 25 | You can print supported list use command 26 | 27 | ```bash 28 | python3 -c "from swagger_ui import supported_list; print(supported_list)" 29 | ``` 30 | 31 | > If you want to add supported frameworks, you can refer to [Flask Support](/swagger_ui/handlers/flask.py) or [Falcon Support](/swagger_ui/handlers/falcon.py), Implement the corresponding `handler` and `match` function. 32 | 33 | ## Usage 34 | 35 | - Install 36 | 37 | ```bash 38 | pip3 install swagger-ui-py 39 | ``` 40 | 41 | - Code 42 | 43 | Using the local config file 44 | 45 | ```python 46 | from swagger_ui import api_doc 47 | api_doc(app, config_path='./config/test.yaml', url_prefix='/api/doc', title='API doc') 48 | ``` 49 | 50 | Or using config url, but need to suport [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) 51 | 52 | ```python 53 | api_doc(app, config_url='https://petstore.swagger.io/v2/swagger.json', url_prefix='/api/doc', title='API doc') 54 | ``` 55 | 56 | Or using the config spec string 57 | 58 | ```python 59 | from swagger_ui import api_doc 60 | spec_string = '{"openapi":"3.0.1","info":{"title":"python-swagger-ui test api","description":"python-swagger-ui test api","version":"1.0.0"},"servers":[{"url":"http://127.0.0.1:8989/api"}],"tags":[{"name":"default","description":"default tag"}],"paths":{"/hello/world":{"get":{"tags":["default"],"summary":"output hello world.","responses":{"200":{"description":"OK","content":{"application/text":{"schema":{"type":"object","example":"Hello World!!!"}}}}}}}},"components":{}}' 61 | 62 | api_doc(app, config_spec=spec_string, url_prefix='/api/doc', title='API doc') 63 | ``` 64 | 65 | Or using the config dict 66 | 67 | ```python 68 | from swagger_ui import api_doc 69 | config = {"openapi":"3.0.1","info":{"title":"python-swagger-ui test api","description":"python-swagger-ui test api","version":"1.0.0"},"servers":[{"url":"http://127.0.0.1:8989/api"}],"tags":[{"name":"default","description":"default tag"}],"paths":{"/hello/world":{"get":{"tags":["default"],"summary":"output hello world.","responses":{"200":{"description":"OK","content":{"application/text":{"schema":{"type":"object","example":"Hello World!!!"}}}}}}}},"components":{}} 70 | 71 | api_doc(app, config=config, url_prefix='/api/doc', title='API doc') 72 | ``` 73 | 74 | And suport config file with editor 75 | 76 | ```python 77 | api_doc(app, config_path='./config/test.yaml', editor=True) 78 | ``` 79 | 80 | And keep the old way 81 | 82 | ```python 83 | # for Tornado 84 | from swagger_ui import tornado_api_doc 85 | tornado_api_doc(app, config_path='./conf/test.yaml', url_prefix='/api/doc', title='API doc') 86 | 87 | # for Sanic 88 | from swagger_ui import sanic_api_doc 89 | sanic_api_doc(app, config_path='./conf/test.yaml', url_prefix='/api/doc', title='API doc') 90 | 91 | # for Flask 92 | from swagger_ui import flask_api_doc 93 | flask_api_doc(app, config_path='./conf/test.yaml', url_prefix='/api/doc', title='API doc') 94 | 95 | # for Quart 96 | from swagger_ui import quart_api_doc 97 | quart_api_doc(app, config_path='./conf/test.yaml', url_prefix='/api/doc', title='API doc') 98 | 99 | # for aiohttp 100 | from swagger_ui import aiohttp_api_doc 101 | aiohttp_api_doc(app, config_path='./conf/test.yaml', url_prefix='/api/doc', title='API doc') 102 | 103 | # for Falcon 104 | from swagger_ui import falcon_api_doc 105 | falcon_api_doc(app, config_path='./conf/test.yaml', url_prefix='/api/doc', title='API doc') 106 | ``` 107 | Passing a value to the keyword argument `host_inject` will disable the behaviour which injects a host value into the specification served by Swagger UI. 108 | 109 | - Edit `Swagger` config file (JSON or YAML) 110 | 111 | Please see [https://swagger.io/resources/open-api/](https://swagger.io/resources/open-api/). 112 | 113 | - Access 114 | 115 | Open `http://:/api/doc/editor`, you can edit api doc config file. 116 | 117 | Open `http://:/api/doc` view api doc. 118 | 119 | ## SwaggerUI Configuration 120 | 121 | You can configure Swagger parameters using the dictionary, Both key and value are of type str, if value is JavaScript string, you need to wrap the quotes around it. 122 | Such as `"layout": "\"StandaloneLayout\""`. 123 | 124 | ```python 125 | parameters = { 126 | "deepLinking": "true", 127 | "displayRequestDuration": "true", 128 | "layout": "\"StandaloneLayout\"", 129 | "plugins": "[SwaggerUIBundle.plugins.DownloadUrl]", 130 | "presets": "[SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset]", 131 | } 132 | api_doc(app, config_path='./config/test.yaml', parameters=parameters) 133 | ``` 134 | 135 | For details about parameters configuration, see the official documentation [Parameters Configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/). 136 | 137 | ## OAuth2 Configuration 138 | 139 | The format is similar to `parameters`. 140 | 141 | ```python 142 | oauth2_config = { 143 | "clientId": "\"your-client-id\"", 144 | "clientSecret": "\"your-client-secret-if-required\"", 145 | "realm": "\"your-realms\"", 146 | "appName": "\"your-app-name\"", 147 | "scopeSeparator": "\" \"", 148 | "scopes": "\"openid profile\"", 149 | "additionalQueryStringParams": "{test: \"hello\"}", 150 | "usePkceWithAuthorizationCodeGrant": True, 151 | } 152 | api_doc(app, config_path='./config/test.yaml', oauth2_config=oauth2_config) 153 | ``` 154 | 155 | For details about OAuth2 configuration, see the official documentation [OAuth2 Configuration](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/). 156 | 157 | ## Swagger UI 158 | Swagger UI version is `v5.7.2`. see [https://github.com/swagger-api/swagger-ui](https://github.com/swagger-api/swagger-ui). 159 | 160 | ## Swagger Editor 161 | Swagger Editor version is `v4.11.1`. see [https://github.com/swagger-api/swagger-editor](https://github.com/swagger-api/swagger-editor). 162 | 163 | ## Update 164 | You can update swagger ui and swagger editor version with 165 | 166 | ```bash 167 | python3 tools/update.py --ui --editor 168 | ``` 169 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /examples/aiohttp_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from aiohttp import web 4 | 5 | 6 | async def hello(request): 7 | return web.Response(text="Hello, world") 8 | 9 | 10 | app = web.Application() 11 | app.add_routes([web.get('/hello/world', hello)]) 12 | 13 | 14 | if __name__ == '__main__': 15 | working_dir = os.path.dirname(os.path.abspath(__file__)) 16 | config_path = os.path.join(working_dir, 'conf/test.yaml') 17 | 18 | from swagger_ui import api_doc 19 | api_doc(app, config_path=config_path) 20 | web.run_app(app, port=8989) 21 | -------------------------------------------------------------------------------- /examples/bottle_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from bottle import Bottle 4 | from bottle import run 5 | 6 | app = Bottle() 7 | 8 | 9 | @app.route('/hello/world') 10 | def hello(): 11 | return 'Hello World!!!' 12 | 13 | 14 | if __name__ == '__main__': 15 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 16 | config_path = os.path.join(cur_dir, 'conf/test.yaml') 17 | 18 | from swagger_ui import api_doc 19 | api_doc(app, config_path=config_path, url_prefix='/api/doc') 20 | run(app, host='0.0.0.0', port=8989, debug=True) 21 | -------------------------------------------------------------------------------- /examples/chalice_test/.chalice/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "app_name": "chalice_test", 4 | "stages": { 5 | "dev": { 6 | "api_gateway_stage": "api" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/chalice_test/README.md: -------------------------------------------------------------------------------- 1 | ## swagger-ui-py for chalice 2 | 3 | ### Run Server 4 | 5 | ```bash 6 | chalice local 7 | ``` 8 | 9 | ### Check 10 | 11 | Visit the url in the browser [http://127.0.0.1:8000/api/doc](http://127.0.0.1:8000/api/doc) 12 | -------------------------------------------------------------------------------- /examples/chalice_test/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from chalice import Chalice 4 | 5 | app = Chalice(app_name='chalice_test') 6 | 7 | 8 | @app.route('/hello/world') 9 | def index(): 10 | return {'hello': 'world'} 11 | 12 | 13 | working_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 14 | config_path = os.path.join(working_dir, 'conf/test.yaml') 15 | 16 | from swagger_ui import api_doc 17 | 18 | api_doc(app, config_path=config_path, url_prefix='/api/doc') 19 | -------------------------------------------------------------------------------- /examples/conf/test.yaml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | # host: '127.0.0.1:8989' 3 | schemes: 4 | - http 5 | basePath: / 6 | info: 7 | version: 1.0.0 8 | title: python-swagger-ui test api 9 | description: python-swagger-ui test api 10 | tags: 11 | - name: default 12 | description: default tag 13 | 14 | paths: 15 | /hello/world: 16 | get: 17 | summary: output hello world. 18 | produces: 19 | - application/text 20 | responses: 21 | '200': 22 | description: OK 23 | schema: 24 | example: "Hello World!!!" 25 | tags: 26 | - default 27 | -------------------------------------------------------------------------------- /examples/conf/test3.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: python-swagger-ui test api 4 | description: python-swagger-ui test api 5 | version: 1.0.0 6 | servers: 7 | - url: http://127.0.0.1:8989/api 8 | tags: 9 | - name: default 10 | description: default tag 11 | paths: 12 | /hello/world: 13 | get: 14 | tags: 15 | - default 16 | summary: output hello world. 17 | responses: 18 | 200: 19 | description: OK 20 | content: 21 | application/text: 22 | schema: 23 | type: object 24 | example: Hello World!!! 25 | components: {} 26 | -------------------------------------------------------------------------------- /examples/falcon_asgi_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import falcon.asgi 5 | import uvicorn 6 | 7 | 8 | class HelloWorldResource(object): 9 | async def on_get(self, req, resp): 10 | resp.body = json.dumps({'text': 'Hello World!!!'}) 11 | 12 | 13 | def create_app(): 14 | working_dir = os.path.dirname(os.path.abspath(__file__)) 15 | config_path = os.path.join(working_dir, 'conf/test.yaml') 16 | 17 | app = falcon.asgi.App() 18 | app.add_route('/hello/world', HelloWorldResource()) 19 | 20 | from swagger_ui import api_doc 21 | api_doc(app, config_path=config_path, url_prefix='/api/doc') 22 | return app 23 | 24 | 25 | uvicorn.run(create_app(), host="0.0.0.0", port=8989, log_level="info") 26 | -------------------------------------------------------------------------------- /examples/falcon_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from wsgiref import simple_server 4 | 5 | import falcon 6 | from packaging.version import Version 7 | 8 | 9 | class HelloWorldResource(object): 10 | def on_get(self, req, resp): 11 | resp.body = json.dumps({'text': 'Hello World!!!'}) 12 | 13 | 14 | if Version(falcon.__version__) < Version('3.0.0'): 15 | app = falcon.API() 16 | else: 17 | app = falcon.App() 18 | 19 | app.add_route('/hello/world', HelloWorldResource()) 20 | 21 | if __name__ == '__main__': 22 | working_dir = os.path.dirname(os.path.abspath(__file__)) 23 | config_path = os.path.join(working_dir, 'conf/test.yaml') 24 | 25 | # from swagger_ui import api_doc 26 | # api_doc(app, config_path=config_path, url_prefix='/api/doc') 27 | 28 | from swagger_ui import falcon_api_doc 29 | falcon_api_doc(app, config_path=config_path, url_prefix='/api/doc') 30 | 31 | httpd = simple_server.make_server('0.0.0.0', 8989, app) 32 | httpd.serve_forever() 33 | -------------------------------------------------------------------------------- /examples/flask_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | 5 | app = Flask(__name__) 6 | 7 | 8 | @app.route(r'/hello/world') 9 | def hello(): 10 | return 'Hello World!!!' 11 | 12 | 13 | if __name__ == '__main__': 14 | working_dir = os.path.dirname(os.path.abspath(__file__)) 15 | config_path = os.path.join(working_dir, 'conf/test.yaml') 16 | 17 | from swagger_ui import api_doc 18 | api_doc(app, config_path=config_path) 19 | 20 | app.run(host='0.0.0.0', port=8989) 21 | -------------------------------------------------------------------------------- /examples/quart_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from quart import Quart 4 | 5 | app = Quart(__name__) 6 | 7 | 8 | @app.route(r'/hello/world', methods=['GET']) 9 | async def index_handler(): 10 | return 'Hello World!!!' 11 | 12 | 13 | if __name__ == '__main__': 14 | working_dir = os.path.dirname(os.path.abspath(__file__)) 15 | config_path = os.path.join(working_dir, 'conf/test.yaml') 16 | 17 | from swagger_ui import api_doc 18 | api_doc(app, config_path=config_path) 19 | 20 | app.run(host='0.0.0.0', port=8989) 21 | -------------------------------------------------------------------------------- /examples/sanic_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from sanic import Sanic 4 | from sanic import response 5 | 6 | app = Sanic(__name__) 7 | 8 | 9 | @app.get(r'/hello/world') 10 | async def index_handler(request): 11 | return response.text('Hello World!!!') 12 | 13 | 14 | if __name__ == '__main__': 15 | working_dir = os.path.dirname(os.path.abspath(__file__)) 16 | config_path = os.path.join(working_dir, 'conf/test.yaml') 17 | 18 | from swagger_ui import api_doc 19 | api_doc(app, config_path=config_path) 20 | 21 | app.run(host='0.0.0.0', port=8989, single_process=True) 22 | -------------------------------------------------------------------------------- /examples/starlette_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import uvicorn 4 | from starlette.applications import Starlette 5 | from starlette.responses import PlainTextResponse 6 | from starlette.routing import Route 7 | 8 | 9 | def hello_world(request): 10 | return PlainTextResponse('Hello World!!!') 11 | 12 | 13 | def startup(): 14 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 15 | config_path = os.path.join(cur_dir, 'conf/test.yaml') 16 | 17 | from swagger_ui import api_doc 18 | api_doc(app, config_path=config_path) 19 | 20 | 21 | routes = [ 22 | Route('/hello/world', hello_world), 23 | ] 24 | 25 | app = Starlette(debug=True, routes=routes, on_startup=[startup]) 26 | 27 | if __name__ == '__main__': 28 | uvicorn.run(app, host="0.0.0.0", port=8989, log_level="info") 29 | -------------------------------------------------------------------------------- /examples/tornado_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import tornado.ioloop 4 | import tornado.web 5 | 6 | 7 | class HelloWorldHandler(tornado.web.RequestHandler): 8 | def get(self, *args, **kwargs): 9 | return self.write('Hello World!!!') 10 | 11 | 12 | def make_app(): 13 | return tornado.web.Application([ 14 | (r'/hello/world', HelloWorldHandler), 15 | ]) 16 | 17 | 18 | if __name__ == '__main__': 19 | app = make_app() 20 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 21 | config_path = os.path.join(cur_dir, 'conf/test.yaml') 22 | 23 | # from swagger_ui import tornado_api_doc 24 | # tornado_api_doc(app, config_path=config_path, url_prefix='/api/doc/') 25 | 26 | from swagger_ui import api_doc 27 | api_doc(app, config_path=config_path, url_prefix='/api/doc/', editor=True) 28 | 29 | app.listen(8989) 30 | tornado.ioloop.IOLoop.current().start() 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from setuptools import find_packages 4 | from setuptools import setup 5 | 6 | setup( 7 | name='swagger-ui-py', 8 | version='23.9.23', 9 | python_requires='>=3.0.0', 10 | description=( 11 | 'Swagger UI for Python web framework, ' 12 | 'such as Tornado, Flask, Quart, Sanic and Falcon.' 13 | ), 14 | long_description=Path(__file__).parent.joinpath('README.md').read_text(), 15 | long_description_content_type='text/markdown', 16 | license='Apache License 2.0', 17 | packages=find_packages(), 18 | package_data={ 19 | 'swagger_ui': ['static/*', 'templates/*'], 20 | }, 21 | install_requires=[ 22 | "jinja2>=2.0", 23 | "packaging>=20.0", 24 | "PyYaml>=5.0", 25 | ], 26 | url='https://github.com/PWZER/swagger-ui-py', 27 | author='PWZER', 28 | author_email='pwzergo@gmail.com', 29 | ) 30 | -------------------------------------------------------------------------------- /swagger_ui/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from swagger_ui import core 4 | from swagger_ui.handlers import supported_list 5 | 6 | 7 | def api_doc(app, **kwargs): 8 | doc = core.ApplicationDocument(app, **kwargs) 9 | 10 | handler = doc.match_handler() 11 | 12 | if not handler: 13 | raise Exception('No match application isinstance type!') 14 | 15 | if not callable(handler): 16 | raise Exception('handler is callable required!') 17 | 18 | return handler(doc) 19 | 20 | 21 | def create_api_doc(app_type): 22 | def _api_doc(app, **kwargs): 23 | kwargs['app_type'] = app_type 24 | return api_doc(app, **kwargs) 25 | return _api_doc 26 | 27 | 28 | for name in supported_list: 29 | setattr(sys.modules[__name__], '{}_api_doc'.format(name), create_api_doc(name)) 30 | -------------------------------------------------------------------------------- /swagger_ui/core.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import importlib 3 | import urllib.request 4 | from pathlib import Path 5 | 6 | from jinja2 import Environment 7 | from jinja2 import FileSystemLoader 8 | from jinja2 import select_autoescape 9 | 10 | from swagger_ui.handlers import supported_list 11 | from swagger_ui.utils import SWAGGER_UI_PY_ROOT 12 | from swagger_ui.utils import _load_config 13 | 14 | _DefaultSwaggerUIBundleParameters = { 15 | "dom_id": "\"#swagger-ui\"", 16 | "deepLinking": "true", 17 | "displayRequestDuration": "true", 18 | "layout": "\"StandaloneLayout\"", 19 | "plugins": "[SwaggerUIBundle.plugins.DownloadUrl]", 20 | "presets": "[SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset]", 21 | } 22 | 23 | 24 | class ApplicationDocument(object): 25 | 26 | def __init__(self, 27 | app, 28 | app_type=None, 29 | config=None, 30 | config_path=None, 31 | config_url=None, 32 | config_spec=None, 33 | config_rel_url=None, 34 | url_prefix=r'/api/doc', 35 | title='API doc', 36 | editor=False, 37 | parameters={}, 38 | oauth2_config={}, 39 | **extra_config): 40 | self.app = app 41 | self.app_type = app_type 42 | self.title = title 43 | self.url_prefix = url_prefix.rstrip('/') 44 | self.editor = editor 45 | self.extra_config = extra_config 46 | 47 | self.config = config 48 | self.config_url = config_url 49 | self.config_path = config_path 50 | self.config_spec = config_spec 51 | self.config_rel_url = config_rel_url 52 | assert (self.config or self.config_url or self.config_path or self.config_spec or 53 | self.config_rel_url), \ 54 | 'One of arguments "config", "config_path", "config_url", "config_spec"' \ 55 | ' or "config_rel_url" is required!' 56 | 57 | # parameters 58 | self.parameters = copy.deepcopy(_DefaultSwaggerUIBundleParameters) 59 | if parameters: 60 | self.parameters.update(parameters) 61 | self.parameters["url"] = "\"{}\"".format(self.swagger_json_uri_absolute) 62 | 63 | # oauth2_config 64 | self.oauth2_config = oauth2_config 65 | 66 | self.env = Environment( 67 | loader=FileSystemLoader( 68 | str(SWAGGER_UI_PY_ROOT.joinpath('templates'))), 69 | autoescape=select_autoescape(['html']), 70 | ) 71 | 72 | @property 73 | def blueprint_name(self): 74 | return 'bp{}'.format(self.url_prefix.replace('/', '_')) 75 | 76 | @property 77 | def static_dir(self): 78 | return str(SWAGGER_UI_PY_ROOT.joinpath('static')) 79 | 80 | @property 81 | def doc_html(self): 82 | return self.env.get_template('doc.html').render( 83 | url_prefix=self.url_prefix, 84 | title=self.title, 85 | config_url=self.swagger_json_uri_absolute, 86 | parameters=self.parameters, 87 | oauth2_config=self.oauth2_config, 88 | ) 89 | 90 | @property 91 | def editor_html(self): 92 | return self.env.get_template('editor.html').render( 93 | url_prefix=self.url_prefix, 94 | title=self.title, 95 | config_url=self.swagger_json_uri_absolute, 96 | parameters=self.parameters, 97 | ) 98 | 99 | def uri(self, suffix=''): 100 | return r'{}{}'.format(self.url_prefix, suffix) 101 | 102 | @property 103 | def static_uri_relative(self): 104 | return r'/static' 105 | 106 | @property 107 | def static_uri_absolute(self): 108 | return self.uri(self.static_uri_relative) 109 | 110 | @property 111 | def swagger_json_uri_relative(self): 112 | return self.config_rel_url or r'/swagger.json' 113 | 114 | @property 115 | def swagger_json_uri_absolute(self): 116 | return self.uri(self.swagger_json_uri_relative) 117 | 118 | def root_uri_relative(self, slashes=False): 119 | return r'/' if slashes else r'' 120 | 121 | def root_uri_absolute(self, slashes=False): 122 | return self.uri(self.root_uri_relative(slashes)) 123 | 124 | def editor_uri_relative(self, slashes=False): 125 | return r'/editor/' if slashes else r'/editor' 126 | 127 | def editor_uri_absolute(self, slashes=False): 128 | return self.uri(self.editor_uri_relative(slashes)) 129 | 130 | def get_config(self, host): 131 | if self.config: 132 | config = self.config 133 | elif self.config_path: 134 | assert Path(self.config_path).is_file() 135 | 136 | with open(self.config_path, 'rb') as config_file: 137 | config = _load_config(config_file.read()) 138 | elif self.config_url: 139 | with urllib.request.urlopen(self.config_url) as config_file: 140 | config = _load_config(config_file.read()) 141 | elif self.config_spec: 142 | config = _load_config(self.config_spec) 143 | else: 144 | raise RuntimeError('No config found!') 145 | 146 | if 'host' not in config and self.extra_config.get('host_inject', True): 147 | config['host'] = host 148 | return config 149 | 150 | def match_handler(self): 151 | 152 | def match(name): 153 | mod = importlib.import_module( 154 | 'swagger_ui.handlers.{}'.format(name)) 155 | return hasattr(mod, 'match') and mod.match(self) 156 | 157 | if self.app_type: 158 | return match(self.app_type) 159 | 160 | for name in supported_list: 161 | handler = match(name) 162 | if handler: 163 | return handler 164 | return None 165 | -------------------------------------------------------------------------------- /swagger_ui/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | import pkgutil 2 | 3 | supported_list = [ 4 | name for _, name, _ in pkgutil.iter_modules(__path__) 5 | if not name.startswith('_') 6 | ] 7 | -------------------------------------------------------------------------------- /swagger_ui/handlers/aiohttp.py: -------------------------------------------------------------------------------- 1 | def handler(doc): 2 | from aiohttp import web 3 | 4 | async def swagger_doc_handler(request): 5 | return web.Response(text=doc.doc_html, content_type='text/html') 6 | 7 | async def swagger_editor_handler(request): 8 | return web.Response(text=doc.editor_html, content_type='text/html') 9 | 10 | async def swagger_config_handler(request): 11 | return web.json_response(doc.get_config(request.host)) 12 | 13 | doc.app.router.add_get( 14 | doc.root_uri_absolute(slashes=True), swagger_doc_handler) 15 | doc.app.router.add_get( 16 | doc.root_uri_absolute(slashes=False), swagger_doc_handler) 17 | 18 | if doc.editor: 19 | doc.app.router.add_get( 20 | doc.editor_uri_absolute(slashes=True), swagger_editor_handler) 21 | doc.app.router.add_get( 22 | doc.editor_uri_absolute(slashes=False), swagger_editor_handler) 23 | 24 | if doc.config_rel_url is None: 25 | doc.app.router.add_get( 26 | doc.swagger_json_uri_absolute, swagger_config_handler) 27 | doc.app.router.add_static(doc.static_uri_absolute, path=doc.static_dir) 28 | 29 | 30 | def match(doc): 31 | try: 32 | import aiohttp.web 33 | if isinstance(doc.app, aiohttp.web.Application): 34 | return handler 35 | except ImportError: 36 | pass 37 | return None 38 | -------------------------------------------------------------------------------- /swagger_ui/handlers/bottle.py: -------------------------------------------------------------------------------- 1 | def handler(doc): 2 | from bottle import request 3 | from bottle import static_file 4 | 5 | @doc.app.get(doc.root_uri_absolute(slashes=True)) 6 | @doc.app.get(doc.root_uri_absolute(slashes=False)) 7 | def index(): 8 | return doc.doc_html 9 | 10 | if doc.config_rel_url is None: 11 | @doc.app.get(doc.swagger_json_uri_absolute) 12 | def config_handler(): 13 | return doc.get_config(request.urlparts.netloc) 14 | 15 | @doc.app.get(r'{}/'.format(doc.static_uri_absolute)) 16 | def java_script_file(filepath): 17 | return static_file(filepath, root=doc.static_dir) 18 | 19 | if doc.editor: 20 | @doc.app.get(doc.editor_uri_absolute(slashes=True)) 21 | @doc.app.get(doc.editor_uri_absolute(slashes=False)) 22 | def editor(): 23 | return doc.editor_html 24 | 25 | 26 | def match(doc): 27 | try: 28 | from bottle import Bottle 29 | if isinstance(doc.app, Bottle): 30 | return handler 31 | except ImportError: 32 | pass 33 | return None 34 | -------------------------------------------------------------------------------- /swagger_ui/handlers/chalice.py: -------------------------------------------------------------------------------- 1 | def handler(doc): 2 | from pathlib import Path 3 | 4 | from chalice import Blueprint 5 | from chalice import NotFoundError 6 | from chalice import Response 7 | 8 | cur_dir = Path(__file__).resolve().parent 9 | static_dir = cur_dir.parent.joinpath("static") 10 | 11 | bp = Blueprint(__name__) 12 | 13 | @bp.route(doc.root_uri_relative(slashes=True), methods=["GET"]) 14 | def bp_doc_handler(): 15 | return Response( 16 | body=doc.doc_html, 17 | status_code=200, 18 | headers={"Content-Type": "text/html"}, 19 | ) 20 | 21 | if doc.config_rel_url is None: 22 | @bp.route(doc.swagger_json_uri_relative, methods=["GET"]) 23 | def bp_config_handler(): 24 | request = doc.app.current_request 25 | return doc.get_config(request.headers["host"]) 26 | 27 | if doc.editor: 28 | @bp.route(doc.editor_uri_relative(slashes=True), methods=["GET"]) 29 | def bp_editor_handler(): 30 | return Response( 31 | body=doc.editor_html, 32 | status_code=200, 33 | headers={"Content-Type": "text/html"}, 34 | ) 35 | 36 | @bp.route(doc.static_uri_relative + r"/{path}", methods=["GET"]) 37 | def bp_static_handler(path): 38 | static_file_path = static_dir.joinpath(path) 39 | if static_file_path.is_file(): 40 | content_type = "application/json" 41 | if static_file_path.suffix in [".png", ".ico"]: 42 | content_type = "image/png" 43 | if static_file_path.suffix in [".jpg", ".jpeg"]: 44 | content_type = "image/jpeg" 45 | if static_file_path.suffix in [".css"]: 46 | content_type = "text/css" 47 | if static_file_path.suffix in [".js"]: 48 | content_type = "text/javascript" 49 | return Response( 50 | body=static_file_path.read_bytes(), 51 | status_code=200, 52 | headers={"Content-Type": content_type}, 53 | ) 54 | return NotFoundError(path) 55 | 56 | doc.app.register_blueprint(bp, url_prefix=doc.url_prefix) 57 | 58 | 59 | def match(doc): 60 | try: 61 | from chalice import Chalice 62 | if isinstance(doc.app, Chalice): 63 | return handler 64 | except ImportError: 65 | pass 66 | return None 67 | -------------------------------------------------------------------------------- /swagger_ui/handlers/falcon.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from packaging.version import Version 4 | 5 | 6 | class FalconInterface(object): 7 | def __init__(self, use_async=False): 8 | self.use_async = use_async 9 | 10 | def handler(self, doc): 11 | 12 | class Handler(object): 13 | async def on_get_async(self, req, resp): 14 | self.on_get(req, resp) 15 | 16 | class SwaggerDocHandler(Handler): 17 | def on_get(self, req, resp): 18 | resp.content_type = 'text/html' 19 | resp.body = doc.doc_html 20 | 21 | class SwaggerEditorHandler(Handler): 22 | def on_get(self, req, resp): 23 | resp.content_type = 'text/html' 24 | resp.body = doc.editor_html 25 | 26 | class SwaggerConfigHandler(Handler): 27 | def on_get(self, req, resp): 28 | resp.content_type = 'application/json' 29 | resp.body = json.dumps(doc.get_config(f'{req.host}:{req.port}')) 30 | 31 | suffix = 'async' if self.use_async else None 32 | doc.app.add_route(doc.root_uri_absolute(slashes=True), 33 | SwaggerDocHandler(), suffix=suffix) 34 | doc.app.add_route(doc.root_uri_absolute(slashes=False), 35 | SwaggerDocHandler(), suffix=suffix) 36 | 37 | if doc.editor: 38 | doc.app.add_route(doc.editor_uri_absolute(slashes=True), 39 | SwaggerEditorHandler(), suffix=suffix) 40 | doc.app.add_route(doc.editor_uri_absolute(slashes=False), 41 | SwaggerEditorHandler(), suffix=suffix) 42 | 43 | if doc.config_rel_url is None: 44 | doc.app.add_route(doc.swagger_json_uri_absolute, SwaggerConfigHandler(), suffix=suffix) 45 | doc.app.add_static_route( 46 | prefix=doc.static_uri_absolute, 47 | directory='{}/'.format(doc.static_dir), 48 | downloadable=True, 49 | ) 50 | 51 | 52 | def match(doc): 53 | try: 54 | import falcon 55 | 56 | interface = None 57 | if Version(falcon.__version__) >= Version('3.0.0'): 58 | import falcon.asgi 59 | if isinstance(doc.app, falcon.asgi.App): 60 | interface = FalconInterface(use_async=True) 61 | elif isinstance(doc.app, falcon.App): 62 | interface = FalconInterface(use_async=False) 63 | else: 64 | if isinstance(doc.app, falcon.API): 65 | interface = FalconInterface(use_async=False) 66 | 67 | if interface: 68 | return interface.handler 69 | except ImportError: 70 | pass 71 | return None 72 | -------------------------------------------------------------------------------- /swagger_ui/handlers/flask.py: -------------------------------------------------------------------------------- 1 | def handler(doc): 2 | from flask import jsonify 3 | from flask import request 4 | from flask.blueprints import Blueprint 5 | 6 | swagger_blueprint = Blueprint( 7 | doc.blueprint_name, 8 | __name__, 9 | url_prefix=doc.url_prefix, 10 | static_folder=doc.static_dir, 11 | static_url_path=doc.static_uri_relative, 12 | ) 13 | 14 | @swagger_blueprint.route(doc.root_uri_relative(slashes=True)) 15 | @swagger_blueprint.route(doc.root_uri_relative(slashes=False)) 16 | def swagger_blueprint_doc_handler(): 17 | return doc.doc_html 18 | 19 | if doc.editor: 20 | @swagger_blueprint.route(doc.editor_uri_relative(slashes=True)) 21 | @swagger_blueprint.route(doc.editor_uri_relative(slashes=False)) 22 | def swagger_blueprint_editor_handler(): 23 | return doc.editor_html 24 | 25 | if doc.config_rel_url is None: 26 | @swagger_blueprint.route(doc.swagger_json_uri_relative) 27 | def swagger_blueprint_config_handler(): 28 | return jsonify(doc.get_config(request.host)) 29 | 30 | doc.app.register_blueprint(swagger_blueprint) 31 | 32 | 33 | def match(doc): 34 | try: 35 | import flask 36 | if isinstance(doc.app, flask.Flask): 37 | return handler 38 | except ImportError: 39 | pass 40 | return None 41 | -------------------------------------------------------------------------------- /swagger_ui/handlers/quart.py: -------------------------------------------------------------------------------- 1 | def handler(doc): 2 | from quart import Blueprint 3 | from quart import request 4 | from quart.json import jsonify 5 | 6 | swagger_blueprint = Blueprint( 7 | doc.blueprint_name, 8 | __name__, 9 | url_prefix=doc.url_prefix, 10 | static_url_path=doc.static_uri_relative, 11 | static_folder=doc.static_dir, 12 | root_path=doc.static_dir 13 | ) 14 | 15 | @swagger_blueprint.route( 16 | doc.root_uri_relative(slashes=True), methods=['GET']) 17 | async def swagger_blueprint_doc_handler(): 18 | return doc.doc_html 19 | 20 | if doc.editor: 21 | @swagger_blueprint.route( 22 | doc.editor_uri_relative(slashes=True), methods=['GET']) 23 | async def swagger_blueprint_editor_handler(): 24 | return doc.editor_html 25 | 26 | if doc.config_rel_url is None: 27 | @swagger_blueprint.route(doc.swagger_json_uri_relative, methods=['GET']) 28 | async def swagger_blueprint_config_handler(): 29 | return jsonify(doc.get_config(request.host)) 30 | 31 | doc.app.register_blueprint( 32 | blueprint=swagger_blueprint, url_prefix=doc.url_prefix) 33 | 34 | 35 | def match(doc): 36 | try: 37 | import quart 38 | if isinstance(doc.app, quart.Quart): 39 | return handler 40 | except ImportError: 41 | pass 42 | return None 43 | -------------------------------------------------------------------------------- /swagger_ui/handlers/sanic.py: -------------------------------------------------------------------------------- 1 | def handler(doc): 2 | from sanic import response 3 | from sanic.blueprints import Blueprint 4 | 5 | swagger_blueprint = Blueprint( 6 | doc.blueprint_name, 7 | url_prefix=doc.url_prefix, 8 | strict_slashes=False, 9 | ) 10 | 11 | @swagger_blueprint.get(doc.root_uri_relative(slashes=True)) 12 | async def swagger_blueprint_doc_handler(request): 13 | return response.html(doc.doc_html) 14 | 15 | if doc.editor: 16 | @swagger_blueprint.get(doc.editor_uri_relative(slashes=True)) 17 | async def swagger_blueprint_editor_handler(request): 18 | return response.html(doc.editor_html) 19 | 20 | if doc.config_rel_url is None: 21 | @swagger_blueprint.get(doc.swagger_json_uri_relative) 22 | async def swagger_blueprint_config_handler(request): 23 | return response.json(doc.get_config(request.host)) 24 | 25 | swagger_blueprint.static(doc.static_uri_relative, doc.static_dir) 26 | doc.app.blueprint(swagger_blueprint) 27 | 28 | 29 | def match(doc): 30 | try: 31 | import sanic 32 | if isinstance(doc.app, sanic.Sanic): 33 | return handler 34 | except ImportError: 35 | pass 36 | return None 37 | -------------------------------------------------------------------------------- /swagger_ui/handlers/starlette.py: -------------------------------------------------------------------------------- 1 | def handler(doc): 2 | from starlette.responses import HTMLResponse 3 | from starlette.responses import JSONResponse 4 | from starlette.staticfiles import StaticFiles 5 | 6 | async def swagger_doc_handler(request): 7 | return HTMLResponse(content=doc.doc_html, media_type='text/html') 8 | 9 | async def swagger_editor_handler(request): 10 | return JSONResponse(content=doc.editor_html, media_type='text/html') 11 | 12 | async def swagger_config_handler(request): 13 | host = '{}:{}'.format(request.url.hostname, request.url.port) 14 | return JSONResponse(doc.get_config(host)) 15 | 16 | doc.app.router.add_route( 17 | doc.root_uri_absolute(slashes=True), swagger_doc_handler, 18 | ['get'], 'swagger-ui', 19 | ) 20 | doc.app.router.add_route( 21 | doc.root_uri_absolute(slashes=False), swagger_doc_handler, 22 | ['get'], 'swagger-ui', 23 | ) 24 | 25 | if doc.editor: 26 | doc.app.router.add_route( 27 | doc.editor_uri_absolute(slashes=True), swagger_doc_handler, 28 | ['get'], 'swagger-editor', 29 | ) 30 | doc.app.router.add_route( 31 | doc.editor_uri_absolute(slashes=False), swagger_doc_handler, 32 | ['get'], 'swagger-editor', 33 | ) 34 | 35 | if doc.config_rel_url is None: 36 | doc.app.router.add_route( 37 | doc.swagger_json_uri_absolute, swagger_config_handler, 38 | ['get'], 'swagger-config', 39 | ) 40 | doc.app.router.mount( 41 | doc.static_uri_absolute, 42 | app=StaticFiles(directory='{}/'.format(doc.static_dir)), 43 | name='swagger-static-files', 44 | ) 45 | 46 | 47 | def match(doc): 48 | try: 49 | import starlette.applications 50 | if isinstance(doc.app, starlette.applications.Starlette): 51 | return handler 52 | except ImportError: 53 | pass 54 | return None 55 | -------------------------------------------------------------------------------- /swagger_ui/handlers/tornado.py: -------------------------------------------------------------------------------- 1 | def handler(doc): 2 | from tornado.web import RequestHandler 3 | from tornado.web import StaticFileHandler 4 | 5 | class DocHandler(RequestHandler): 6 | def get(self, *args, **kwargs): 7 | return self.write(doc.doc_html) 8 | 9 | class EditorHandler(RequestHandler): 10 | def get(self, *args, **kwargs): 11 | return self.write(doc.editor_html) 12 | 13 | class ConfigHandler(RequestHandler): 14 | def get(self, *args, **kwargs): 15 | return self.write(doc.get_config(self.request.host)) 16 | 17 | handlers = [ 18 | (doc.root_uri_absolute(slashes=True), DocHandler), 19 | (doc.root_uri_absolute(slashes=False), DocHandler), 20 | (r'{}/(.+)'.format(doc.static_uri_absolute), 21 | StaticFileHandler, {'path': doc.static_dir}), 22 | ] 23 | 24 | if doc.config_rel_url is None: 25 | handlers.append((doc.swagger_json_uri_absolute, ConfigHandler)) 26 | 27 | if doc.editor: 28 | handlers += [ 29 | (doc.editor_uri_absolute(slashes=True), EditorHandler), 30 | (doc.editor_uri_absolute(slashes=False), EditorHandler), 31 | ] 32 | 33 | doc.app.add_handlers(r'.*', handlers) 34 | 35 | 36 | def match(doc): 37 | try: 38 | import tornado.web 39 | if isinstance(doc.app, tornado.web.Application): 40 | return handler 41 | except ImportError: 42 | pass 43 | return None 44 | -------------------------------------------------------------------------------- /swagger_ui/static/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /swagger_ui/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PWZER/swagger-ui-py/21c62701c12d7a5b47f340c1631500cdaa5ef00f/swagger_ui/static/favicon-16x16.png -------------------------------------------------------------------------------- /swagger_ui/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PWZER/swagger-ui-py/21c62701c12d7a5b47f340c1631500cdaa5ef00f/swagger_ui/static/favicon-32x32.png -------------------------------------------------------------------------------- /swagger_ui/static/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | overflow: -moz-scrollbars-vertical; 4 | overflow-y: scroll; 5 | } 6 | 7 | *, 8 | *:before, 9 | *:after { 10 | box-sizing: inherit; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | background: #fafafa; 16 | } 17 | -------------------------------------------------------------------------------- /swagger_ui/static/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Swagger UI: OAuth2 Redirect 4 | 5 | 6 | 7 | 69 | -------------------------------------------------------------------------------- /swagger_ui/static/swagger-initializer.js: -------------------------------------------------------------------------------- 1 | window.onload = function() { 2 | // 3 | 4 | // the following lines will be replaced by docker/configurator, when it runs in a docker-container 5 | window.ui = SwaggerUIBundle({ 6 | url: "https://petstore.swagger.io/v2/swagger.json", 7 | dom_id: '#swagger-ui', 8 | deepLinking: true, 9 | presets: [ 10 | SwaggerUIBundle.presets.apis, 11 | SwaggerUIStandalonePreset 12 | ], 13 | plugins: [ 14 | SwaggerUIBundle.plugins.DownloadUrl 15 | ], 16 | layout: "StandaloneLayout" 17 | }); 18 | 19 | // 20 | }; 21 | -------------------------------------------------------------------------------- /swagger_ui/templates/doc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /swagger_ui/templates/editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ title }} 7 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /swagger_ui/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import yaml 5 | 6 | SWAGGER_UI_PY_ROOT = Path(__file__).resolve().parent 7 | 8 | 9 | def _load_config(content): 10 | try: 11 | return json.loads(content) 12 | except ValueError: 13 | pass 14 | 15 | try: 16 | return yaml.load(content, Loader=yaml.FullLoader) 17 | except yaml.YAMLError: 18 | pass 19 | 20 | raise Exception('Invalid swagger config format!') 21 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PWZER/swagger-ui-py/21c62701c12d7a5b47f340c1631500cdaa5ef00f/test/__init__.py -------------------------------------------------------------------------------- /test/aiohttp_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from aiohttp import web 3 | 4 | from swagger_ui import aiohttp_api_doc 5 | from swagger_ui import api_doc 6 | 7 | from .common import config_content 8 | from .common import parametrize_list 9 | 10 | 11 | @pytest.fixture 12 | def app(): 13 | async def hello(request): 14 | return web.Response(text="Hello, world") 15 | 16 | app = web.Application() 17 | app.add_routes([web.get('/hello/world', hello)]) 18 | return app 19 | 20 | 21 | @pytest.mark.asyncio 22 | @pytest.mark.parametrize('mode, kwargs', parametrize_list) 23 | async def test_aiohttp(app, aiohttp_client, mode, kwargs): 24 | if kwargs.get('config_rel_url'): 25 | async def swagger_config_handler(request): 26 | return web.Response(text=config_content) 27 | app.add_routes([web.get(kwargs['config_rel_url'], swagger_config_handler)]) 28 | 29 | if mode == 'auto': 30 | api_doc(app, **kwargs) 31 | else: 32 | aiohttp_api_doc(app, **kwargs) 33 | 34 | url_prefix = kwargs['url_prefix'] 35 | if url_prefix.endswith('/'): 36 | url_prefix = url_prefix[:-1] 37 | 38 | client = await aiohttp_client(app) 39 | 40 | resp = await client.get('/hello/world') 41 | assert resp.status == 200, await resp.text() 42 | 43 | resp = await client.get(url_prefix) 44 | assert resp.status == 200, await resp.text() 45 | 46 | resp = await client.get(f'{url_prefix}/static/LICENSE') 47 | assert resp.status == 200, await resp.text() 48 | 49 | if kwargs.get('editor'): 50 | resp = await client.get(f'{url_prefix}/editor') 51 | assert resp.status == 200, await resp.text() 52 | else: 53 | resp = await client.get(f'{url_prefix}/editor') 54 | assert resp.status == 404, await resp.text() 55 | 56 | if kwargs.get('config_rel_url'): 57 | resp = await client.get(kwargs['config_rel_url']) 58 | assert resp.status == 200, await resp.text() 59 | else: 60 | resp = await client.get(f'{url_prefix}/swagger.json') 61 | assert resp.status == 200, await resp.text() 62 | -------------------------------------------------------------------------------- /test/bottle_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from bottle import Bottle 3 | from webtest import AppError 4 | from webtest import TestApp 5 | 6 | from swagger_ui import api_doc 7 | from swagger_ui import bottle_api_doc 8 | 9 | from .common import config_content 10 | from .common import parametrize_list 11 | 12 | 13 | @pytest.fixture 14 | def app(): 15 | app = Bottle() 16 | 17 | @app.route('/hello/world') 18 | def hello(): 19 | return 'Hello World!!!' 20 | return app 21 | 22 | 23 | @pytest.mark.parametrize('mode, kwargs', parametrize_list) 24 | def test_bottle(app, mode, kwargs): 25 | if kwargs.get('config_rel_url'): 26 | @app.route(kwargs['config_rel_url']) 27 | def swagger_config_handler(): 28 | return config_content 29 | 30 | if mode == 'auto': 31 | api_doc(app, **kwargs) 32 | else: 33 | bottle_api_doc(app, **kwargs) 34 | 35 | url_prefix = kwargs['url_prefix'] 36 | if url_prefix.endswith('/'): 37 | url_prefix = url_prefix[:-1] 38 | 39 | client = TestApp(app) 40 | 41 | resp = client.get('/hello/world') 42 | assert resp.status_code == 200, resp.data 43 | 44 | resp = client.get(url_prefix) 45 | assert resp.status_code == 200, resp.data 46 | 47 | resp = client.get(f'{url_prefix}/static/LICENSE') 48 | assert resp.status_code == 200, resp.data 49 | 50 | if kwargs.get('editor'): 51 | resp = client.get(f'{url_prefix}/editor') 52 | assert resp.status_code == 200, resp.data 53 | else: 54 | try: 55 | resp = client.get(f'{url_prefix}/editor') 56 | except AppError as ex: 57 | assert '404 Not Found' in str(ex), str(ex) 58 | 59 | if kwargs.get('config_rel_url'): 60 | resp = client.get(kwargs['config_rel_url']) 61 | assert resp.status_code == 200, resp.data 62 | else: 63 | resp = client.get(f'{url_prefix}/swagger.json') 64 | assert resp.status_code == 200, resp.data 65 | -------------------------------------------------------------------------------- /test/chalice_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from chalice import Chalice 3 | from chalice.config import Config 4 | from chalice.local import ForbiddenError 5 | from chalice.local import LocalGateway 6 | 7 | from swagger_ui import api_doc 8 | from swagger_ui import chalice_api_doc 9 | 10 | from .common import config_content 11 | from .common import parametrize_list 12 | 13 | 14 | @pytest.fixture 15 | def app(): 16 | app = Chalice(__name__) 17 | 18 | @app.route('/hello/world') 19 | def hello(): 20 | return 'Hello World!!!' 21 | return app 22 | 23 | 24 | @pytest.mark.parametrize('mode, kwargs', parametrize_list) 25 | def test_chalice(app, mode, kwargs): 26 | if kwargs.get('config_rel_url'): 27 | @app.route(kwargs['config_rel_url']) 28 | def swagger_config_handler(): 29 | return config_content 30 | 31 | if mode == 'auto': 32 | api_doc(app, **kwargs) 33 | else: 34 | chalice_api_doc(app, **kwargs) 35 | 36 | url_prefix = kwargs['url_prefix'] 37 | if url_prefix.endswith('/'): 38 | url_prefix = url_prefix[:-1] 39 | 40 | client = LocalGateway(app, config=Config()) 41 | 42 | headers = {'Host': 'localhost'} 43 | 44 | resp = client.handle_request( 45 | method='GET', path='/hello/world', headers=headers, body=None) 46 | assert resp['statusCode'] == 200, resp['body'] 47 | 48 | resp = client.handle_request( 49 | method='GET', path=url_prefix, headers=headers, body=None) 50 | assert resp['statusCode'] == 200, resp['body'] 51 | 52 | resp = client.handle_request( 53 | method='GET', path=f'{url_prefix}/static/LICENSE', 54 | headers=headers, body=None) 55 | assert resp['statusCode'] == 200, resp['body'] 56 | 57 | if kwargs.get('editor'): 58 | resp = client.handle_request( 59 | method='GET', path=f'{url_prefix}/editor', 60 | headers=headers, body=None) 61 | assert resp['statusCode'] == 200, resp['body'] 62 | else: 63 | try: 64 | resp = client.handle_request( 65 | method='GET', path=f'{url_prefix}/editor', 66 | headers=headers, body=None) 67 | except ForbiddenError as ex: 68 | assert "Missing Authentication Token" in str(ex), str(ex) 69 | 70 | if kwargs.get('config_rel_url'): 71 | resp = client.handle_request( 72 | method='GET', path=kwargs['config_rel_url'], 73 | headers=headers, body=None) 74 | assert resp['statusCode'] == 200, resp['body'] 75 | else: 76 | resp = client.handle_request( 77 | method='GET', path=f'{url_prefix}/swagger.json', 78 | headers=headers, body=None) 79 | assert resp['statusCode'] == 200, resp['body'] 80 | -------------------------------------------------------------------------------- /test/common.py: -------------------------------------------------------------------------------- 1 | from itertools import product 2 | from pathlib import Path 3 | 4 | cur_dir = Path(__file__).resolve().parent 5 | 6 | config_path = str(cur_dir.joinpath('conf/test3.yaml')) 7 | config_content = Path(config_path).read_text() 8 | 9 | mode_list = ['auto', None] 10 | 11 | kwargs_list = [ 12 | { 13 | 'url_prefix': '/api/doc', 14 | 'config_path': config_path, 15 | }, 16 | { 17 | 'url_prefix': '/api/doc', 18 | 'config_path': config_path, 19 | 'editor': True, 20 | }, 21 | { 22 | 'url_prefix': '/', 23 | 'config_path': config_path, 24 | }, 25 | { 26 | 'url_prefix': '', 27 | 'config_path': config_path, 28 | }, 29 | { 30 | 'url_prefix': '/', 31 | 'config_path': config_path, 32 | 'config_rel_url': '/swagger.json', 33 | }, 34 | ] 35 | 36 | parametrize_list = list(product(mode_list, kwargs_list)) 37 | -------------------------------------------------------------------------------- /test/conf/test3.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: python-swagger-ui test api 4 | description: python-swagger-ui test api 5 | version: 1.0.0 6 | servers: 7 | - url: http://127.0.0.1:8989/ 8 | tags: 9 | - name: default 10 | description: default tag 11 | paths: 12 | /hello/world: 13 | get: 14 | tags: 15 | - default 16 | summary: output hello world. 17 | responses: 18 | 200: 19 | description: OK 20 | content: 21 | application/text: 22 | schema: 23 | type: object 24 | example: Hello World!!! 25 | components: {} 26 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PWZER/swagger-ui-py/21c62701c12d7a5b47f340c1631500cdaa5ef00f/test/conftest.py -------------------------------------------------------------------------------- /test/falcon_test.py: -------------------------------------------------------------------------------- 1 | import falcon 2 | import pytest 3 | from falcon import testing 4 | from packaging.version import Version 5 | 6 | from swagger_ui import api_doc 7 | from swagger_ui import falcon_api_doc 8 | 9 | from .common import config_content 10 | from .common import parametrize_list 11 | 12 | 13 | @pytest.fixture 14 | def app(): 15 | class HelloWorldHandler(object): 16 | def on_get(self, req, resp): 17 | resp.body = 'Hello World!!!' 18 | 19 | if Version(falcon.__version__) < Version('3.0.0'): 20 | app = falcon.API() 21 | else: 22 | app = falcon.App() 23 | 24 | app.add_route('/hello/world', HelloWorldHandler()) 25 | return app 26 | 27 | 28 | @pytest.mark.parametrize('mode, kwargs', parametrize_list) 29 | def test_falcon(app, mode, kwargs): 30 | if kwargs['url_prefix'] in ('', '/'): 31 | return 32 | 33 | if kwargs.get('config_rel_url'): 34 | class SwaggerConfigHandler(object): 35 | def on_get(self, req, resp): 36 | resp.body = config_content 37 | app.add_route(kwargs['config_rel_url'], SwaggerConfigHandler()) 38 | 39 | if mode == 'auto': 40 | api_doc(app, **kwargs) 41 | else: 42 | falcon_api_doc(app, **kwargs) 43 | 44 | url_prefix = kwargs['url_prefix'] 45 | if url_prefix.endswith('/'): 46 | url_prefix = url_prefix[:-1] 47 | 48 | client = testing.TestClient(app) 49 | 50 | resp = client.get('/hello/world') 51 | assert resp.status_code == 200, resp.text 52 | 53 | resp = client.get(url_prefix) 54 | assert resp.status_code == 200, resp.text 55 | 56 | resp = client.get(f'{url_prefix}/static/LICENSE') 57 | assert resp.status_code == 200, resp.text 58 | 59 | resp = client.get(f'{url_prefix}/editor') 60 | if kwargs.get('editor'): 61 | assert resp.status_code == 200, resp.text 62 | else: 63 | assert resp.status_code == 404, resp.text 64 | 65 | if kwargs.get('config_rel_url'): 66 | resp = client.get(kwargs['config_rel_url']) 67 | assert resp.status_code == 200, resp.text 68 | else: 69 | resp = client.get(f'{url_prefix}/swagger.json') 70 | assert resp.status_code == 200, resp.text 71 | -------------------------------------------------------------------------------- /test/flask_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask import Flask 3 | 4 | from swagger_ui import api_doc 5 | from swagger_ui import flask_api_doc 6 | 7 | from .common import config_content 8 | from .common import parametrize_list 9 | 10 | 11 | @pytest.fixture 12 | def app(): 13 | app = Flask(__name__) 14 | 15 | @app.route(r'/hello/world') 16 | def hello(): 17 | return 'Hello World!!!' 18 | return app 19 | 20 | 21 | @pytest.mark.parametrize('mode, kwargs', parametrize_list) 22 | def test_flask(app, mode, kwargs): 23 | if kwargs['url_prefix'] in ('/', ''): 24 | return 25 | 26 | if kwargs.get('config_rel_url'): 27 | @app.route(kwargs['config_rel_url']) 28 | def swagger_config(): 29 | return config_content 30 | 31 | if mode == 'auto': 32 | api_doc(app, **kwargs) 33 | else: 34 | flask_api_doc(app, **kwargs) 35 | 36 | url_prefix = kwargs['url_prefix'] 37 | if url_prefix.endswith('/'): 38 | url_prefix = url_prefix[:-1] 39 | 40 | client = app.test_client() 41 | 42 | resp = client.get('/hello/world') 43 | assert resp.status_code == 200, resp.data 44 | 45 | resp = client.get(url_prefix) 46 | assert resp.status_code == 200, resp.data 47 | 48 | resp = client.get(f'{url_prefix}/static/LICENSE') 49 | assert resp.status_code == 200, resp.data 50 | 51 | resp = client.get(f'{url_prefix}/editor') 52 | if kwargs.get('editor'): 53 | assert resp.status_code == 200, resp.data 54 | else: 55 | assert resp.status_code == 404, resp.data 56 | 57 | if kwargs.get('config_rel_url'): 58 | resp = client.get(kwargs['config_rel_url']) 59 | assert resp.status_code == 200, resp.data 60 | else: 61 | resp = client.get(f'{url_prefix}/swagger.json') 62 | assert resp.status_code == 200, resp.data 63 | -------------------------------------------------------------------------------- /test/quart_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from quart import Quart 3 | 4 | from swagger_ui import api_doc 5 | from swagger_ui import quart_api_doc 6 | 7 | from .common import config_content 8 | from .common import parametrize_list 9 | 10 | 11 | @pytest.fixture 12 | def app(): 13 | app = Quart(__name__) 14 | 15 | @app.route(r'/hello/world', methods=['GET']) 16 | async def hello(): 17 | return 'Hello World!!!' 18 | return app 19 | 20 | 21 | @pytest.mark.asyncio 22 | @pytest.mark.parametrize('mode, kwargs', parametrize_list) 23 | async def test_quart(app, mode, kwargs): 24 | if kwargs['url_prefix'] in ('/', ''): 25 | return 26 | 27 | if kwargs.get('config_rel_url'): 28 | @app.route(kwargs['config_rel_url'], methods=['GET']) 29 | def swagger_config(): 30 | return config_content 31 | 32 | if mode == 'auto': 33 | api_doc(app, **kwargs) 34 | else: 35 | quart_api_doc(app, **kwargs) 36 | 37 | url_prefix = kwargs['url_prefix'] 38 | if url_prefix.endswith('/'): 39 | url_prefix = url_prefix[:-1] 40 | 41 | client = app.test_client() 42 | 43 | resp = await client.get('/hello/world', follow_redirects=True) 44 | assert resp.status_code == 200, await resp.get_data() 45 | 46 | resp = await client.get(url_prefix, follow_redirects=True) 47 | assert resp.status_code == 200, await resp.get_data() 48 | 49 | resp = await client.get(f'{url_prefix}/static/LICENSE', follow_redirects=True) 50 | assert resp.status_code == 200, await resp.get_data() 51 | 52 | resp = await client.get(f'{url_prefix}/editor', follow_redirects=True) 53 | if kwargs.get('editor'): 54 | assert resp.status_code == 200, await resp.get_data() 55 | else: 56 | assert resp.status_code == 404, await resp.get_data() 57 | 58 | if kwargs.get('config_rel_url'): 59 | resp = await client.get(kwargs['config_rel_url'], follow_redirects=True) 60 | assert resp.status_code == 200, await resp.get_data() 61 | else: 62 | resp = await client.get(f'{url_prefix}/swagger.json', follow_redirects=True) 63 | assert resp.status_code == 200, await resp.get_data() 64 | -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | bottle 3 | chalice 4 | falcon 5 | flask 6 | pytest 7 | pytest-aiohttp 8 | quart 9 | requests 10 | sanic 11 | sanic-testing 12 | starlette 13 | tornado 14 | uvicorn 15 | webtest 16 | -------------------------------------------------------------------------------- /test/sanic_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sanic import Sanic 3 | from sanic import response 4 | from sanic_testing.testing import SanicTestClient 5 | 6 | from swagger_ui import api_doc 7 | from swagger_ui import sanic_api_doc 8 | 9 | from .common import config_content 10 | from .common import parametrize_list 11 | 12 | 13 | @pytest.fixture 14 | def app(): 15 | app = Sanic('test_sanic') 16 | 17 | @app.get(r'/hello/world') 18 | async def index_handler(request): 19 | return response.text('Hello World!!!') 20 | return app 21 | 22 | 23 | @pytest.mark.parametrize('mode, kwargs', parametrize_list) 24 | def test_sanic(app, mode, kwargs): 25 | if kwargs.get('config_rel_url'): 26 | @app.get(kwargs['config_rel_url']) 27 | async def swagger_config_handler(request): 28 | return response.json(config_content) 29 | 30 | if mode == 'auto': 31 | api_doc(app, **kwargs) 32 | else: 33 | sanic_api_doc(app, **kwargs) 34 | 35 | url_prefix = kwargs['url_prefix'] 36 | if url_prefix.endswith('/'): 37 | url_prefix = url_prefix[:-1] 38 | 39 | client = SanicTestClient(app) 40 | _, resp = client.get('/hello/world') 41 | assert resp.status == 200, resp.text 42 | 43 | _, resp = client.get(url_prefix) 44 | assert resp.status == 200, resp.text 45 | 46 | _, resp = client.get(f'{url_prefix}/static/LICENSE') 47 | assert resp.status == 200, resp.text 48 | 49 | if kwargs.get('editor'): 50 | _, resp = client.get(f'{url_prefix}/editor') 51 | assert resp.status == 200, resp.text 52 | else: 53 | _, resp = client.get(f'{url_prefix}/editor') 54 | assert resp.status == 404, resp.text 55 | 56 | if kwargs.get('config_rel_url'): 57 | _, resp = client.get(kwargs['config_rel_url']) 58 | assert resp.status == 200, resp.text 59 | else: 60 | _, resp = client.get(f'{url_prefix}/swagger.json') 61 | assert resp.status == 200, resp.text 62 | -------------------------------------------------------------------------------- /test/starlette_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from starlette.applications import Starlette 3 | from starlette.responses import PlainTextResponse 4 | from starlette.testclient import TestClient 5 | 6 | from swagger_ui import api_doc 7 | from swagger_ui import starlette_api_doc 8 | 9 | from .common import config_content 10 | from .common import parametrize_list 11 | 12 | 13 | @pytest.fixture 14 | def app(): 15 | app = Starlette() 16 | 17 | @app.route('/hello/world') 18 | def hello_world(request): 19 | return PlainTextResponse('Hello World!!!') 20 | return app 21 | 22 | 23 | @pytest.mark.parametrize('mode, kwargs', parametrize_list) 24 | def test_starlette(app, mode, kwargs): 25 | if kwargs['url_prefix'] in ('/', ''): 26 | return 27 | 28 | if kwargs.get('config_rel_url'): 29 | @app.route(kwargs['config_rel_url']) 30 | def swagger_config(request): 31 | return PlainTextResponse(config_content) 32 | 33 | if mode == 'auto': 34 | api_doc(app, **kwargs) 35 | else: 36 | starlette_api_doc(app, **kwargs) 37 | 38 | url_prefix = kwargs['url_prefix'] 39 | if url_prefix.endswith('/'): 40 | url_prefix = url_prefix[:-1] 41 | 42 | client = TestClient(app) 43 | 44 | resp = client.get('/hello/world') 45 | assert resp.status_code == 200, resp.text 46 | 47 | resp = client.get(url_prefix) 48 | assert resp.status_code == 200, resp.text 49 | 50 | resp = client.get(f'{url_prefix}/static/LICENSE') 51 | assert resp.status_code == 200, resp.text 52 | 53 | if kwargs.get('editor'): 54 | resp = client.get(f'{url_prefix}/editor') 55 | assert resp.status_code == 200, resp.text 56 | else: 57 | resp = client.get(f'{url_prefix}/editor') 58 | assert resp.status_code == 404, resp.text 59 | 60 | if kwargs.get('config_rel_url'): 61 | resp = client.get(kwargs['config_rel_url']) 62 | assert resp.status_code == 200, resp.text 63 | else: 64 | resp = client.get(f'{url_prefix}/swagger.json') 65 | assert resp.status_code == 200, resp.text 66 | -------------------------------------------------------------------------------- /test/tornado_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tornado.httpclient 3 | import tornado.httpserver 4 | import tornado.ioloop 5 | import tornado.web 6 | 7 | from swagger_ui import api_doc 8 | from swagger_ui import tornado_api_doc 9 | 10 | from .common import config_content 11 | from .common import parametrize_list 12 | 13 | 14 | @pytest.fixture 15 | def app(): 16 | class HelloWorldHandler(tornado.web.RequestHandler): 17 | def get(self, *args, **kwargs): 18 | return self.write('Hello World!!!') 19 | 20 | app = tornado.web.Application([ 21 | (r'/hello/world', HelloWorldHandler), 22 | ]) 23 | return app 24 | 25 | 26 | @pytest.mark.asyncio 27 | @pytest.mark.parametrize('mode, kwargs', parametrize_list) 28 | async def test_tornado(app, mode, kwargs): 29 | if kwargs.get('config_rel_url'): 30 | class SwaggerConfigHandler(tornado.web.RequestHandler): 31 | def get(self, *args, **kwargs): 32 | return self.write(config_content) 33 | app.add_handlers('.*', [(kwargs['config_rel_url'], SwaggerConfigHandler)]) 34 | 35 | if mode == 'auto': 36 | api_doc(app, **kwargs) 37 | else: 38 | tornado_api_doc(app, **kwargs) 39 | 40 | server = tornado.httpserver.HTTPServer(app) 41 | server.listen(0) 42 | 43 | host, port = list(server._sockets.values())[0].getsockname() 44 | server_addr = f'http://{host}:{port}' 45 | 46 | url_prefix = kwargs['url_prefix'] 47 | if url_prefix.endswith('/'): 48 | url_prefix = url_prefix[:-1] 49 | url_prefix = f'{server_addr}{url_prefix}' 50 | 51 | http_client = tornado.httpclient.AsyncHTTPClient() 52 | 53 | resp = await http_client.fetch(f'{server_addr}/hello/world') 54 | assert resp.code == 200, resp.body 55 | 56 | resp = await http_client.fetch(url_prefix) 57 | assert resp.code == 200, resp.body 58 | 59 | resp = await http_client.fetch(f'{url_prefix}/static/LICENSE') 60 | assert resp.code == 200, resp.body 61 | 62 | if kwargs.get('editor'): 63 | resp = await http_client.fetch(f'{url_prefix}/editor') 64 | assert resp.code == 200, resp.body 65 | else: 66 | try: 67 | resp = await http_client.fetch(f'{url_prefix}/editor') 68 | except tornado.httpclient.HTTPClientError as e: 69 | assert e.code == 404, e.response.body 70 | 71 | if kwargs.get('config_rel_url'): 72 | resp = await http_client.fetch(server_addr + kwargs['config_rel_url']) 73 | assert resp.code == 200, resp.body 74 | else: 75 | resp = await http_client.fetch(f'{url_prefix}/swagger.json') 76 | assert resp.code == 200, resp.body 77 | -------------------------------------------------------------------------------- /tools/update.py: -------------------------------------------------------------------------------- 1 | #! env python 2 | import argparse 3 | import json 4 | import re 5 | import shutil 6 | import tarfile 7 | from pathlib import Path 8 | 9 | import requests 10 | 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument('--ui', action='store_true', 13 | help='Enabled to update swagger ui.') 14 | parser.add_argument('--editor', action='store_true', 15 | help='Enabled to update swagger editor.') 16 | parser.add_argument('--ui-version', type=str, default=None, 17 | help='Specify the version of swagger ui, Default latest version.') 18 | parser.add_argument('--editor-version', type=str, default=None, 19 | help='Specify the version of swagger editor, Default latest version.') 20 | parser.add_argument('--no-clean', action='store_true', 21 | help='disable auto clean the temporary files.') 22 | cmd_args = parser.parse_args() 23 | 24 | 25 | SWAGGER_UI_REPO = 'swagger-api/swagger-ui' 26 | SWAGGER_EDITOR_REPO = 'swagger-api/swagger-editor' 27 | 28 | 29 | DOC_HTML_JAVASCRIPT = '''window.onload = function() { 30 | const ui = SwaggerUIBundle({ 31 | {%- for key, value in parameters.items() %} 32 | {{ key|safe }}: {{ value|safe }}, 33 | {%- endfor %} 34 | }); 35 | 36 | {% if oauth2_config %} 37 | ui.initOAuth({ 38 | {%- for key, value in oauth2_config.items() %} 39 | {{ key|safe }}: {{ value|safe }}, 40 | {%- endfor %} 41 | }); 42 | {% endif %} 43 | 44 | window.ui = ui; 45 | };''' 46 | 47 | 48 | def detect_latest_release(repo): 49 | print('detect latest release') 50 | resp = requests.get( 51 | 'https://api.github.com/repos/{}/releases/latest'.format(repo), 52 | timeout=120) 53 | latest = json.loads(resp.text) 54 | tag = latest['tag_name'] 55 | print('{} latest version is {}'.format(repo, tag)) 56 | return tag 57 | 58 | 59 | def dist_copy(repo, dist_dir): 60 | # index.html for swagger editor 61 | if repo == SWAGGER_UI_REPO: 62 | index_html_path = dist_dir.joinpath('index.html') 63 | dst_path = templates_dir.joinpath('doc.html') 64 | 65 | # license file 66 | license_path = dist_dir.parent.joinpath('LICENSE') 67 | dst_license_path = static_dir.joinpath('LICENSE') 68 | if license_path.exists(): 69 | shutil.copyfile(license_path, dst_license_path) 70 | print('copy {} => {}'.format(license_path, dst_license_path)) 71 | elif repo == SWAGGER_EDITOR_REPO: 72 | index_html_path = dist_dir.parent.joinpath('index.html') 73 | dst_path = templates_dir.joinpath('editor.html') 74 | 75 | shutil.copyfile(index_html_path, dst_path) 76 | print('copy {} => {}'.format(index_html_path, dst_path)) 77 | 78 | for path in dist_dir.glob('**/*'): 79 | if path.name == 'index.html': 80 | continue 81 | dst_path = static_dir.joinpath(path.relative_to(dist_dir)) 82 | shutil.copyfile(path, dst_path) 83 | print('copy {} => {}'.format(path, dst_path)) 84 | 85 | 86 | def download_archive(repo, version): 87 | if version is None: 88 | version = detect_latest_release(repo) 89 | 90 | file_name = '{}.tar.gz'.format(version) 91 | save_path = cur_dir.joinpath(file_name) 92 | 93 | if not (cmd_args.no_clean and save_path.exists()): 94 | archive_url = 'https://github.com/{}/archive/{}'.format(repo, file_name) 95 | print('archive downloading: {}'.format(archive_url)) 96 | with requests.get(archive_url, stream=True) as resp: 97 | assert resp.status_code == 200, resp.status_code 98 | with save_path.open('wb') as out: 99 | shutil.copyfileobj(resp.raw, out) 100 | print('archive download completed: {}'.format(save_path)) 101 | 102 | print('open tarfile: {}'.format(file_name)) 103 | tar_file = tarfile.open(save_path) 104 | tar_file.extractall(path=cur_dir) 105 | swagger_ui_dir = cur_dir.joinpath(tar_file.getnames()[0]) 106 | 107 | dist_copy(repo, swagger_ui_dir.joinpath('dist')) 108 | 109 | if not cmd_args.no_clean: 110 | print('remove {}'.format(swagger_ui_dir)) 111 | shutil.rmtree(swagger_ui_dir) 112 | 113 | print('remove {}'.format(save_path)) 114 | save_path.unlink() 115 | 116 | print('Successed') 117 | return version 118 | 119 | 120 | def replace_html_content(): 121 | for html_path in templates_dir.glob('**/*.html'): 122 | print(html_path) 123 | with html_path.open('r') as html_file: 124 | html = html_file.read() 125 | 126 | html = re.sub(r'.*', ' {{ title }} ', html) 127 | html = re.sub(r'src="(\./dist/|\./|(?!{{))', 'src="{{ url_prefix }}/static/', html) 128 | html = re.sub(r'href="(\./dist/|\./|(?!{{))', 'href="{{ url_prefix }}/static/', html) 129 | html = re.sub(r'https://petstore.swagger.io/v[1-9]/swagger.json', '{{ config_url }}', html) 130 | 131 | if str(html_path).endswith('doc.html'): 132 | html = re.sub(r'window.onload = function\(\) {.*};$', DOC_HTML_JAVASCRIPT, html, 133 | flags=re.MULTILINE | re.DOTALL) 134 | html = re.sub(r''.format(DOC_HTML_JAVASCRIPT), 136 | html) 137 | 138 | with html_path.open('w') as html_file: 139 | html_file.write(html) 140 | 141 | 142 | def replace_readme(ui_version, editor_version): 143 | readme_path = cur_dir.parent.joinpath('README.md') 144 | readme = readme_path.read_text(encoding='utf-8') 145 | if ui_version: 146 | readme = re.sub(r'Swagger UI version is `.*`', 147 | 'Swagger UI version is `{}`'.format(ui_version), readme) 148 | print('update swagger ui version: {}'.format(ui_version)) 149 | if editor_version: 150 | readme = re.sub(r'Swagger Editor version is `.*`', 151 | 'Swagger Editor version is `{}`'.format(editor_version), readme) 152 | print('update swagger editor version: {}'.format(editor_version)) 153 | readme_path.write_text(readme) 154 | 155 | 156 | if __name__ == '__main__': 157 | cur_dir = Path(__file__).resolve().parent 158 | 159 | static_dir = cur_dir.parent.joinpath('swagger_ui/static') 160 | if static_dir.exists(): 161 | shutil.rmtree(static_dir) 162 | static_dir.mkdir(parents=True, exist_ok=True) 163 | 164 | templates_dir = cur_dir.parent.joinpath('swagger_ui/templates') 165 | if templates_dir.exists(): 166 | shutil.rmtree(templates_dir) 167 | templates_dir.mkdir(parents=True, exist_ok=True) 168 | 169 | ui_version = editor_version = None 170 | 171 | if cmd_args.ui: 172 | ui_version = download_archive(SWAGGER_UI_REPO, cmd_args.ui_version) 173 | replace_html_content() 174 | 175 | if cmd_args.editor: 176 | editor_version = download_archive(SWAGGER_EDITOR_REPO, cmd_args.editor_version) 177 | replace_html_content() 178 | replace_readme(ui_version, editor_version) 179 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py38,py39 3 | 4 | [testenv] 5 | extras = flake8, pytest 6 | 7 | [testenv:flake8] 8 | commands = 9 | pip install flake8 10 | flake8 . 11 | 12 | [testenv:pytest] 13 | commands = 14 | pip install -r test/requirements.txt 15 | pytest -s test/ 16 | --------------------------------------------------------------------------------