Skip to content

Commit

Permalink
Merge pull request #5 from dave-shawley/setuptools-task
Browse files Browse the repository at this point in the history
Add a setuptools task
  • Loading branch information
dave-shawley authored Mar 22, 2017
2 parents f38e7f0 + 0d9b87f commit f3f1354
Show file tree
Hide file tree
Showing 7 changed files with 582 additions and 244 deletions.
30 changes: 28 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,38 @@ Usage

1. Enable the extension in your *conf.py* by adding ``'sphinxswagger'``
to the ``extensions`` list
2. Run the ``swagger`` builder (e.g., ``setup.py build_sphinx -b swagger``)
2. Run the ``swagger`` builder (e.g., ``setup.py swagger``)
3. Use the generated *swagger.json*

Setuptools Command
------------------
This library installs a new command named **swagger** that is available
from the *setup.py* utility. It runs sphinx to generate the swagger
output file. It is similar to running ``sphinx-build -b swagger`` except
that it has access to your packages metadata so you don't have to
configure it in two places!

**This is the recommend approach for using this package.**

You can configure the output file name in your project's *setup.cfg* in
the ``[swagger]`` section::

[swagger]
output-file = static/swagger.json

This makes it easier to include it directly into your built artifact
by adding it as ``package_data`` in *setup.py*. Remember to add it to
your *MANIFEST.in* as well.

Configuration
-------------
This extension contains a few useful configuration values:
This extension contains a few useful configuration values that can be
set from within the sphinx configuration file.

:swagger_description:
Sets the description of the application in the generated swagger file.
If this is not set, then the "description" value in ``html_theme_options``
will be used if it is set.

:swagger_file:
Sets the name of the generated swagger file. The file is always
Expand Down
9 changes: 8 additions & 1 deletion docs/history.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Release History
===============

`0.0.3`_ (2017 Mar 22)
----------------------
- Rewrite to be more resilient to changes in the underlying
docutil structure.
- Added ``swagger`` setup command.

`0.0.2`_ (2017 Mar 2)
---------------------
- Added support for JSON responses.
Expand All @@ -9,6 +15,7 @@ Release History
----------------------
- Initial release with simple Sphinx writer+builder.

.. _Next Release: https://github.com/dave-shawley/sphinx-swagger/compare/0.0.2...HEAD
.. _Next Release: https://github.com/dave-shawley/sphinx-swagger/compare/0.0.3...HEAD
.. _0.0.3: https://github.com/dave-shawley/sphinx-swagger/compare/0.0.2...0.0.3
.. _0.0.2: https://github.com/dave-shawley/sphinx-swagger/compare/0.0.1...0.0.2
.. _0.0.1: https://github.com/dave-shawley/sphinx-swagger/compare/0.0.0...0.0.1
3 changes: 2 additions & 1 deletion sphinxswagger/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version_info = (0, 0, 2)
version_info = (0, 0, 3)
__version__ = '.'.join(str(v) for v in version_info)


Expand All @@ -17,6 +17,7 @@ def setup(app):
app.add_builder(builder.SwaggerBuilder)
app.add_config_value('swagger_file', 'swagger.json', True)
app.add_config_value('swagger_license', {'name': 'Proprietary'}, True)
app.add_config_value('swagger_description', '', True)
app.connect('build-finished', writer.write_swagger_file)

return {'version': __version__}
6 changes: 2 additions & 4 deletions sphinxswagger/builder.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
from __future__ import print_function

import docutils.io

from sphinx import builders

from . import writer
from . import document, writer


class SwaggerBuilder(builders.Builder):
Expand All @@ -17,7 +15,7 @@ def init(self):

def prepare_writing(self, docnames):
"""Called before :meth:`write_doc`"""
self.swagger = writer.SwaggerDocument()
self.swagger = document.SwaggerDocument()
self.writer = writer.SwaggerWriter(swagger_document=self.swagger)

def write_doc(self, docname, doctree):
Expand Down
68 changes: 68 additions & 0 deletions sphinxswagger/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from distutils import cmd, log
import os.path

from sphinx import application


class BuildSwagger(cmd.Command):
description = 'Build a swagger definition from Sphinx docs'
user_options = [
('config-dir=', 'c', 'configuration directory'),
('output-file=', 'o', 'output file name'),
('ignore-distinfo', 'u', 'ignore distribution metadata'),
]
boolean_options = ['ignore-distinfo']

def initialize_options(self):
self.config_dir = None
self.output_file = None
self.ignore_distinfo = False

def finalize_options(self):
if self.config_dir is None:
self.config_dir = 'docs'
self.ensure_dirname('config_dir')
if self.config_dir is None:
self.config_dir = os.curdir
self.warning('Using {} as configuration directory',
self.source_dir)
self.config_dir = os.path.abspath(self.config_dir)

