Skip to content

Commit

Permalink
Merge pull request #37 from hfaran/urlannotations
Browse files Browse the repository at this point in the history
URL Annotations
  • Loading branch information
hfaran committed Feb 23, 2014
2 parents a608cef + 2d45496 commit 355c47c
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 43 deletions.
6 changes: 3 additions & 3 deletions demos/helloworld/API_Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Output schemas only represent `data` and not the full output; see output examples and the JSend specification.**

# /api/asynchelloworld
# /api/asynchelloworld/?

Content-Type: application/json

Expand Down Expand Up @@ -66,7 +66,7 @@ Greets you.
<br>
<br>

# /api/helloworld
# /api/helloworld/?

Content-Type: application/json

Expand Down Expand Up @@ -98,7 +98,7 @@ Shouts hello to the world!
<br>
<br>

# /api/postit
# /api/postit/?

Content-Type: application/json

Expand Down
9 changes: 6 additions & 3 deletions demos/helloworld/helloworld.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,24 @@
sys.path.append("../../")
# ---- Can be removed if Tornado-JSON is installed ----#

import json
import tornado.ioloop
from tornado_json.routes import get_routes
from tornado_json.application import Application

import helloworld


def main():
# Pass the web app's package the get_routes and it will generate
# routes based on the submodule names and ending with lowercase
# request handler name (with 'handler' removed from the end of the
# name if it is the name).
# [("/api/helloworld", helloworld.api.HelloWorldHandler)]
import helloworld
routes = get_routes(helloworld)

print("Routes\n======\n\n" + json.dumps(
[(url, repr(rh)) for url, rh in routes],
indent=2)
)
# Create the application by passing routes and any settings
application = Application(routes=routes, settings={})

Expand Down
3 changes: 3 additions & 0 deletions demos/rest_api/API_Documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**This documentation is automatically generated.**

**Output schemas only represent `data` and not the full output; see output examples and the JSend specification.**
32 changes: 32 additions & 0 deletions demos/rest_api/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python2.7

# ---- The following so demo can be run without having to install package ----#
import sys
sys.path.append("../../")
# ---- Can be removed if Tornado-JSON is installed ----#

# This module contains essentially the same boilerplate
# as the corresponding one in the helloworld example;
# refer to that for details.

import json
import tornado.ioloop
from tornado_json.routes import get_routes
from tornado_json.application import Application


def main():
import cars
routes = get_routes(cars)
print("Routes\n======\n\n" + json.dumps(
[(url, repr(rh)) for url, rh in routes],
indent=2)
)
application = Application(routes=routes, settings={})

application.listen(8888)
tornado.ioloop.IOLoop.instance().start()


if __name__ == '__main__':
main()
Empty file added demos/rest_api/cars/__init__.py
Empty file.
116 changes: 116 additions & 0 deletions demos/rest_api/cars/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from tornado_json.requesthandlers import APIHandler


DATA = {
"Ford": {
"Fusion": {
"2013": "http://www.ford.ca/cars/fusion/2013/",
"2014": "http://www.ford.ca/cars/fusion/2014/"
},
"Taurus": {
"2013": "http://www.ford.ca/cars/taurus/2013/",
"2014": "http://www.ford.ca/cars/taurus/2014/"
}
}
}


class CarsAPIHandler(APIHandler):

