├── tests ├── __init__.py └── test_utils.py ├── example ├── __init__.py ├── readme.txt ├── manage.py ├── apidoc.json └── views.py ├── requirements.txt ├── flask_apidoc ├── __init__.py ├── utils.py ├── commands.py └── apidoc.py ├── CHANGELOG.rst ├── .gitignore ├── setup.py ├── LICENSE └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask>=1.0.1 2 | -------------------------------------------------------------------------------- /flask_apidoc/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_apidoc.apidoc import ApiDoc 2 | -------------------------------------------------------------------------------- /example/readme.txt: -------------------------------------------------------------------------------- 1 | You need to generate the ApiDoc files before running it. 2 | 3 | python manage.py apidoc 4 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | from .views import app 2 | from flask_apidoc.commands import GenerateApiDoc 3 | 4 | app.cli.add_command(GenerateApiDoc(), "apidoc") 5 | 6 | if __name__ == "__main__": 7 | pass 8 | -------------------------------------------------------------------------------- /example/apidoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Flask REST API", 3 | "version": "1.0.0", 4 | "description": "A Flask REST API example", 5 | "title": "A Flask REST API example", 6 | "url" : "http://localhost:5000" 7 | } 8 | -------------------------------------------------------------------------------- /flask_apidoc/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helpers. 3 | """ 4 | 5 | import functools 6 | 7 | 8 | def cached(f): 9 | """ 10 | Cache decorator for functions taking one or more arguments. 11 | :param f: The function to be cached. 12 | :return: The cached value. 13 | """ 14 | 15 | cache = f.cache = {} 16 | 17 | @functools.wraps(f) 18 | def decorator(*args, **kwargs): 19 | key = str(args) + str(kwargs) 20 | if key not in cache: 21 | cache[key] = f(*args, **kwargs) 22 | return cache[key] 23 | return decorator 24 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from flask_apidoc.utils import cached 3 | 4 | 5 | class TestUtils(TestCase): 6 | def test_cached_function(self): 7 | @cached 8 | def f(a, b): 9 | return object() 10 | 11 | self.assertEqual(f(1, 2), f(1, 2)) 12 | self.assertNotEqual(f(1, 2), f(1, 3)) 13 | 14 | def test_cached_method(self): 15 | class C(object): 16 | @cached 17 | def f(self, a, b): 18 | return object() 19 | 20 | c = C() 21 | self.assertEqual(c.f(1, 2), c.f(1, 2)) 22 | self.assertNotEqual(c.f(1, 2), c.f(1, 3)) 23 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | 1.3.0 5 | ++++++++++++++++++ 6 | - Add requirement for flask>=1.0.1 7 | - Replace flask_script with Flask's built-in CLI tool. Thanks :user:`olivetree123`. 8 | 9 | 1.2.0 10 | ++++++++++++++++++ 11 | - Send to STDOUT the output from apidoc #12. 12 | 13 | 1.1.2 14 | ++++++++++++++++++ 15 | - Fix: apidoc.json without a url was raising an error. 16 | 17 | 1.1.1 18 | ++++++++++++++++++ 19 | - Fix: Open files using utf-8 encoding, this prevent errors like "UnicodeDecodeError: 'ascii' codec can't decode." 20 | 21 | 1.1.0 22 | ++++++++++++++++++ 23 | - Add support for absolute url in @api 24 | 25 | 1.0.0 26 | ++++++++++++++++++ 27 | - Add support to Python 2.7 (:issue:`1`). Thanks :user:`hellking4u`. 28 | - Add ``dynamic_url`` parameter to enabled/disable dynamic urls in ApiDoc files. 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | 58 | # PyCharm 59 | .idea/ 60 | 61 | # vscode 62 | .vscode 63 | 64 | # OSX 65 | .DS_Store 66 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='flask-apidoc', 5 | version='1.3.0', 6 | packages=['flask_apidoc'], 7 | url='https://github.com/viniciuschiele/flask-apidoc', 8 | license='MIT', 9 | author='Vinicius Chiele', 10 | author_email='vinicius.chiele@gmail.com', 11 | description='Adds ApiDoc support to Flask', 12 | keywords=['flask', 'apidoc', 'doc', 'documentation', 'rest', 'restful'], 13 | install_requires=['flask>=1.0.1'], 14 | classifiers=[ 15 | 'Development Status :: 5 - Production/Stable', 16 | 'Intended Audience :: Developers', 17 | 'Natural Language :: English', 18 | 'Operating System :: OS Independent', 19 | 'Programming Language :: Python', 20 | 'Programming Language :: Python :: 2.7', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.2', 23 | 'Programming Language :: Python :: 3.3', 24 | 'Programming Language :: Python :: 3.4', 25 | 'Programming Language :: Python :: Implementation :: CPython' 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /flask_apidoc/commands.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from click import Command 3 | 4 | 5 | class GenerateApiDoc(Command): 6 | """ 7 | GenerateApiDoc is a command to generate the apidoc files. 8 | """ 9 | def __init__(self, 10 | input_path="./", 11 | output_path="static/docs", 12 | template_path=None): 13 | super(GenerateApiDoc, self).__init__(name="GenerateApiDoc") 14 | self.input_path = input_path 15 | self.output_path = output_path 16 | self.template_path = template_path 17 | self.callback = self.run 18 | 19 | def run(self): 20 | cmd = ['apidoc'] 21 | 22 | if self.input_path: 23 | cmd.append('--input') 24 | cmd.append(self.input_path) 25 | 26 | if self.output_path: 27 | cmd.append('--output') 28 | cmd.append(self.output_path) 29 | 30 | if self.template_path: 31 | cmd.append('--template') 32 | cmd.append(self.template_path) 33 | 34 | p = subprocess.Popen(cmd) 35 | 36 | p.communicate() 37 | 38 | return p.returncode 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Vinicius Chiele 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | Flask ApiDoc 3 | ================================= 4 | Flask ApiDoc is a Flask extension which adds support for the ApiDoc_. 5 | 6 | |Version| 7 | 8 | Features 9 | =============== 10 | - click command to generate the apidoc's files, by default they are generated into static/docs 11 | - Host apidoc's files, by default they are hosted in http://localhost:5000/docs 12 | - Replace the base URL for the endpoints by the current Flask URL. 13 | - Allow absolute urls in @api 14 | 15 | Documentation 16 | =============== 17 | Soon... take a look at the example_ to see how it works. 18 | 19 | Installation 20 | =============== 21 | You can install flask-apidoc via Python Package Index (PyPI_),:: 22 | 23 | $ pip install flask-apidoc 24 | 25 | Feedback 26 | =============== 27 | Please use the Issues_ for feature requests and troubleshooting usage. 28 | 29 | .. |Version| image:: https://badge.fury.io/py/flask-apidoc.svg 30 | :target: http://badge.fury.io/py/flask-apidoc 31 | 32 | .. _ApiDoc: http://www.apidocjs.com 33 | 34 | .. _example: https://github.com/viniciuschiele/flask-apidoc/tree/master/example 35 | 36 | .. _PyPi: https://pypi.python.org/pypi/flask-apidoc 37 | 38 | .. _Issues: https://github.com/viniciuschiele/flask-apidoc/issues 39 | -------------------------------------------------------------------------------- /example/views.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_apidoc import ApiDoc 3 | 4 | app = Flask(__name__) 5 | doc = ApiDoc(app=app) 6 | 7 | 8 | @app.route('/users', methods=['POST']) 9 | def add_user(): 10 | """ 11 | @api {post} /user Adds a new User 12 | @apiVersion 1.0.0 13 | @apiName add_user 14 | @apiGroup User 15 | 16 | @apiParam {String} username The user's username. 17 | @apiParam {String} first_name The first name of the User. 18 | @apiParam {String} last_name the last name of the User. 19 | @apiParam {Object} profile The profile data 20 | @apiParam {Number} profile.age The user's age. 21 | @apiParam {String} profile.image The user's avatar-image. 22 | 23 | @apiSuccess {Number} id The new user id. 24 | """ 25 | return 26 | 27 | 28 | @app.route('/users/') 29 | def get_user(id): 30 | """ 31 | @api {get} /users/:id Gets an user 32 | @apiVersion 1.0.0 33 | @apiName get_user 34 | @apiGroup User 35 | 36 | @apiDescription Gets an user for the given id. 37 | 38 | @apiExample Example usage: 39 | curl -i http://localhost/users/4711 40 | 41 | @apiParam {Number} id The user's i 42 | 43 | @apiSuccess {Number} id The user's id. 44 | @apiSuccess {String} username The user's username. 45 | @apiSuccess {String} first_name The first name of the User. 46 | @apiSuccess {String} last_name The last name of the User. 47 | @apiSuccess {Object} profile The profile data 48 | @apiSuccess {Number} profile.age The user's age. 49 | @apiSuccess {String} profile.image The user's avatar-image. 50 | 51 | @apiError UserNotFound The id of the User was not found. 52 | """ 53 | return 54 | 55 | 56 | @app.route('/users') 57 | def get_users(): 58 | """ 59 | @api {get} /users/:id Gets all the users 60 | @apiVersion 1.0.0 61 | @apiName get_users 62 | @apiGroup User 63 | 64 | @apiExample Example usage: 65 | curl -i http://localhost/users 66 | 67 | @apiSuccess {Object} users The user data 68 | @apiSuccess {Number} users.id The user's id. 69 | @apiSuccess {String} users.username The user's username. 70 | @apiSuccess {String} users.first_name The first name of the User. 71 | @apiSuccess {String} users.last_name The last name of the User. 72 | @apiSuccess {Object} users.profile The profile data 73 | @apiSuccess {Number} users.profile.age The user's age. 74 | @apiSuccess {String} users.profile.image The user's avatar-image. 75 | """ 76 | return 77 | 78 | 79 | @app.route('/users/', methods=['POST']) 80 | def update_user(): 81 | """ 82 | @api {post} /users/:id Updates a user 83 | @apiVersion 1.0.0 84 | @apiName update_user 85 | @apiGroup User 86 | 87 | @apiParam {Number} id The user's id 88 | @apiParam {String} first_name The first name of the User. 89 | @apiParam {String} last_name The last name of the User. 90 | """ 91 | return 92 | -------------------------------------------------------------------------------- /flask_apidoc/apidoc.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import json 3 | import mimetypes 4 | 5 | from flask import request 6 | from os.path import join, getmtime, getsize 7 | from werkzeug.datastructures import Headers 8 | from .utils import cached 9 | 10 | 11 | class ApiDoc(object): 12 | """ 13 | ApiDoc hosts the apidoc files in a specified url. 14 | """ 15 | 16 | def __init__(self, folder_path=None, url_path=None, dynamic_url=True, app=None): 17 | """ 18 | Initializes a new instance of ApiDoc. 19 | 20 | :param folder_path: The folder with apidoc files. Defaults to the 'docs' folder in the flask static folder. 21 | :param url_path: The url path for the apidoc files. Defaults to the '/docs'. 22 | :param dynamic_url: Set `True` to replace all the urls in ApiDoc files by the current url. 23 | :param app: The flask application. 24 | """ 25 | 26 | self.folder_path = folder_path 27 | self.url_path = url_path 28 | 29 | if self.folder_path is None: 30 | self.folder_path = 'docs' 31 | 32 | if self.url_path is None: 33 | self.url_path = '/docs' 34 | 35 | self.dynamic_url = dynamic_url 36 | self.allow_absolute_url = True 37 | 38 | self.app = None 39 | 40 | if app: 41 | self.init_app(app) 42 | 43 | def init_app(self, app): 44 | """ 45 | Adds the flask url routes for the apidoc files. 46 | :param app: the flask application. 47 | """ 48 | 49 | self.app = app 50 | 51 | self.dynamic_url = self.app.config.get('APIDOC_DYNAMIC_URL', self.dynamic_url) 52 | self.allow_absolute_url = self.app.config.get('APIDOC_ALLOW_ABSOLUTE_URL', self.allow_absolute_url) 53 | 54 | url = self.url_path 55 | 56 | if not self.url_path.endswith('/'): 57 | url += '/' 58 | 59 | app.add_url_rule(url, 'docs', self.__send_static_file, strict_slashes=True) 60 | app.add_url_rule(url + '', 'docs', self.__send_static_file, strict_slashes=True) 61 | 62 | def __send_static_file(self, path=None): 63 | """ 64 | Send apidoc files from the apidoc folder to the browser. 65 | :param path: the apidoc file. 66 | """ 67 | 68 | if not path: 69 | path = 'index.html' 70 | 71 | file_name = join(self.folder_path, path) 72 | 73 | # the api_project.js has the absolute url 74 | # hard coded so we replace them by the current url. 75 | if self.dynamic_url and path == 'api_project.js': 76 | return self.__send_api_file(file_name) 77 | 78 | if self.allow_absolute_url and path == 'main.js': 79 | return self.__send_main_file(file_name) 80 | 81 | # Any other apidoc file is treated as a normal static file 82 | return self.app.send_static_file(file_name) 83 | 84 | @cached 85 | def __send_api_file(self, file_name): 86 | """ 87 | Send apidoc files from the apidoc folder to the browser. 88 | This method replaces all absolute urls in the file by 89 | the current url. 90 | :param file_name: the apidoc file. 91 | """ 92 | 93 | file_name = join(self.app.static_folder, file_name) 94 | 95 | with codecs.open(file_name, 'r', 'utf-8') as file: 96 | data = file.read() 97 | 98 | # replaces the hard coded url by the current url. 99 | api_project = self.__read_api_project() 100 | 101 | old_url = api_project.get('url') 102 | 103 | # replaces the project's url only if it is present in the file. 104 | if old_url: 105 | new_url = request.url_root.strip('/') 106 | data = data.replace(old_url, new_url) 107 | 108 | # creates a flask response to send 109 | # the file to the browser 110 | 111 | headers = Headers() 112 | headers['Content-Length'] = getsize(file_name) 113 | 114 | response = self.app.response_class(data, 115 | mimetype=mimetypes.guess_type(file_name)[0], 116 | headers=headers, 117 | direct_passthrough=True) 118 | 119 | response.last_modified = int(getmtime(file_name)) 120 | 121 | return response 122 | 123 | @cached 124 | def __send_main_file(self, file_name): 125 | """ 126 | Send apidoc files from the apidoc folder to the browser. 127 | This method replaces all absolute urls in the file by 128 | the current url. 129 | :param file_name: the apidoc file. 130 | """ 131 | 132 | file_name = join(self.app.static_folder, file_name) 133 | 134 | with codecs.open(file_name, 'r', 'utf-8') as file: 135 | data = file.read() 136 | 137 | data = data.replace( 138 | 'fields.article.url = apiProject.url + fields.article.url;', 139 | '''if (fields.article.url.substr(0, 4).toLowerCase() !== \'http\') 140 | fields.article.url = apiProject.url + fields.article.url;''') 141 | 142 | headers = Headers() 143 | headers['Content-Length'] = getsize(file_name) 144 | 145 | response = self.app.response_class(data, 146 | mimetype=mimetypes.guess_type(file_name)[0], 147 | headers=headers, 148 | direct_passthrough=True) 149 | 150 | response.last_modified = int(getmtime(file_name)) 151 | 152 | return response 153 | 154 | @cached 155 | def __read_api_project(self): 156 | """ 157 | Reads the api_project.json file from apidoc folder as a json string. 158 | :return: a json string 159 | """ 160 | 161 | file_name = join(self.app.static_folder, self.folder_path, 'api_project.json') 162 | 163 | with open(file_name, 'rt') as file: 164 | data = file.read() 165 | 166 | return json.loads(data) 167 | --------------------------------------------------------------------------------