11.05 小节完成~
This commit is contained in:
@@ -1,246 +1,246 @@
|
|||||||
===============================
|
===============================
|
||||||
11.5 生成一个简单的REST接口
|
11.5 创建一个简单的REST接口
|
||||||
===============================
|
===============================
|
||||||
|
|
||||||
----------
|
----------
|
||||||
问题
|
问题
|
||||||
----------
|
----------
|
||||||
You want to be able to control or interact with your program remotely over the network
|
你想使用一个简单的REST接口通过网络远程控制或访问你的应用程序,但是你又不想自己去安装一个完整的web框架。
|
||||||
using a simple REST-based interface. However, you don’t 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
|
构建一个REST风格的接口最简单的方法是创建一个基于WSGI标准(PEP 3333)的很小的库,下面是一个例子:
|
||||||
on the WSGI standard, as described in PEP 3333. Here is an example:
|
|
||||||
|
|
||||||
# resty.py
|
.. code-block:: python
|
||||||
|
|
||||||
import cgi
|
# resty.py
|
||||||
|
|
||||||
def notfound_404(environ, start_response):
|
import cgi
|
||||||
start_response('404 Not Found', [ ('Content-type', 'text/plain') ])
|
|
||||||
return [b'Not Found']
|
|
||||||
|
|
||||||
class PathDispatcher:
|
def notfound_404(environ, start_response):
|
||||||
def __init__(self):
|
start_response('404 Not Found', [ ('Content-type', 'text/plain') ])
|
||||||
self.pathmap = { }
|
return [b'Not Found']
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
class PathDispatcher:
|
||||||
path = environ['PATH_INFO']
|
def __init__(self):
|
||||||
params = cgi.FieldStorage(environ['wsgi.input'],
|
self.pathmap = { }
|
||||||
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):
|
def __call__(self, environ, start_response):
|
||||||
self.pathmap[method.lower(), path] = function
|
path = environ['PATH_INFO']
|
||||||
return function
|
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)
|
||||||
|
|
||||||
To use this dispatcher, you simply write different handlers, such as the following:
|
def register(self, method, path, function):
|
||||||
|
self.pathmap[method.lower(), path] = function
|
||||||
|
return function
|
||||||
|
|
||||||
import time
|
为了使用这个调度器,你只需要编写不同的处理器,就像下面这样:
|
||||||
|
|
||||||
_hello_resp = '''\
|
.. code-block:: python
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Hello {name}</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Hello {name}!</h1>
|
|
||||||
</body>
|
|
||||||
</html>'''
|
|
||||||
|
|
||||||
def hello_world(environ, start_response):
|
import time
|
||||||
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 = '''\
|
_hello_resp = '''\
|
||||||
<?xml version="1.0"?>
|
<html>
|
||||||
<time>
|
<head>
|
||||||
<year>{t.tm_year}</year>
|
<title>Hello {name}</title>
|
||||||
<month>{t.tm_mon}</month>
|
</head>
|
||||||
<day>{t.tm_mday}</day>
|
<body>
|
||||||
<hour>{t.tm_hour}</hour>
|
<h1>Hello {name}!</h1>
|
||||||
<minute>{t.tm_min}</minute>
|
</body>
|
||||||
<second>{t.tm_sec}</second>
|
</html>'''
|
||||||
</time>'''
|
|
||||||
|
|
||||||
def localtime(environ, start_response):
|
def hello_world(environ, start_response):
|
||||||
start_response('200 OK', [ ('Content-type', 'application/xml') ])
|
start_response('200 OK', [ ('Content-type','text/html')])
|
||||||
resp = _localtime_resp.format(t=time.localtime())
|
params = environ['params']
|
||||||
yield resp.encode('utf-8')
|
resp = _hello_resp.format(name=params.get('name'))
|
||||||
|
yield resp.encode('utf-8')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
_localtime_resp = '''\
|
||||||
from resty import PathDispatcher
|
<?xml version="1.0"?>
|
||||||
from wsgiref.simple_server import make_server
|
<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>'''
|
||||||
|
|
||||||
# Create the dispatcher and register functions
|
def localtime(environ, start_response):
|
||||||
dispatcher = PathDispatcher()
|
start_response('200 OK', [ ('Content-type', 'application/xml') ])
|
||||||
dispatcher.register('GET', '/hello', hello_world)
|
resp = _localtime_resp.format(t=time.localtime())
|
||||||
dispatcher.register('GET', '/localtime', localtime)
|
yield resp.encode('utf-8')
|
||||||
|
|
||||||
# Launch a basic server
|
if __name__ == '__main__':
|
||||||
httpd = make_server('', 8080, dispatcher)
|
from resty import PathDispatcher
|
||||||
print('Serving on port 8080...')
|
from wsgiref.simple_server import make_server
|
||||||
httpd.serve_forever()
|
|
||||||
|
|
||||||
To test your server, you can interact with it using a browser or urllib. For example:
|
# Create the dispatcher and register functions
|
||||||
|
dispatcher = PathDispatcher()
|
||||||
|
dispatcher.register('GET', '/hello', hello_world)
|
||||||
|
dispatcher.register('GET', '/localtime', localtime)
|
||||||
|
|
||||||
>>> u = urlopen('http://localhost:8080/hello?name=Guido')
|
# Launch a basic server
|
||||||
>>> print(u.read().decode('utf-8'))
|
httpd = make_server('', 8080, dispatcher)
|
||||||
<html>
|
print('Serving on port 8080...')
|
||||||
<head>
|
httpd.serve_forever()
|
||||||
<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>
|
要测试下这个服务器,你可以使用一个浏览器或 ``urllib`` 和它交互。例如:
|
||||||
<month>11</month>
|
|
||||||
<day>24</day>
|
.. code-block:: python
|
||||||
<hour>14</hour>
|
|
||||||
<minute>49</minute>
|
>>> u = urlopen('http://localhost:8080/hello?name=Guido')
|
||||||
<second>17</second>
|
>>> print(u.read().decode('utf-8'))
|
||||||
</time>
|
<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
|
在编写REST接口时,通常都是服务于普通的HTTP请求。但是跟那些功能完整的网站相比,你通常只需要处理数据。
|
||||||
HTTP requests. However, unlike a full-fledged website, you’re often just pushing data
|
这些数据以各种标准格式编码,比如XML、JSON或CSV。
|
||||||
around. This data might be encoded in a variety of standard formats such as XML, JSON,
|
尽管程序看上去很简单,但是以这种方式提供的API对于很多应用程序来讲是非常有用的。
|
||||||
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. What’s 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
|
例如,长期运行的程序可能会使用一个REST API来实现监控或诊断。
|
||||||
|
大数据应用程序可以使用REST来构建一个数据查询或提取系统。
|
||||||
|
REST还能用来控制硬件设备比如机器人、传感器、工厂或灯泡。
|
||||||
|
更重要的是,REST API已经被大量客户端编程环境所支持,比如Javascript, Android, iOS等。
|
||||||
|
因此,利用这种接口可以让你开发出更加复杂的应用程序。
|
||||||
|
|
||||||
def wsgi_app(environ, start_response):
|
为了实现一个简单的REST接口,你只需让你的程序代码满足Python的WSGI标准即可。
|
||||||
...
|
WSGI被标准库支持,同时也被绝大部分第三方web框架支持。
|
||||||
|
因此,如果你的代码遵循这个标准,在后面的使用过程中就会更加的灵活!
|
||||||
|
|
||||||
The environ argument is a dictionary that contains values inspired by the CGI interface
|
在WSGI中,你可以像下面这样约定的方式以一个可调用对象形式来实现你的程序。
|
||||||
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):
|
.. code-block:: python
|
||||||
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‐
|
import cgi
|
||||||
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):
|
def wsgi_app(environ, start_response):
|
||||||
...
|
pass
|
||||||
start_response('200 OK', [('Content-type', 'text/plain')])
|
|
||||||
|
|
||||||
To return data, an WSGI application must return a sequence of byte strings. This can
|
``environ`` 属性是一个字典,包含了从web服务器如Apache[参考Internet RFC 3875]提供的CGI接口中获取的值。
|
||||||
be done using a list like this:
|
要将这些不同的值提取出来,你可以像这么这样写:
|
||||||
|
|
||||||
def wsgi_app(environ, start_response):
|
.. code-block:: python
|
||||||
...
|
|
||||||
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):
|
||||||
|
method = environ['REQUEST_METHOD']
|
||||||
|
path = environ['PATH_INFO']
|
||||||
|
# Parse the query parameters
|
||||||
|
params = cgi.FieldStorage(environ['wsgi.input'], environ=environ)
|
||||||
|
|
||||||
def wsgi_app(environ, start_response):
|
我们展示了一些常见的值。``environ['REQUEST_METHOD']`` 代表请求类型如GET、POST、HEAD等。
|
||||||
...
|
``environ['PATH_INFO']`` 表示被请求资源的路径。
|
||||||
start_response('200 OK', [('Content-type', 'text/plain')])
|
调用 ``cgi.FieldStorage()`` 可以从请求中提取查询参数并将它们放入一个类字典对象中以便后面使用。
|
||||||
yield b'Hello World\n'
|
|
||||||
yield b'Goodbye!\n'
|
|
||||||
|
|
||||||
It’s important to emphasize that byte strings must be used in the result. If the response
|
``start_response`` 参数是一个为了初始化一个请求对象而必须被调用的函数。
|
||||||
consists of text, it will need to be encoded into bytes first. Of course, there is no re‐
|
第一个参数是返回的HTTP状态值,第二个参数是一个(名,值)元组列表,用来构建返回的HTTP头。例如:
|
||||||
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:
|
.. code-block:: python
|
||||||
def __init__(self):
|
|
||||||
...
|
|
||||||
def __call__(self, environ, start_response)
|
|
||||||
...
|
|
||||||
|
|
||||||
This technique has been used to create the PathDispatcher class in the recipe. The
|
def wsgi_app(environ, start_response):
|
||||||
dispatcher does nothing more than manage a dictionary mapping (method, path) pairs
|
pass
|
||||||
to handler functions. When a request arrives, the method and path are extracted and
|
start_response('200 OK', [('Content-type', 'text/plain')])
|
||||||
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
|
为了返回数据,一个WSGI程序必须返回一个字节字符串序列。可以像下面这样使用一个列表来完成:
|
||||||
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__':
|
.. code-block:: python
|
||||||
from wsgiref.simple_server import make_server
|
|
||||||
|
|
||||||
# Create the dispatcher and register functions
|
def wsgi_app(environ, start_response):
|
||||||
dispatcher = PathDispatcher()
|
pass
|
||||||
...
|
start_response('200 OK', [('Content-type', 'text/plain')])
|
||||||
|
resp = []
|
||||||
|
resp.append(b'Hello World\n')
|
||||||
|
resp.append(b'Goodbye!\n')
|
||||||
|
return resp
|
||||||
|
|
||||||
# Launch a basic server
|
或者,你还可以使用 ``yield`` :
|
||||||
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.
|
.. code-block:: python
|
||||||
Later on, when you’re 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 doesn’t 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.
|
|
||||||
|
|
||||||
|
def wsgi_app(environ, start_response):
|
||||||
|
pass
|
||||||
|
start_response('200 OK', [('Content-type', 'text/plain')])
|
||||||
|
yield b'Hello World\n'
|
||||||
|
yield b'Goodbye!\n'
|
||||||
|
|
||||||
|
这里要强调的一点是最后返回的必须是字节字符串。如果返回结果包含文本字符串,必须先将其编码成字节。
|
||||||
|
当然,并没有要求你返回的一点是文本,你可以很轻松的编写一个生成图片的程序。
|
||||||
|
|
||||||
|
尽管WSGI程序通常被定义成一个函数,不过你也可以使用类实例来实现,只要它实现了合适的 ``__call__()`` 方法。例如:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class WSGIApplication:
|
||||||
|
def __init__(self):
|
||||||
|
...
|
||||||
|
def __call__(self, environ, start_response)
|
||||||
|
...
|
||||||
|
|
||||||
|
我们已经在上面使用这种技术创建 ``PathDispatcher`` 类。
|
||||||
|
这个分发器仅仅只是管理一个字典,将(方法,路径)对映射到处理器函数上面。
|
||||||
|
当一个请求到来时,它的方法和路径被提取出来,然后被分发到对应的处理器上面去。
|
||||||
|
另外,任何查询变量会被解析后放到一个字典中,以 ``environ['params']`` 形式存储。
|
||||||
|
后面这个步骤太常见,所以建议你在分发器里面完成,这样可以省掉很多重复代码。
|
||||||
|
使用分发器的时候,你只需简单的创建一个实例,然后通过它注册各种WSGI形式的函数。
|
||||||
|
编写这些函数应该超级简单了,只要你遵循 ``start_response()`` 函数的编写规则,并且最后返回字节字符串即可。
|
||||||
|
|
||||||
|
当编写这种函数的时候还需注意的一点就是对于字符串模板的使用。
|
||||||
|
没人愿意写那种到处混合着 ``print()`` 函数 、XML和大量格式化操作的代码。
|
||||||
|
我们上面使用了三引号包含的预先定义好的字符串模板。
|
||||||
|
这种方式的可以让我们很容易的在以后修改输出格式(只需要修改模板本身,而不用动任何使用它的地方)。
|
||||||
|
|
||||||
|
最后,使用WSGI还有一个很重要的部分就是没有什么地方是针对特定web服务器的。
|
||||||
|
因为标准对于服务器和框架是中立的,你可以将你的程序放入任何类型服务器中。
|
||||||
|
我们使用下面的代码测试测试本节代码:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from wsgiref.simple_server import make_server
|
||||||
|
|
||||||
|
# Create the dispatcher and register functions
|
||||||
|
dispatcher = PathDispatcher()
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Launch a basic server
|
||||||
|
httpd = make_server('', 8080, dispatcher)
|
||||||
|
print('Serving on port 8080...')
|
||||||
|
httpd.serve_forever()
|
||||||
|
|
||||||
|
上面代码创建了一个简单的服务器,然后你就可以来测试下你的实现是否能正常工作。
|
||||||
|
最后,当你准备进一步扩展你的程序的时候,你可以修改这个代码,让它可以为特定服务器工作。
|
||||||
|
|
||||||
|
WSGI本身是一个很小的标准。因此它并没有提供一些高级的特性比如认证、cookies、重定向等。
|
||||||
|
这些你自己实现起来也不难。不过如果你想要更多的支持,可以考虑第三方库,比如 ``WebOb`` 或者 ``Paste`` 。
|
||||||
|
|||||||
Reference in New Issue
Block a user