if self.output_file is not None:
self.output_file = os.path.abspath(self.output_file)

def run(self):
build_cmd = self.get_finalized_command('build')
build_dir = os.path.join(os.path.abspath(build_cmd.build_base),
'swagger')
self.mkpath(build_dir)
doctree_dir = os.path.join(build_dir, 'doctrees')
self.mkpath(doctree_dir)

overrides = {}
if self.output_file is not None:
overrides['swagger_file'] = self.output_file

if not self.ignore_distinfo:
if self.distribution.get_description():
overrides['swagger_description'] = \
self.distribution.get_description()
if self.distribution.get_license():
overrides['swagger_license.name'] = \
self.distribution.get_license()
if self.distribution.get_version():
overrides['version'] = self.distribution.get_version()

app = application.Sphinx(
self.config_dir, self.config_dir, build_dir, doctree_dir,
'swagger', confoverrides=overrides)
app.build()

def warning(self, msg, *args):
self.announce(msg.format(*args), level=log.WARNING)

def info(self, msg, *args):
self.announce(msg.format(*args), level=log.INFO)

def debug(self, msg, *args):
self.announce(msg.format(*args), level=log.DEBUG)
132 changes: 132 additions & 0 deletions sphinxswagger/document.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
try:
import http.client as http_client
except ImportError:
import httplib as http_client


class SwaggerDocument(object):

def __init__(self):
super(SwaggerDocument, self).__init__()
self._paths = {}

def get_document(self, config):
"""
:param sphinx.config.Config config: project level configuration
:return: the swagger document as a :class:`dict`
:rtype: dict
"""
info = {'title': config.project,
'description': config.swagger_description,
'license': config.swagger_license,
'version': config.version}
if not info['description'] and hasattr(config, 'html_theme_options'):
info['description'] = config.html_theme_options.get('description')

return {'swagger': '2.0',
'info': info,
'host': 'localhost:80',
'basePath': '/',
'paths': self._paths}

def add_endpoint(self, endpoint, debug_info=None):
"""
Add a swagger endpoint document.
:param SwaggerEndpoint endpoint: the endpoint to add
:param dict debug_info: optional debug information to include
in the swagger definition
"""
path_info = self._paths.setdefault(endpoint.uri_template, {})
if endpoint.method in path_info:
pass # already gots this ... good this isn't
path_info[endpoint.method] = endpoint.generate_swagger()
if debug_info:
path_info[endpoint.method]['x-debug-info'] = debug_info


class SwaggerEndpoint(object):

def __init__(self):
self.method = None
self.uri_template = None
self.summary = ''
self.description = ''
self.parameters = []
self.responses = {}
self.default_response_schema = None
self.response_headers = None

def set_default_response_structure(self, properties, is_array=False):
schema = {'type': 'object', 'properties': {}, 'required': []}
for prop in properties:
name = prop.pop('name')
schema['properties'][name] = prop.copy()
schema['required'].append(name)
if is_array:
schema = {'type': 'array', 'items': schema}
self.default_response_schema = schema

def add_request_headers(self, headers):
for name, description in headers.items():
self.parameters.append({
'name': name,
'description': description,
'in': 'header',
'type': 'string',
})

def add_response_headers(self, headers):
self.response_headers = {
name: {'description': description, 'type': 'string'}
for name, description in headers.items()
}

def add_response_codes(self, status_dict):
for code, info in status_dict.items():
swagger_rsp = self.responses.setdefault(code, {})
if not info['reason']:
try:
code = int(code)
info['reason'] = http_client.responses[code]
except (KeyError, TypeError, ValueError):
info['reason'] = 'Unknown'

tokens = info['description'].split(maxsplit=2)
if tokens:
tokens[0] = tokens[0].title()
swagger_rsp['description'] = '{}\n\n{}'.format(
info['reason'], ' '.join(tokens)).strip()

def generate_swagger(self):
swagger = {'summary': self.summary, 'description': self.description}
if self.parameters:
swagger['parameters'] = self.parameters

if self.responses:
swagger['responses'] = self.responses
else: # swagger requires at least one response
swagger['responses'] = {'default': {'description': ''}}

# Figure out where to put the response schema and response
# header details. This is probably going to change in the
# future since it is `hinky' at best.
default_code = 'default'
status_codes = sorted(int(code)
for code in swagger['responses']
if code.isdigit())
for code in status_codes:
if 200 <= code < 400:
default_code = str(code)
break

if default_code in swagger['responses']:
if self.default_response_schema:
swagger['responses'][default_code]['schema'] = \
self.default_response_schema
if self.response_headers:
swagger['responses'][default_code]['headers'] = \
self.response_headers

return swagger
Loading

0 comments on commit f3f1354

Please sign in to comment.