sphinx-swagger¶
Generates a swagger API definition directly from httpdomain based documentation.
Usage¶
- Enable the extension in your conf.py by adding
'sphinxswagger'
to theextensions
list - Run the
swagger
builder (e.g.,setup.py swagger
) - 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.