-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from dave-shawley/initial-impl
First few commits.
- Loading branch information
Showing
20 changed files
with
978 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
.. include:: ../README.rst | ||
|
||
.. toctree:: | ||
:hidden: | ||
|
||
advanced | ||
contributing |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
graft docs | ||
recursive-include sample *.json |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
Sample API Documentation | ||
======================== | ||
|
||
.. autotornado:: sample.app:Application() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.