Skip to content

Commit

Permalink
Merge pull request #1 from dave-shawley/initial-impl
Browse files Browse the repository at this point in the history
First few commits.
  • Loading branch information
dave-shawley committed Jun 6, 2016
2 parents 1897ed2 + b5c60ed commit ede41dc
Show file tree
Hide file tree
Showing 20 changed files with 978 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
build/
dist/
env/
*.egg-info/
__pycache__/
*.pyc

sample/sample/swagger.json
sample/sample/build/
sample/sample/dist/
sample/sample/env/
24 changes: 24 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,28 @@ 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 build_sphinx -b swagger``)
3. Use the generated *swagger.json*

Configuration
-------------
This extension contains a few useful configuration values:

: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**.

.. _httpdomain: https://pythonhosted.org/sphinxcontrib-httpdomain/
.. _License: https://github.com/OAI/OpenAPI-Specification/blob/master/
versions/2.0.md#licenseObject
75 changes: 75 additions & 0 deletions docs/advanced.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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:

.. code-block:: python
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.

.. literalinclude:: ../sample/sample/app.py
:pyobject: SwaggerHandler

.. _Swagger UI: http://swagger.io/swagger-ui/
.. _Tornado: https://tornadoweb.org/
23 changes: 23 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import alabaster
import sphinxswagger


project = 'sphinx-swagger'
copyright = '2016, Dave Shawley'
release = '.'.join(str(v) for v in sphinxswagger.version_info[:2])
version = sphinxswagger.__version__
needs_sphinx = '1.0'
extensions = []

master_doc = 'index'
html_theme = 'alabaster'
html_theme_path = [alabaster.get_path()]
html_sidebars = {
'**': ['about.html',
'navigation.html'],
}
html_theme_options = {
'description': 'Generate swagger definitions',
'github_user': 'dave-shawley',
'github_repo': 'sphinx-swagger',
}
49 changes: 49 additions & 0 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Contributing
============

Setting up your environment
---------------------------
First of all, build yourself a nice clean virtual environment using the
:mod:`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 [email protected]: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.

.. _virtualenv: https://virtualenv.pypa.io/en/stable/
7 changes: 7 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.. include:: ../README.rst

.. toctree::
:hidden:

advanced
contributing
2 changes: 1 addition & 1 deletion requires/installation.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Sphinx>=1.4,<2
sphinxcontrib.httpdomain==1.5.0
sphinxcontrib-httpdomain==1.5.0
2 changes: 2 additions & 0 deletions sample/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
graft docs
recursive-include sample *.json
Empty file added sample/README.rst
Empty file.
47 changes: 47 additions & 0 deletions sample/docs/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import alabaster
import sample


project = 'sample'
copyright = '2016, Dave Shawley.'
version = sample.__version__
release = '.'.join(str(x) for x in sample.version_info[:2])

needs_sphinx = '1.0'
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
'sphinxcontrib.autohttp.tornado',
'sphinxswagger',
]
templates_path = []
source_suffix = '.rst'
source_encoding = 'utf-8-sig'
master_doc = 'index'
pygments_style = 'sphinx'
html_theme = 'alabaster'
html_theme_path = [alabaster.get_path()]
html_static_path = []
html_sidebars = {
'**': [
'about.html',
],
}
html_theme_options = {
'description': 'Sample HTTP API',
'github_banner': False,
'github_button': False,
'travis_button': False,
}

intersphinx_mapping = {
'python': ('http://docs.python.org/', None),
'tornado': ('http://tornadoweb.org/en/latest/', None),
}

swagger_license = {'name': 'BSD 3-clause',
'url': 'https://opensource.org/licenses/BSD-3-Clause'}
4 changes: 4 additions & 0 deletions sample/docs/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Sample API Documentation
========================

.. autotornado:: sample.app:Application()
2 changes: 2 additions & 0 deletions sample/sample/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version_info = (0, 0, 0)
__version__ = '.'.join(str(v) for v in version_info)
120 changes: 120 additions & 0 deletions sample/sample/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import datetime
import hashlib
import json
import logging
import os.path
import pkg_resources
import signal

from tornado import ioloop, web

from sample import simple_handlers


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


class Application(web.Application):

def __init__(self, io_loop=None, **kwargs):
self.io_loop = kwargs.pop('io_loop', ioloop.IOLoop.current())
swagger_path = pkg_resources.resource_filename('sample',
'swagger.json')
super(Application, self).__init__(
[web.url('/ip', simple_handlers.IPHandler),
web.url('/echo', simple_handlers.MethodHandler),
web.url('/status/(?P<code>\d+)', simple_handlers.StatusHandler),
web.url('/swagger.json', SwaggerHandler,
{'swagger_path': swagger_path})],
**kwargs)

self.logger = logging.getLogger(self.__class__.__name__)
signal.signal(signal.SIGINT, self.handle_signal)
signal.signal(signal.SIGTERM, self.handle_signal)

def handle_signal(self, signo, frame):
self.io_loop.add_callback_from_signal(self.io_loop.stop)


if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG,
format='%(levelname)1.1s - %(name)s: %(message)s')
iol = ioloop.IOLoop.current()
app = Application(io_loop=iol, debug=True)
app.listen(8888)
iol.start()
Loading

0 comments on commit ede41dc

Please sign in to comment.