├── setup.cfg ├── requirements.txt ├── .travis.yml ├── sphinxcontrib ├── __init__.py └── swaggerdoc │ ├── __init__.py │ ├── swagger_doc.py │ └── swaggerv2_doc.py ├── .gitignore ├── LICENSE ├── setup.py └── README.rst /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | requests 3 | requests-file 4 | future 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | # command to install dependencies 6 | install: 7 | - "pip install -r requirements.txt" 8 | - python setup.py install 9 | # command to run tests 10 | script: nosetests 11 | -------------------------------------------------------------------------------- /sphinxcontrib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | sphinxcontrib 4 | ~~~~~~~~~~~~~ 5 | 6 | This package is a namespace package that contains all extensions 7 | distributed in the ``sphinx-contrib`` distribution. 8 | 9 | :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS. 10 | :license: BSD, see LICENSE for details. 11 | """ 12 | 13 | __import__('pkg_resources').declare_namespace(__name__) 14 | 15 | -------------------------------------------------------------------------------- /.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 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /sphinxcontrib/swaggerdoc/__init__.py: -------------------------------------------------------------------------------- 1 | from .swagger_doc import swaggerdoc, visit_swaggerdoc_node, depart_swaggerdoc_node, SwaggerDocDirective 2 | from .swaggerv2_doc import swaggerv2doc, visit_swaggerv2doc_node, depart_swaggerv2doc_node, SwaggerV2DocDirective 3 | 4 | def setup(app): 5 | app.add_node(swaggerdoc, 6 | html=(visit_swaggerdoc_node, depart_swaggerdoc_node), 7 | latex=(visit_swaggerdoc_node, depart_swaggerdoc_node), 8 | text=(visit_swaggerdoc_node, depart_swaggerdoc_node)) 9 | 10 | app.add_directive('swaggerdoc', SwaggerDocDirective) 11 | 12 | app.add_node(swaggerv2doc, 13 | html=(visit_swaggerv2doc_node, depart_swaggerv2doc_node), 14 | latex=(visit_swaggerv2doc_node, depart_swaggerv2doc_node), 15 | text=(visit_swaggerv2doc_node, depart_swaggerv2doc_node)) 16 | 17 | app.add_directive('swaggerv2doc', SwaggerV2DocDirective) 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Unai Aguilera 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. 22 | 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from setuptools import setup, find_packages 4 | 5 | # Utility function to read the README file. 6 | # Used for the long_description. It's nice, because now 1) we have a top level 7 | # README file and 2) it's easier to type in the README file than to put a raw 8 | # string in below ... 9 | def read(fname): 10 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 11 | 12 | setup( 13 | name='sphinxcontrib-swaggerdoc', 14 | version='0.1.5', 15 | author='Unai Aguilera', 16 | author_email='unai.aguilera@deusto.es', 17 | description='Sphinx extension for documenting Swagger 2.0 APIs', 18 | long_description=read('README.rst'), 19 | license='MIT', 20 | keywords='', 21 | url='https://github.com/unaguil/sphinx-swaggerdoc', 22 | classifiers=[ 23 | 'Development Status :: 4 - Beta', 24 | 'Environment :: Console', 25 | 'Environment :: Web Environment', 26 | 'Intended Audience :: Developers', 27 | 'Operating System :: OS Independent', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Programming Language :: Python', 30 | 'Topic :: Documentation', 31 | 'Topic :: Utilities' 32 | ], 33 | packages=find_packages(), 34 | install_requires=['sphinx', 'requests', 'requests-file', 'future'] 35 | ) 36 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================== 2 | sphinxcontrib-swaggerdoc 3 | ======================== 4 | 5 | Sphinx extension for documenting Swagger 2.0 APIs 6 | 7 | .. code:: bash 8 | 9 | pip install sphinxcontrib-swaggerdoc 10 | 11 | Usage 12 | ===== 13 | 14 | Include extension in ``conf.py`` 15 | 16 | .. code:: python 17 | 18 | extensions = ['sphinxcontrib.swaggerdoc'] 19 | 20 | Add directive pointing to a remote Swagger api-docs 21 | 22 | .. code:: restructuredtext 23 | 24 | .. swaggerv2doc:: URL/swagger.json 25 | 26 | or to a local file 27 | 28 | .. code:: restructuredtext 29 | 30 | .. swaggerv2doc:: file:///PATH/swagger.json 31 | 32 | For example 33 | 34 | .. code:: restructuredtext 35 | 36 | .. swaggerv2doc:: http://petstore.swagger.io/v2/swagger.json 37 | 38 | If the Swagger description contains multiple tags, you can select a subset 39 | for the documentation generation. For example, the following directive only 40 | generates the documentation for the methods contained in tags **pet** and 41 | **store**. 42 | 43 | .. code:: restructuredtext 44 | 45 | .. swaggerv2doc:: http://petstore.swagger.io/v2/swagger.json 46 | pet 47 | store 48 | 49 | Note 50 | ==== 51 | 52 | The old directive for Swagger 1.0 is still usable. For example, 53 | 54 | .. code:: restructuredtext 55 | 56 | .. swaggerdoc:: http://petstore.swagger.wordnik.com/api/api-docs/pet 57 | .. swaggerdoc:: http://petstore.swagger.wordnik.com/api/api-docs/user 58 | .. swaggerdoc:: http://petstore.swagger.wordnik.com/api/api-docs/store 59 | -------------------------------------------------------------------------------- /sphinxcontrib/swaggerdoc/swagger_doc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from docutils import nodes 3 | 4 | from docutils.parsers.rst import Directive 5 | 6 | from sphinx.locale import _ 7 | 8 | import requests 9 | import json 10 | 11 | class swaggerdoc(nodes.Admonition, nodes.Element): 12 | pass 13 | 14 | def visit_swaggerdoc_node(self, node): 15 | self.visit_admonition(node) 16 | 17 | def depart_swaggerdoc_node(self, node): 18 | self.depart_admonition(node) 19 | 20 | class SwaggerDocDirective(Directive): 21 | 22 | # this enables content in the directive 23 | has_content = True 24 | 25 | def processSwaggerURL(self, url): 26 | r = requests.get(url) 27 | 28 | return r.json()['apis'] 29 | 30 | def create_item(self, key, value): 31 | para = nodes.paragraph() 32 | para += nodes.strong('', key) 33 | para += nodes.Text(value) 34 | 35 | item = nodes.list_item() 36 | item += para 37 | 38 | return item 39 | 40 | def expand_values(self, list): 41 | expanded_values = '' 42 | for value in list: 43 | expanded_values += value + ' ' 44 | 45 | return expanded_values 46 | 47 | def make_operation(self, path, operation): 48 | swagger_node = swaggerdoc(path) 49 | swagger_node += nodes.title(path, operation['method'].upper() + ' ' + path) 50 | 51 | content = nodes.paragraph() 52 | content += nodes.Text(operation['summary']) 53 | 54 | bullet_list = nodes.bullet_list() 55 | bullet_list += self.create_item('Notes: ', operation.get('notes', '')) 56 | bullet_list += self.create_item('Consumes: ', self.expand_values(operation.get('consumes', ''))) 57 | bullet_list += self.create_item('Produces: ', self.expand_values(operation.get('produces', ''))) 58 | content += bullet_list 59 | 60 | swagger_node += content 61 | 62 | return [swagger_node] 63 | 64 | def run(self): 65 | try: 66 | methods = self.processSwaggerURL(self.content[0]) 67 | 68 | entries = [] 69 | 70 | for method in methods: 71 | for operation in method['operations']: 72 | entries += self.make_operation(method['path'], operation) 73 | 74 | return entries 75 | except: 76 | print('Unable to process URL: %s' % self.content[0]) 77 | error = nodes.error('') 78 | para = nodes.paragraph() 79 | para += nodes.Text('Unable to process URL: ') 80 | para += nodes.strong('', self.content[0]) 81 | para += nodes.Text('. Please check that the URL is a valid Swagger api-docs URL and it is accesible') 82 | error += para 83 | return [error] 84 | -------------------------------------------------------------------------------- /sphinxcontrib/swaggerdoc/swaggerv2_doc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from docutils import nodes 3 | import traceback 4 | 5 | from docutils.parsers.rst import Directive 6 | from past.builtins import basestring 7 | 8 | from sphinx.locale import _ 9 | 10 | from six.moves.urllib import parse as urlparse # Retain Py2 compatibility for urlparse 11 | import requests 12 | from requests_file import FileAdapter 13 | import json 14 | 15 | class swaggerv2doc(nodes.Admonition, nodes.Element): 16 | pass 17 | 18 | def visit_swaggerv2doc_node(self, node): 19 | self.visit_admonition(node) 20 | 21 | def depart_swaggerv2doc_node(self, node): 22 | self.depart_admonition(node) 23 | 24 | class SwaggerV2DocDirective(Directive): 25 | 26 | DEFAULT_GROUP = '' 27 | 28 | # this enables content in the directive 29 | has_content = True 30 | 31 | def processSwaggerURL(self, url): 32 | parsed_url = urlparse.urlparse(url) 33 | if not parsed_url.scheme: # Assume file relative to documentation 34 | env = self.state.document.settings.env 35 | relfn, absfn = env.relfn2path(url) 36 | env.note_dependency(relfn) 37 | 38 | with open(absfn) as fd: 39 | content = fd.read() 40 | 41 | return json.loads(content) 42 | else: 43 | s = requests.Session() 44 | s.mount('file://', FileAdapter()) 45 | r = s.get(url) 46 | return r.json() 47 | 48 | def create_item(self, key, value): 49 | para = nodes.paragraph() 50 | para += nodes.strong('', key) 51 | para += nodes.Text(value) 52 | 53 | item = nodes.list_item() 54 | item += para 55 | 56 | return item 57 | 58 | def expand_values(self, list): 59 | expanded_values = '' 60 | for value in list: 61 | expanded_values += value + ' ' 62 | 63 | return expanded_values 64 | 65 | def cell(self, contents): 66 | if isinstance(contents, basestring): 67 | contents = nodes.paragraph(text=contents) 68 | 69 | return nodes.entry('', contents) 70 | 71 | def row(self, cells): 72 | return nodes.row('', *[self.cell(c) for c in cells]) 73 | 74 | def create_table(self, head, body, colspec=None): 75 | table = nodes.table() 76 | tgroup = nodes.tgroup() 77 | table.append(tgroup) 78 | 79 | # Create a colspec for each column 80 | if colspec is None: 81 | colspec = [1 for n in range(len(head))] 82 | 83 | for width in colspec: 84 | tgroup.append(nodes.colspec(colwidth=width)) 85 | 86 | # Create the table headers 87 | thead = nodes.thead() 88 | thead.append(self.row(head)) 89 | tgroup.append(thead) 90 | 91 | # Create the table body 92 | tbody = nodes.tbody() 93 | tbody.extend([self.row(r) for r in body]) 94 | tgroup.append(tbody) 95 | 96 | return table 97 | 98 | def make_responses(self, responses): 99 | # Create an entry with swagger responses and a table of the response properties 100 | entries = [] 101 | paragraph = nodes.paragraph() 102 | paragraph += nodes.strong('', 'Responses') 103 | 104 | entries.append(paragraph) 105 | 106 | head = ['Name', 'Description', 'Type'] 107 | for response_name, response in responses.items(): 108 | paragraph = nodes.paragraph() 109 | paragraph += nodes.emphasis('', '%s - %s' % (response_name, 110 | response.get('description', ''))) 111 | entries.append(paragraph) 112 | 113 | body = [] 114 | 115 | # if the optional field properties is in the schema, display the properties 116 | if isinstance(response.get('schema'), dict) and 'properties' in response.get('schema'): 117 | for property_name, property in response.get('schema').get('properties', {}).items(): 118 | row = [] 119 | row.append(property_name) 120 | row.append(property.get('description', '')) 121 | row.append(property.get('type', '')) 122 | 123 | body.append(row) 124 | 125 | table = self.create_table(head, body) 126 | entries.append(table) 127 | return entries 128 | 129 | def make_parameters(self, parameters): 130 | entries = [] 131 | 132 | head = ['Name', 'Position', 'Description', 'Type'] 133 | body = [] 134 | for param in parameters: 135 | row = [] 136 | row.append(param.get('name', '')) 137 | row.append(param.get('in', '')) 138 | row.append(param.get('description', '')) 139 | row.append(param.get('type', '')) 140 | 141 | body.append(row) 142 | 143 | table = self.create_table(head, body) 144 | 145 | paragraph = nodes.paragraph() 146 | paragraph += nodes.strong('', 'Parameters') 147 | 148 | entries.append(paragraph) 149 | entries.append(table) 150 | 151 | return entries 152 | 153 | def make_method(self, path, method_type, method): 154 | swagger_node = swaggerv2doc(path) 155 | swagger_node += nodes.title(path, method_type.upper() + ' ' + path) 156 | 157 | paragraph = nodes.paragraph() 158 | paragraph += nodes.Text(method.get('summary', '')) 159 | 160 | bullet_list = nodes.bullet_list() 161 | 162 | method_sections = {'Description': 'description', 'Consumes': 'consumes', 'Produces': 'produces'} 163 | for title in method_sections: 164 | value_name = method_sections[title] 165 | value = method.get(value_name) 166 | if value is not None: 167 | bullet_list += self.create_item(title + ': \n', value) 168 | 169 | paragraph += bullet_list 170 | 171 | swagger_node += paragraph 172 | 173 | parameters = method.get('parameters') 174 | if parameters is not None: 175 | swagger_node += self.make_parameters(parameters) 176 | 177 | 178 | responses = method.get('responses') 179 | if responses is not None: 180 | swagger_node += self.make_responses(responses) 181 | 182 | return [swagger_node] 183 | 184 | def group_tags(self, api_desc): 185 | groups = {} 186 | 187 | if 'tags' in api_desc: 188 | for tag in api_desc['tags']: 189 | groups[tag['name']] = [] 190 | 191 | if len(groups) == 0: 192 | groups[SwaggerV2DocDirective.DEFAULT_GROUP] = [] 193 | 194 | for path, methods in api_desc['paths'].items(): 195 | for method_type, method in methods.items(): 196 | if SwaggerV2DocDirective.DEFAULT_GROUP in groups: 197 | groups[SwaggerV2DocDirective.DEFAULT_GROUP].append((path, method_type, method)) 198 | else: 199 | for tag in method['tags']: 200 | groups.setdefault(tag, []).append((path, method_type, method)) 201 | 202 | return groups 203 | 204 | def create_section(self, title): 205 | section = nodes.section(ids=[title]) 206 | section += nodes.title(title, title) 207 | return section 208 | 209 | def check_tags(self, selected_tags, tags, api_url): 210 | invalid_tags = list(set(selected_tags) - set(tags)) 211 | if len(invalid_tags) > 0: 212 | msg = self.reporter.error("Error. Tag '%s' not found in Swagger URL %s." % (invalid_tags[0], api_url)) 213 | return [msg] 214 | 215 | def run(self): 216 | self.reporter = self.state.document.reporter 217 | 218 | api_url = self.content[0] 219 | 220 | if len(self.content) > 1: 221 | selected_tags = self.content[1:] 222 | else: 223 | selected_tags = [] 224 | 225 | try: 226 | api_desc = self.processSwaggerURL(api_url) 227 | 228 | groups = self.group_tags(api_desc) 229 | 230 | self.check_tags(selected_tags, groups.keys(), api_url) 231 | 232 | entries = [] 233 | for tag_name, methods in groups.items(): 234 | if tag_name in selected_tags or len(selected_tags) == 0: 235 | section = self.create_section(tag_name) 236 | 237 | for path, method_type, method in methods: 238 | section += self.make_method(path, method_type, method) 239 | 240 | entries.append(section) 241 | 242 | return entries 243 | except Exception as e: 244 | error_message = 'Unable to process URL: %s' % api_url 245 | print(error_message) 246 | traceback.print_exc() 247 | 248 | error = nodes.error('') 249 | para_error = nodes.paragraph() 250 | para_error += nodes.Text(error_message + '. Please check that the URL is a valid Swagger api-docs URL and it is accesible') 251 | para_error_detailed = nodes.paragraph() 252 | para_error_detailed = nodes.strong('Processing error. See console output for a more detailed error') 253 | error += para_error 254 | error += para_error_detailed 255 | return [error] 256 | --------------------------------------------------------------------------------