sphinx-swagger

Generates a swagger API definition directly from httpdomain based documentation.

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 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 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 generated in the sphinx output directory – usually build/sphinx/swagger. The default file name is swagger.json.
swagger_license:
 A dictionary that describes the license that governs the API. This is written as-is to the License section of the API document. It should contain two keys – name and url.

Advanced Usage

Including your definition in a package

The goal is to generate a swagger.json file and include it into your source distribution. There are a few reasons for doing this but the most obvious is to serve this file from a endpoint within your application. I do this in the example project by embedding the JSON file in a package data directory as shown in the following tree:

<project-root>/
|-- docs/
|   |-- conf.py
|   `-- index.rst
|-- MANIFEST.in
|-- README.rst
|-- sample/
|   |-- __init__.py
|   |-- app.py
|   |-- simple_handlers.py
|   `-- swagger.json
`-- setup.py

The MANIFEST.in controls which files are included in a source distribution. Since you will be generating the API definition when you build your package, you aren’t required to include the definition in the source distribution but you should. This is pretty simple:

graft docs
recursive-include sample *.json

That takes care of the source distributions. The API definition also needs to be added to binary distributions if you want to serve it from within an application. You need to modify your setup.py for this:

import setuptools

setuptools.setup(
   name='sample',
   # ...
   packages=['sample'],
   package_data={'': ['**/*.json']},
   include_package_data=True,
)

This tells the setuptools machinery to include any JSON files that it finds in a package directory in the binary distribution.

Now for the awful part... there is no easy way to do this using the standard setup.py build_sphinx command. It will always generate the swagger directory and does not let you customize the location of the doctrees. Use the sphinx-build utility instead:

$ sphinx-build -b swagger -d build/tmp docs sample

That will generate the swagger.json directly into the sample package. Alternatively, you can use setup.py build_sphinx and copy the API definition into the package before generating the distribution.

Serving the API definition

The Swagger UI allows you to browse an API by pointing at it’s API definition file. Once the API definition is packaged into your application as described above, it is relatively easy to write a handler to serve the document. The following snippet implements one such handler in the Tornado web framework.

class SwaggerHandler(web.RequestHandler):
    """Tornado request handler for serving a API definition."""

    def initialize(self, swagger_path):
        super(SwaggerHandler, self).initialize()
        self.swagger_path = swagger_path
        self.application.settings.setdefault('swagger_state', {
            'document': None,
            'last-read': None,
        })

    def set_default_headers(self):
        super(SwaggerHandler, self).set_default_headers()
        self.set_header('Access-Control-Allow-Origin', '*')

    def options(self, *args):
        self.set_header('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')
        self.set_status(204)
        self.finish()

    def head(self):
        """Retrieve API definition metadata."""
        last_modified = datetime.datetime.utcfromtimestamp(
            self.swagger_state['last-modified'])
        self.set_header('Last-Modified',
                        last_modified.strftime('%a, %d %b %Y %H:%M:%S GMT'))
        self.set_header('Content-Type', 'application/json')
        self.set_header('ETag', self.compute_etag())
        self.set_status(204)
        self.finish()

    def get(self):
        """Retrieve the API definition."""
        try:
            if self.request.headers['If-None-Match'] == self.compute_etag():
                self.set_status(304)
                return
        except KeyError:
            pass

        self.swagger_state['document']['host'] = self.request.host
        last_modified = datetime.datetime.utcfromtimestamp(
            self.swagger_state['last-modified'])
        self.set_header('Content-Type', 'application/json')
        self.set_header('Last-Modified',
                        last_modified.strftime('%a, %d %b %Y %H:%M:%S GMT'))
        self.write(self.swagger_state['document'])

    @property
    def swagger_state(self):
        """
        Returns a :class:`dict` containing the cached state.

        :return: :class:`dict` containing the following keys: ``document``,
            ``last-modified``, and ``digest``.
        :rtype: dict
        """
        self.refresh_swagger_document()
        return self.application.settings['swagger_state']

    def compute_etag(self):
        """Return the digest of the document for use as an ETag."""
        return self.swagger_state['digest']

    def refresh_swagger_document(self):
        state = self.application.settings['swagger_state']
        last_modified = os.path.getmtime(self.swagger_path)
        if state['document']:
            if last_modified <= state['last-modified']:
                return

        with open(self.swagger_path, 'rb') as f:
            raw_data = f.read()
            state['document'] = json.loads(raw_data.decode('utf-8'))
        state['last-modified'] = last_modified
        state['digest'] = hashlib.md5(raw_data).hexdigest()

Contributing

Setting up your environment

First of all, build yourself a nice clean virtual environment using the venv module (or virtualenv if you must). Then pull in the requirements:

sphinx-swagger$ python3 -mvenv env
sphinx-swagger$ env/bin/pip install -qr requires/development.txt

Then you can test the package using the embedded sample package starting with the same pattern:

sphinx-swagger$ cd sample
sample$ python3 -mvenv env
sample$ env/bin/python setup.py develop
sample$ env/bin/pip install -e ..
sample$ env/bin/sphinx-build -b swagger -d build/tmp docs sample
sample$ env/bin/python sample/app.py

This will run the Tornado stack and serve the API definition at /swagger.json on port 8888 – http://localhost:8888/swagger.json You can use the Swagger UI to browse the generated documentation in a web browser as well:

sample$ git clone git@github.com:swagger-api/swagger-ui.git
sample$ open swagger-ui/dist/index.html

Point it at the Tornado application on localhost and you should get a nice way to browse the API.

Seeing Changes

If you followed the installation instructions above, then you have a locally running Tornado application that is serving a API definition and a local version of the Swagger UI running. Changes to the Sphinx extension can be easily tested by running the sphinx-build command in the sample directory. The swagger.json file will be regenerated and picked up the next time that it is requested from the UI.

Giving it Back

Once you have something substantial that you would like to contribute back to the extension, push your branch up to github.com and issue a Pull Request against the main repository.

Release History

0.0.4 (2017 Jun 30)

  • Loosened the pin on sphinxcontrib-httpdomain.
  • Fixed breakage by newer versions of sphinxcontrib-httpdomain.

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.

0.0.1 (2016 Jun 06)

  • Initial release with simple Sphinx writer+builder.