# APIHandler has two "special" attributes:
# * __url_names__ :
# This is a list of names you'd like the
# the requesthandler to be called in auto-
# generated routes, i.e., since the absolute
# path of this handler (in context of the
# ``cars`` package) is, ``cars.api.CarsAPIHandler``,
# if you've read earlier documentation, you'll
# know that the associated URL to this that
# will be autogenerated is "/api/carsapi".
# __url_names__ can change the last ``carsapi``
# part to whatever is in the list. So in this
# case, we change it to "/api/cars". If we added
# additional names to the list, we would generate
# more routes with the given names.
# Of course, this isn't an actual handler, just
# a handy superclass that we'll let all of the handlers
# below inherit from so they can have a base URL of
# "/api/cars" also, but extended to match additional
# things based on the parameters of their ``get`` methods.
#
# An important note on __url_names__ is that by default,
# it exists as ``__url_names__ = ["__self__"]``. The
# ``__self__`` is a special value, which means that the
# requesthandler should get a URL such as the ones you
# assigned to the handlers in the Hello World demo.
# You can either ADD to the list to keep this, or
# create a new list to not.
#
# * __urls__ :
# I'll mention __urls__ as well; this let's you just
# assign a completely custom URL pattern to match to
# the requesthandlers, i.e., I could add something like,
# ``__urls__ = [r"/api/cars/?"]``
# and that would give me the exact same URL mapped
# to this handler, but defined by me. Note that both
# URLs generated from ``__url_names__`` and URLs provided
# by you in ``__urls__`` will be created and assigned to
# the associated requesthandler, so make sure to modify/overwrite
# both attributes to get only the URLs mapped that you want.
#
__url_names__ = ["cars"]


class MakeListHandler(CarsAPIHandler):

def get(self):
self.success(DATA.keys())


class MakeHandler(CarsAPIHandler):

def get(self, make):
try:
self.success(DATA[make])
except KeyError:
self.fail("No data on such make `{}`.".format(make))


class ModelHandler(CarsAPIHandler):

def get(self, make, model):
try:
self.success(DATA[make][model])
except KeyError:
self.fail("No data on `{} {}`.".format(make, model))


class YearHandler(CarsAPIHandler):

def get(self, make, model, year):
try:
self.success(DATA[make][model][year])
except KeyError:
self.fail("No data on `{} {} {}`.".format(year, make, model))


# Routes for the handlers above will look like this:
#
# [
# [
# "/api/cars/?",
# "<class 'cars.api.MakeListHandler'>"
# ],
# [
# "/api/cars/(?P<make>[a-zA-Z0-9_]+)/(?P<model>[a-zA-Z0-9_]+)/?$",
# "<class 'cars.api.ModelHandler'>"
# ],
# [
# "/api/cars/(?P<make>[a-zA-Z0-9_]+)/(?P<model>[a-zA-Z0-9_]+)/(?P<year>[a-zA-Z0-9_]+)/?$",
# "<class 'cars.api.YearHandler'>"
# ],
# [
# "/api/cars/(?P<make>[a-zA-Z0-9_]+)/?$",
# "<class 'cars.api.MakeHandler'>"
# ]
# ]
9 changes: 9 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ _
---------


v0.30 - URL Annotations
~~~~~~~~~~~~~~~~~~~~~~~

* Added ``__urls__`` and ``__url_names__`` attributes to allow flexible creation of custom URLs that make creating REST APIs etc. easy
* Added a REST API demo as an example for URL annotations
* Added URL annotations documentation
* Refactored and improved route generation in ``routes``


v0.20 - Refactor of ``utils`` module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Some of the key features the included modules provide:
using_tornado_json
requesthandler_guidelines
docgen
restapi
changelog
tornado_json

Expand Down
16 changes: 16 additions & 0 deletions docs/restapi.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Creating a REST API Using URL Annotations
=========================================

You may have noticed that the automatic URL generation
is meant to be quick and easy-to-use for simple cases (creating an
API in 15 minutes kind of thing).

