上一篇文章咱們瞭解了一個巨囉嗦的框架:Paste + PasteDeploy + Routes + WebOb。後來OpenStack社區的人受不了這麼囉嗦的代碼了,決定換一個框架,他們最終選中了Pecan。Pecan框架相比上一篇文章的囉嗦框架有以下好處:html
不用本身寫WSGI application了node
請求路由很容易就能夠實現了git
總的來講,用上Pecan框架之後,不少重複的代碼不用寫了,開發人員能夠專一於業務,也就是實現每一個API的功能。數據庫
Pecan框架的目標是實現一個採用對象分發方式進行URL路由的輕量級Web框架。它很是專一於本身的目標,它的大部分功能都和URL路由以及請求和響應的處理相關,而不去實現模板、安全以及數據庫層,這些東西均可以經過其餘的庫來實現。關於Pecan的更多信息,能夠查看文檔:https://pecan.readthedocs.org/en/latest/index.html。本文以OpenStack的magnum項目爲例來講明Pecan項目在實際中的應用,可是本文不會詳細講解Pecan的各個方面,一些細節請讀者閱讀Pecan的文檔。json
使用Pecan框架時,OpenStack項目通常會把API服務的實現都放在一個api目錄下,好比magnum項目是這樣的:segmentfault
➜ ~/openstack/env/p/magnum git:(master) $ tree magnum/api magnum/api ├── app.py ├── auth.py ├── config.py ├── controllers │ ├── base.py │ ├── __init__.py │ ├── link.py │ ├── root.py │ └── v1 │ ├── base.py │ ├── baymodel.py │ ├── bay.py │ ├── certificate.py │ ├── collection.py │ ├── container.py │ ├── __init__.py │ ├── magnum_services.py │ ├── node.py │ ├── pod.py │ ├── replicationcontroller.py │ ├── service.py │ ├── types.py │ ├── utils.py │ └── x509keypair.py ├── expose.py ├── hooks.py ├── __init__.py ├── middleware │ ├── auth_token.py │ ├── __init__.py │ └── parsable_error.py ├── servicegroup.py └── validation.py
你也能夠在Ceilometer項目中看到相似的結構。介紹一下幾個主要的文件,這樣你之後看到一個使用Pecan的OpenStack項目時就會比較容易找到入口。api
app.py 通常包含了Pecan應用的入口,包含應用初始化代碼數組
config.py 包含Pecan的應用配置,會被app.py使用安全
controllers/ 這個目錄會包含全部的控制器,也就是API具體邏輯的地方app
controllers/root.py 這個包含根路徑對應的控制器
controllers/v1/ 這個目錄對應v1版本的API的控制器。若是有多個版本的API,你通常能看到v2等目錄。
Pecan的配置很容易,經過一個Python源碼式的配置文件就能夠完成基本的配置。這個配置的主要目的是指定應用程序的root,而後用於生成WSGI application。咱們來看Magnum項目的例子。Magnum項目有個API服務是用Pecan實現的,在magnum/api/config.py文件中能夠找到這個文件,主要內容以下:
app = { 'root': 'magnum.api.controllers.root.RootController', 'modules': ['magnum.api'], 'debug': False, 'hooks': [ hooks.ContextHook(), hooks.RPCHook(), hooks.NoExceptionTracebackHook(), ], 'acl_public_routes': [ '/' ], }
上面這個app對象就是Pecan的配置,每一個Pecan應用都須要有這麼一個名爲app的配置。app配置中最主要的就是root的值,這個值表示了應用程序的入口,也就是從哪一個地方開始解析HTTP的根path:/。hooks對應的配置是一些Pecan的hook,做用相似於WSGI Middleware。
有了app配置後,就可讓Pecan生成一個WSGI application。在Magnum項目中,magnum/api/app.py文件就是生成WSGI application的地方,咱們來看一下這個的主要內容:
def get_pecan_config(): # Set up the pecan configuration filename = api_config.__file__.replace('.pyc', '.py') return pecan.configuration.conf_from_file(filename) def setup_app(config=None): if not config: config = get_pecan_config() app_conf = dict(config.app) app = pecan.make_app( app_conf.pop('root'), logging=getattr(config, 'logging', {}), wrap_app=middleware.ParsableErrorMiddleware, **app_conf ) return auth.install(app, CONF, config.app.acl_public_routes)
get_pecan_config()
方法讀取咱們上面提到的config.py文件,而後返回一個pecan.configuration.Config對象。setup_app()
函數首先調用get_pecan_config()
函數獲取application的配置,而後調用pecan.make_app()
函數建立了一個WSGI application,最後調用了 auth.install()
函數(也就是magnum.api.auth.install()函數)爲剛剛生成的WSGI application加上Keystone的認證中間件(確保全部的請求都會經過Keystone認證)。
到這邊爲止,一個Pecan的WSGI application就已經準備好了,只要調用這個setup_app()
函數就能得到。至於如何部署這個WSGI application,請參考WSGI簡介這篇文章。
從Magnum這個實際的例子能夠看出,使用了Pecan以後,咱們再也不須要本身寫那些冗餘的WSGI application代碼了,直接調用Pecan的make_app()
函數就能完成這些工做。另外,對於以前使用PasteDeploy時用到的不少WSGI中間件,能夠選擇使用Pecan的hooks機制來實現,也選擇使用WSGI中間件的方式來實現。在Magnum的API服務就同時使用了這兩種方式。其實,Pecan還能夠和PasteDeploy一塊兒使用,Ceilometer項目就是這麼作的,你們能夠看看。
Pecan不只縮減了生成WSGI application的代碼,並且也讓開發人員更容易的指定一個application的路由。Pecan採用了一種對象分發風格(object-dispatch style)的路由模式。咱們直接經過例子來解釋這種路由模式,仍是以Magnum項目爲例。
上面提到了,Magnum的API服務的root是magnum.api.controllers.root.RootController。這裏的RootController的是一個類,咱們來看它的代碼:
class RootController(rest.RestController): _versions = ['v1'] """All supported API versions""" _default_version = 'v1' """The default API version""" v1 = v1.Controller() @expose.expose(Root) def get(self): # NOTE: The reason why convert() it's being called for every # request is because we need to get the host url from # the request object to make the links. return Root.convert() @pecan.expose() def _route(self, args): """Overrides the default routing behavior. It redirects the request to the default version of the magnum API if the version number is not specified in the url. """ if args[0] and args[0] not in self._versions: args = [self._default_version] + args return super(RootController, self)._route(args)
別看這個類這麼長,我來解釋一下你就懂了。首先,你能夠先忽略掉_route()
函數,這個函數是用來覆蓋Pecan的默認路由實現的,在這裏去掉它不妨礙咱們理解Pecan(這裏的_route()
函數的做用把全部請求重定向到默認的API版本去)。去掉_route()
和其餘的東西后,整個類就變成這麼短:
class RootController(rest.RestController): v1 = v1.Controller() @expose.expose(Root) def get(self): return Root.convert()
首先,你要記住,這個RootController對應的是URL中根路徑,也就是path中最左邊的/。
RootController繼承自rest.RestController,是Pecan實現的RESTful控制器。這裏的get()
函數表示,當訪問的是GET /時,由該函數處理。get()
函數會返回一個WSME對象,表示一個形式化的HTTP Response,這個下面再講。get()
函數上面的expose
裝飾器是Pecan實現路由控制的一個方式,被expose的函數纔會被路由處理。
這裏的v1 = v1.Controller()
表示,當訪問的是GET /v1或者GET /v1/...時,請求由一個v1.Controller
實例來處理。
爲了加深你們的理解,咱們再來看下v1.Controller
的實現:
class Controller(rest.RestController): """Version 1 API controller root.""" bays = bay.BaysController() baymodels = baymodel.BayModelsController() containers = container.ContainersController() nodes = node.NodesController() pods = pod.PodsController() rcs = rc.ReplicationControllersController() services = service.ServicesController() x509keypairs = x509keypair.X509KeyPairController() certificates = certificate.CertificateController() @expose.expose(V1) def get(self): return V1.convert() ...
上面這個Controller也是繼承自rest.RestController。因此它的get函數表示,當訪問的是GET /v1的時候,要作的處理。而後,它還有不少類屬性,這些屬性分別表示不一樣URL路徑的控制器:
/v1/bays 由bays處理
/v1/baymodels 由baymodels處理
/v1/containers 由containers處理
其餘的都是相似的。咱們再繼續看bay.BaysController
的代碼:
class BaysController(rest.RestController): """REST controller for Bays.""" def __init__(self): super(BaysController, self).__init__() _custom_actions = { 'detail': ['GET'], } def get_all(...): def detail(...): def get_one(...): def post(...): def patch(...): def delete(...):
這個controller中只有函數,沒有任何類屬性,並且沒有實現任何特殊方法,因此/v1/bays開頭的URL處理都在這個controller中終結。這個類會處理以下請求:
GET /v1/bays
GET /v1/bays/{UUID}
POST /v1/bays
PATCH /v1/bays/{UUID}
DELETE /v1/bays/{UUID}
GET /v1/bays/detail/{UUID}
看了上面的3個controller以後,你應該能大概明白Pecan是如何對URL進行路由的。這種路由方式就是對象分發:根據類屬性,包括數據屬性和方法屬性來決定如何路由一個HTTP請求。Pecan的文檔中對請求的路由有專門的描述,要想掌握Pecan的路由仍是要完整的看一下官方文檔。
咱們上面舉例的controller都是繼承自pecan.rest.RestController
,這種controller稱爲RESTful controller,專門用於實現RESTful API的,所以在OpenStack中使用特別多。Pecan還支持普通的controller,稱爲Generic controller。Generic controller繼承自object對象,默認沒有實現對RESTful請求的方法。簡單的說,RESTful controller幫咱們規定好了get_one()
, get_all()
, get()
, post()
等方法對應的HTTP請求,而Generic controller則沒有。關於這兩種controller的區別,能夠看官方文檔Writing RESTful Web Services with Generic Controllers,有很清楚的示例。
對於RestController中沒有預先定義好的方法,咱們能夠經過控制器的_custom_actions
屬性來指定其能處理的方法。
class RootController(rest.RestController): _custom_actions = { 'test': ['GET'], } @expose() def test(self): return 'hello'
上面這個控制器是一個根控制器,指定了/test路徑支持GET方法,效果以下:
$ curl http://localhost:8080/test hello%
上面講了這麼多,咱們都沒有說明在Pecan中如何處理請求和如何返回響應。這個將在下一章中說明,同時咱們會引入一個新的庫WSME。
在開始提到WSME以前,咱們先來看下Pecan本身對HTTP請求和響應的處理。這樣你能更好的理解爲何會再引入一個WSME庫。
Pecan框架爲每一個線程維護了單獨的請求和響應對象,你能夠直接在請求處理函數中訪問。pecan.request和pecan.response分別表明當前須要處理的請求和響應對象。你能夠直接操做這兩個對象,好比指定響應的狀態碼,就像下面這個例子同樣(例子來自官方文檔):
@pecan.expose() def login(self): assert pecan.request.path == '/login' username = pecan.request.POST.get('username') password = pecan.request.POST.get('password') pecan.response.status = 403 pecan.response.text = 'Bad Login!'
這個例子演示了訪問POST請求的參數以及返回403。你也能夠從新構造一個pecan.Response
對象做爲返回值(例子來自官方文檔):
from pecan import expose, Response class RootController(object): @expose() def hello(self): return Response('Hello, World!', 202)
另外,HTTP請求的參數也會能夠做爲控制器方法的參數,仍是來看幾個官方文檔的例子:
class RootController(object): @expose() def index(self, arg): return arg @expose() def kwargs(self, **kwargs): return str(kwargs)
這個控制器中的方法直接返回了參數,演示了對GET請求參數的處理,效果是這樣的:
$ curl http://localhost:8080/?arg=foo foo $ curl http://localhost:8080/kwargs?a=1&b=2&c=3 {u'a': u'1', u'c': u'3', u'b': u'2'}
有時候,參數也多是URL的一部分,好比最後的一段path做爲參數,就像下面這樣:
class RootController(object): @expose() def args(self, *args): return ','.join(args)
效果是這樣的:
$ curl http://localhost:8080/args/one/two/three one,two,three
另外,咱們還要看一下POST方法的參數如何處理(例子來自官方文檔):
class RootController(object): @expose() def index(self, arg): return arg
效果以下,就是把HTTP body解析成了控制器方法的參數:
$ curl -X POST "http://localhost:8080/" -H "Content-Type: application/x-www-form-urlencoded" -d "arg=foo" foo
若是你不是明確的返回一個Response對象,那麼Pecan中方法的返回內容類型就是由expose()
裝飾器決定的。默認狀況下,控制器的方法返回的content-type是HTML。
class RootController(rest.RestController): _custom_actions = { 'test': ['GET'], } @expose() def test(self): return 'hello'
效果以下:
$ curl -v http://localhost:8080/test * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8080 (#0) > GET /test HTTP/1.1 > User-Agent: curl/7.38.0 > Host: localhost:8080 > Accept: */* > * HTTP 1.0, assume close after body < HTTP/1.0 200 OK < Date: Tue, 15 Sep 2015 14:31:28 GMT < Server: WSGIServer/0.1 Python/2.7.9 < Content-Length: 5 < Content-Type: text/html; charset=UTF-8 < * Closing connection 0 hello%
也可讓它返回JSON:
class RootController(rest.RestController): _custom_actions = { 'test': ['GET'], } @expose('json') def test(self): return 'hello'
效果以下:
curl -v http://localhost:8080/test * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8080 (#0) > GET /test HTTP/1.1 > User-Agent: curl/7.38.0 > Host: localhost:8080 > Accept: */* > * HTTP 1.0, assume close after body < HTTP/1.0 200 OK < Date: Tue, 15 Sep 2015 14:33:27 GMT < Server: WSGIServer/0.1 Python/2.7.9 < Content-Length: 18 < Content-Type: application/json; charset=UTF-8 < * Closing connection 0 {"hello": "world"}%
甚至,你還可讓一個控制器方法根據URL path的來決定是返回HTML仍是JSON:
class RootController(rest.RestController): _custom_actions = { 'test': ['GET'], } @expose() @expose('json') def test(self): return json.dumps({'hello': 'world'})
返回JSON:
$ curl -v http://localhost:8080/test.json * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8080 (#0) > GET /test.json HTTP/1.1 > User-Agent: curl/7.38.0 > Host: localhost:8080 > Accept: */* > * HTTP 1.0, assume close after body < HTTP/1.0 200 OK < Date: Wed, 16 Sep 2015 14:26:27 GMT < Server: WSGIServer/0.1 Python/2.7.9 < Content-Length: 24 < Content-Type: application/json; charset=UTF-8 < * Closing connection 0 "{\"hello\": \"world\"}"%
返回HTML:
$ curl -v http://localhost:8080/test.html * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8080 (#0) > GET /test.html HTTP/1.1 > User-Agent: curl/7.38.0 > Host: localhost:8080 > Accept: */* > * HTTP 1.0, assume close after body < HTTP/1.0 200 OK < Date: Wed, 16 Sep 2015 14:26:24 GMT < Server: WSGIServer/0.1 Python/2.7.9 < Content-Length: 18 < Content-Type: text/html; charset=UTF-8 < * Closing connection 0 {"hello": "world"}%
這裏要注意一下:
同一個字符串做爲JSON返回和做爲HTML返回是不同的,仔細看一下HTTP響應的內容。
咱們的例子中在URL的最後加上了.html後綴或者.json後綴,請嘗試一下不加後綴的化是返回什麼?而後,調換一下兩個expose()
的順序再試一下。
從上面的例子能夠看出,決定響應類型的主要是傳遞給expose()
函數的參數,咱們看下expose()
函數的完整聲明:
pecan.decorators.expose(template=None, content_type='text/html', generic=False)
template參數用來指定返回值的模板,若是是'json'就會返回JSON內容,這裏能夠指定一個HTML文件,或者指定一個mako模板。
content_type指定響應的content-type,默認值是'text/html'。
generic參數代表該方法是一個「泛型」方法,能夠指定多個不一樣的函數對應同一個路徑的不一樣的HTTP方法。
看過參數的解釋後,你應該能大概瞭解expose()
函數是如何控制HTTP響應的內容和類型的。
上面兩節已經說明了Pecan能夠比較好的處理HTTP請求中的參數以及控制HTTP返回值。那麼爲何咱們還須要WSME呢?由於Pecan在作下面這個事情的時候比較麻煩:請求參數和響應內容的類型檢查(英文簡稱就是typing)。固然,作是能夠作的,不過你須要本身訪問pecan.request和pecan.response,而後檢查指定的值的類型。WSME就是爲解決這個問題而生的,並且適用場景就是RESTful API。
WSME的全稱是Web Service Made Easy,是專門用於實現REST服務的typing庫,讓你不須要直接操做請求和響應,並且恰好和Pecan結合得很是好,因此OpenStack的不少項目都使用了Pecan + WSME的組合來實現API(好吧,我看過的項目,用了Pecan的都用了WSME)。WSME的理念是:在大部分狀況下,Web服務的輸入和輸出對數據類型的要求都是嚴格的。因此它就專門解決了這個事情,而後把其餘事情都交給其餘框架去實現。所以,通常WSME都是和其餘框架配合使用的,支持Pecan、Flask等。WSME的文檔地址是http://wsme.readthedocs.org/en/latest/index.html。
用了WSME後的好處是什麼呢?WSME會自動幫你檢查HTTP請求和響應中的數據是否符合預先設定好的要求。WSME的主要方式是經過裝飾器來控制controller方法的輸入和輸出。WSME中主要使用兩個控制器:
@signature: 這個裝飾器用來描述一個函數的輸入和輸出。
@wsexpose: 這個裝飾器包含@signature的功能,同時會把函數的路由信息暴露給Web框架,效果就像Pecan的expose裝飾器。
這裏咱們結合Pecan來說解WSME的使用。先來看一個原始類型的例子:
from wsmeext.pecan import wsexpose class RootController(rest.RestController): _custom_actions = { 'test': ['GET'], } @wsexpose(int, int) def test(self, number): return number
若是不提供參數,訪問會失敗:
$ curl http://localhost:8080/test {"debuginfo": null, "faultcode": "Client", "faultstring": "Missing argument: \"number\""}%
若是提供的參數不是整型,訪問也會失敗:
$ curl http://localhost:8080/test\?number\=a {"debuginfo": null, "faultcode": "Client", "faultstring": "Invalid input for field/attribute number. Value: 'a'. unable to convert to int"}%
上面這些錯誤信息都是由WSME框架直接返回的,尚未執行到你寫的方法。若是請求正確,那麼會是這樣的:
$ curl -v http://localhost:8080/test\?number\=1 * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8080 (#0) > GET /test?number=1 HTTP/1.1 > User-Agent: curl/7.38.0 > Host: localhost:8080 > Accept: */* > * HTTP 1.0, assume close after body < HTTP/1.0 200 OK < Date: Wed, 16 Sep 2015 15:06:35 GMT < Server: WSGIServer/0.1 Python/2.7.9 < Content-Length: 1 < Content-Type: application/json; charset=UTF-8 < * Closing connection 0 1%
請注意返回的content-type,這裏返回JSON是由於咱們使用的wsexpose
設置的返回類型是XML和JSON,而且JSON是默認值。上面這個例子就是WSME最簡單的應用了。
那麼如今有下面這些問題須要思考一下:
若是想用POST的方式來傳遞參數,要怎麼作呢?提示:要閱讀WSME中@signature裝飾器的文檔。
若是我但願使用/test/1這種方式來傳遞參數要怎麼作呢?提示:要閱讀Pecan文檔中關於路由的部分。
WSME中支持對哪些類型的檢查呢?WSME支持整型、浮點型、字符串、布爾型、日期時間等,甚至還支持用戶自定義類型。提示:要閱讀WSME文檔中關於類型的部分。
WSME支持數組類型麼?支持。
上面的問題其實也是不少人使用WSME的時候常常問的問題。咱們將在下一篇文章中使用Pecan + WSME來繼續開發咱們的demo,而且用代碼來回答上面全部的問題。