Files
python3-cookbook/source/c11/p05_creating_simple_rest_based_interface.rst
2015-05-07 16:05:14 +08:00

247 lines
9.1 KiB
ReStructuredText
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

===============================
11.5 生成一个简单的REST接口
===============================
----------
问题
----------
You want to be able to control or interact with your program remotely over the network
using a simple REST-based interface. However, you dont want to do it by installing a
full-fledged web programming framework.
|
----------
解决方案
----------
One of the easiest ways to build REST-based interfaces is to create a tiny library based
on the WSGI standard, as described in PEP 3333. Here is an example:
# resty.py
import cgi
def notfound_404(environ, start_response):
start_response('404 Not Found', [ ('Content-type', 'text/plain') ])
return [b'Not Found']
class PathDispatcher:
def __init__(self):
self.pathmap = { }
def __call__(self, environ, start_response):
path = environ['PATH_INFO']
params = cgi.FieldStorage(environ['wsgi.input'],
environ=environ)
method = environ['REQUEST_METHOD'].lower()
environ['params'] = { key: params.getvalue(key) for key in params }
handler = self.pathmap.get((method,path), notfound_404)
return handler(environ, start_response)
def register(self, method, path, function):
self.pathmap[method.lower(), path] = function
return function
To use this dispatcher, you simply write different handlers, such as the following:
import time
_hello_resp = '''\
<html>
<head>
<title>Hello {name}</title>
</head>
<body>
<h1>Hello {name}!</h1>
</body>
</html>'''
def hello_world(environ, start_response):
start_response('200 OK', [ ('Content-type','text/html')])
params = environ['params']
resp = _hello_resp.format(name=params.get('name'))
yield resp.encode('utf-8')
_localtime_resp = '''\
<?xml version="1.0"?>
<time>
<year>{t.tm_year}</year>
<month>{t.tm_mon}</month>
<day>{t.tm_mday}</day>
<hour>{t.tm_hour}</hour>
<minute>{t.tm_min}</minute>
<second>{t.tm_sec}</second>
</time>'''
def localtime(environ, start_response):
start_response('200 OK', [ ('Content-type', 'application/xml') ])
resp = _localtime_resp.format(t=time.localtime())
yield resp.encode('utf-8')
if __name__ == '__main__':
from resty import PathDispatcher
from wsgiref.simple_server import make_server
# Create the dispatcher and register functions
dispatcher = PathDispatcher()
dispatcher.register('GET', '/hello', hello_world)
dispatcher.register('GET', '/localtime', localtime)
# Launch a basic server
httpd = make_server('', 8080, dispatcher)
print('Serving on port 8080...')
httpd.serve_forever()
To test your server, you can interact with it using a browser or urllib. For example:
>>> u = urlopen('http://localhost:8080/hello?name=Guido')
>>> print(u.read().decode('utf-8'))
<html>
<head>
<title>Hello Guido</title>
</head>
<body>
<h1>Hello Guido!</h1>
</body>
</html>
>>> u = urlopen('http://localhost:8080/localtime')
>>> print(u.read().decode('utf-8'))
<?xml version="1.0"?>
<time>
<year>2012</year>
<month>11</month>
<day>24</day>
<hour>14</hour>
<minute>49</minute>
<second>17</second>
</time>
>>>
|
----------
讨论
----------
In REST-based interfaces, you are typically writing programs that respond to common
HTTP requests. However, unlike a full-fledged website, youre often just pushing data
around. This data might be encoded in a variety of standard formats such as XML, JSON,
or CSV. Although it seems minimal, providing an API in this manner can be a very
useful thing for a wide variety of applications.
For example, long-running programs might use a REST API to implement monitoring
or diagnostics. Big data applications can use REST to build a query/data extraction
system. REST can even be used to control hardware devices, such as robots, sensors,
mills, or lightbulbs. Whats more, REST APIs are well supported by various client-side
programming environments, such as Javascript, Android, iOS, and so forth. Thus, hav
ing such an interface can be a way to encourage the development of more complex
applications that interface with your code.
For implementing a simple REST interface, it is often easy enough to base your code on
the Python WSGI standard. WSGI is supported by the standard library, but also by most
third-party web frameworks. Thus, if you use it, there is a lot of flexibility in how your
code might be used later.
In WSGI, you simply implement applications in the form of a callable that accepts this
calling convention:
import cgi
def wsgi_app(environ, start_response):
...
The environ argument is a dictionary that contains values inspired by the CGI interface
provided by various web servers such as Apache [see Internet RFC 3875]. To extract
different fields, you would write code like this:
def wsgi_app(environ, start_response):
method = environ['REQUEST_METHOD']
path = environ['PATH_INFO']
# Parse the query parameters
params = cgi.FieldStorage(environ['wsgi.input'], environ=environ)
...
A few common values are shown here. environ['REQUEST_METHOD'] is the type of re
quest (e.g., GET, POST, HEAD, etc.). environ['PATH_INFO'] is the path or the resource
being requested. The call to cgi.FieldStorage() extracts supplied query parameters
from the request and puts them into a dictionary-like object for later use.
The start_response argument is a function that must be called to initiate a response.
The first argument is the resulting HTTP status. The second argument is a list of (name,
value) tuples that make up the HTTP headers of the response. For example:
def wsgi_app(environ, start_response):
...
start_response('200 OK', [('Content-type', 'text/plain')])
To return data, an WSGI application must return a sequence of byte strings. This can
be done using a list like this:
def wsgi_app(environ, start_response):
...
start_response('200 OK', [('Content-type', 'text/plain')])
resp = []
resp.append(b'Hello World\n')
resp.append(b'Goodbye!\n')
return resp
Alternatively, you can use yield:
def wsgi_app(environ, start_response):
...
start_response('200 OK', [('Content-type', 'text/plain')])
yield b'Hello World\n'
yield b'Goodbye!\n'
Its important to emphasize that byte strings must be used in the result. If the response
consists of text, it will need to be encoded into bytes first. Of course, there is no re
quirement that the returned value be text—you could easily write an application func
tion that creates images.
Although WSGI applications are commonly defined as a function, as shown, an instance
may also be used as long as it implements a suitable __call__() method. For example:
class WSGIApplication:
def __init__(self):
...
def __call__(self, environ, start_response)
...
This technique has been used to create the PathDispatcher class in the recipe. The
dispatcher does nothing more than manage a dictionary mapping (method, path) pairs
to handler functions. When a request arrives, the method and path are extracted and
used to dispatch to a handler. In addition, any query variables are parsed and put into
a dictionary that is stored as environ['params'] (this latter step is so common, it makes
a lot of sense to simply do it in the dispatcher in order to avoid a lot of replicated code).
To use the dispatcher, you simply create an instance and register various WSGI-style
application functions with it, as shown in the recipe. Writing these functions should be
extremely straightforward, as you follow the rules concerning the start_response()
function and produce output as byte strings.
One thing to consider when writing such functions is the careful use of string templates.
Nobody likes to work with code that is a tangled mess of print() functions, XML, and
various formatting operations. In the solution, triple-quoted string templates are being
defined and used internally. This particular approach makes it easier to change the
format of the output later (just change the template as opposed to any of the code that
uses it).
Finally, an important part of using WSGI is that nothing in the implementation is spe
cific to a particular web server. That is actually the whole idea—since the standard is
server and framework neutral, you should be able to plug your application into a wide
variety of servers. In the recipe, the following code is used for testing:
if __name__ == '__main__':
from wsgiref.simple_server import make_server
# Create the dispatcher and register functions
dispatcher = PathDispatcher()
...
# Launch a basic server
httpd = make_server('', 8080, dispatcher)
print('Serving on port 8080...')
httpd.serve_forever()
This will create a simple server that you can use to see if your implementation works.
Later on, when youre ready to scale things up to a larger level, you will change this code
to work with a particular server.
WSGI is an intentionally minimal specification. As such, it doesnt provide any support
for more advanced concepts such as authentication, cookies, redirection, and so forth.
These are not hard to implement yourself. However, if you want just a bit more support,
you might consider third-party libraries, such as WebOb or Paste.