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()