It is more powerful though, however, as you can customize it
to get the URLs for RequestHandlers how you want without
having to make additions to output from ``routes.get_routes``
yourself. This is done through the use of "URL annotations".
``APIHandler`` and ``ViewHandler`` have two "magic" attributes
(``__urls__`` and ``__url_names__``) that allow you to define custom routes right in the handler
body. See relevant documentation in the
`REST API <https://github.com/hfaran/Tornado-JSON/blob/master/demos/rest_api/cars/api/__init__.py>`__
example in the demos.
42 changes: 27 additions & 15 deletions tests/test_tornado_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
from tornado_json import exceptions
from tornado_json import jsend
sys.path.append('demos/helloworld')
sys.path.append('demos/rest_api')
import helloworld
import cars
except ImportError as e:
print("Please run `py.test` from the root project directory")
exit(1)
Expand Down Expand Up @@ -48,12 +50,22 @@ def test_get_routes(self):
"""Tests routes.get_routes"""
assert sorted(routes.get_routes(
helloworld)) == sorted([
("/api/helloworld", helloworld.api.HelloWorldHandler),
("/api/asynchelloworld", helloworld.api.AsyncHelloWorld),
("/api/postit", helloworld.api.PostIt),
("/api/greeting/(?P<fname>[a-zA-Z0-9_]+)/(?P<lname>[a-zA-Z0-9_]+)/?$",
("/api/helloworld/?", helloworld.api.HelloWorldHandler),
("/api/asynchelloworld/?", helloworld.api.AsyncHelloWorld),
("/api/postit/?", helloworld.api.PostIt),
("/api/greeting/(?P<fname>[a-zA-Z0-9_]+)/"
"(?P<lname>[a-zA-Z0-9_]+)/?$",
helloworld.api.Greeting),
("/api/freewilled", helloworld.api.FreeWilledHandler)
("/api/freewilled/?", helloworld.api.FreeWilledHandler)
])
assert sorted(routes.get_routes(
cars)) == sorted([
("/api/cars/?", cars.api.MakeListHandler),
("/api/cars/(?P<make>[a-zA-Z0-9_]+)/(?P<model>[a-zA-Z0-9_]+)/?$",
cars.api.ModelHandler),
("/api/cars/(?P<make>[a-zA-Z0-9_]+)/(?P<model>[a-zA-Z0-9_]+)/"
"(?P<year>[a-zA-Z0-9_]+)/?$", cars.api.YearHandler),
("/api/cars/(?P<make>[a-zA-Z0-9_]+)/?$", cars.api.MakeHandler),
])

def test_gen_submodule_names(self):
Expand All @@ -64,13 +76,13 @@ def test_gen_submodule_names(self):
def test_get_module_routes(self):
"""Tests routes.get_module_routes"""
assert sorted(routes.get_module_routes(
'helloworld.api')) == sorted([
("/api/helloworld", helloworld.api.HelloWorldHandler),
("/api/asynchelloworld", helloworld.api.AsyncHelloWorld),
("/api/postit", helloworld.api.PostIt),
("/api/greeting/(?P<fname>[a-zA-Z0-9_]+)/(?P<lname>[a-zA-Z0-9_]+)/?$",
helloworld.api.Greeting),
("/api/freewilled", helloworld.api.FreeWilledHandler)
"cars.api")) == sorted([
("/api/cars/?", cars.api.MakeListHandler),
("/api/cars/(?P<make>[a-zA-Z0-9_]+)/(?P<model>[a-zA-Z0-9_]+)/?$",
cars.api.ModelHandler),
("/api/cars/(?P<make>[a-zA-Z0-9_]+)/(?P<model>[a-zA-Z0-9_]+)/"
"(?P<year>[a-zA-Z0-9_]+)/?$", cars.api.YearHandler),
("/api/cars/(?P<make>[a-zA-Z0-9_]+)/?$", cars.api.MakeHandler),
])


Expand Down Expand Up @@ -159,15 +171,15 @@ def post(self):
# th = self.TerribleHandler()
# rh = self.ReasonableHandler()

# # Expect a TypeError to be raised because of invalid output
# Expect a TypeError to be raised because of invalid output
# with pytest.raises(TypeError):
# th.get("Duke", "Flywalker")

# # Expect a validation error because of invalid input
# Expect a validation error because of invalid input
# with pytest.raises(ValidationError):
# th.post()

# # Both of these should succeed as the body matches the schema
# Both of these should succeed as the body matches the schema
# with pytest.raises(SuccessException):
# rh.get("J", "S")
# with pytest.raises(SuccessException):
Expand Down
2 changes: 1 addition & 1 deletion tornado_json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
# Alternatively, just put the version in a text file or something to avoid
# this.

__version__ = '0.20'
__version__ = '0.30'
3 changes: 3 additions & 0 deletions tornado_json/requesthandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class BaseHandler(RequestHandler):

"""BaseHandler for all other RequestHandlers"""

__url_names__ = ["__self__"]
__urls__ = []

@property
def db_conn(self):
"""Returns database connection abstraction
Expand Down
Loading

0 comments on commit 355c47c

Please sign in to comment.