天天看點

來杯咖啡看Pecan

Pecan的介紹及安裝

文章内容來自官方文檔:http://pecan.readthedocs.io/en/latest/quick_start.html

Pecan的介紹:

   Pecan是一個路由對象分發的oython web架構。本質上可以将url通過分割為每一部分,然後對每一部分查找對應處理該URL部分的處理類,處理後,繼續交給後面部分的URL處理,直到所有URL部分都被處理後,調用最後分割的URL對應的處理函數處理。

本文以Xshall為主在其進行操作

Pecan的安裝

來杯咖啡看Pecan

建立項目

來杯咖啡看Pecan

項目建立好之後目錄結構如下

來杯咖啡看Pecan
來杯咖啡看Pecan

app.py:一般包含了Pecan應用的入口,包含初始化代碼。

Config.py : 包含Pecan的應用配置,會被 app.py使用。

Controllersl : 這個目錄包含所有的控制器,也就是API具體邏輯的地方 。

Cotrollers/root.py : 這個包含根路徑對應的控制器。

Controllers/v1/ 這個目錄放的是版本的API的。

Public : 檔案夾存放一些Web應用所需的Image,Css或者JavaScript。

setup.py和setup.cfg用于Web應用的安裝部署。

templates:存儲Html或者Json的末班檔案。

tests:存放測試用例。

代碼變少了:application的配置

Pecan的配置很容易,通過 一個python的源碼式的配置檔案就可以完成基本的配置,這個配置的主要目的是指定應用程式的root,然後用于生成WSGI application。我們來看Magnum項目的列子,Magnum項目有個API服務是 用Pecan實作的,在magnum/api/config.py檔案中可以找到這個檔案,主要内容如下:

1 app = {
 2     'root': 'magnum.api.controllers.root.RootController',
 3     'modules': ['magnum.api'],
 4     'debug': False,
 5     'hooks': [
 6         hooks.ContextHook(),
 7         hooks.RPCHook(),
 8         hooks.NoExceptionTracebackHook(),
 9     ],
10     'acl_public_routes': [
11         '/'
12     ],
13 }      

上面這個app對象就是Pecan的配置,每個Pecan應用都需要有這麼一 個名為app的配置。app配置中最重要的就是root的值,這個值表示了應用程式的入口,也就是從哪個地方開始解析HTTP的根path:/。 hooks對應的配置是一些pecan的hook,作用類似于WSGI Middleware。有了app配置後,就可以讓Pecan生成一個WSGI application。在Magnum項目中,magnum/api/app.py檔案就是生成WSGI application的地方,我們來看一下這個主要的内容:

1 def get_pecan_config():
 2     # Set up the pecan configuration
 3     filename = api_config.__file__.replace('.pyc', '.py')
 4     return pecan.configuration.conf_from_file(filename)
 5 
 6 
 7 def setup_app(config=None):
 8     if not config:
 9         config = get_pecan_config()
10 
11     app_conf = dict(config.app)
12 
13     app = pecan.make_app(
14         app_conf.pop('root'),
15         logging=getattr(config, 'logging', {}),
16         wrap_app=middleware.ParsableErrorMiddleware,
17         **app_conf
18     )
19 
20     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簡介這篇文章(https://segmentfault.com/a/1190000003069785)從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的是一個類,我們來看代碼:

1 class RootController(rest.RestController):
 2 
 3     _versions = ['v1']
 4     """All supported API versions"""
 5 
 6     _default_version = 'v1'
 7     """The default API version"""
 8 
 9     v1 = v1.Controller()
10 
11     @expose.expose(Root)
12     def get(self):
13         # NOTE: The reason why convert() it's being called for every
14         #       request is because we need to get the host url from
15         #       the request object to make the links.
16         return Root.convert()
17 
18     @pecan.expose()
19     def _route(self, args):
20         """Overrides the default routing behavior.
21 
22         It redirects the request to the default version of the magnum API
23         if the version number is not specified in the url.
24         """
25 
26         if args[0] and args[0] not in self._versions:
27             args = [self._default_version] + args
28         return super(RootController, self)._route(args)      

别看這個類這麼長,我來解釋下你就懂了,首先你可以忽略掉_route()函數,這個函數使用來覆寫Pecan的預設路由實作的,在這裡去掉它不妨礙我們了解Pecan(這裡的_route()函數的作用把所有請求重定向到預設的API版本去),去掉_route()和其他的東西後,整個類就是變成這麼短:

1 class RootController(rest.RestController):
2     v1 = v1.Controller()
3 
4     @expose.expose(Root)
5     def get(self):
6         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的實作:

1 class Controller(rest.RestController):
 2     """Version 1 API controller root."""
 3 
 4     bays = bay.BaysController()
 5     baymodels = baymodel.BayModelsController()
 6     containers = container.ContainersController()
 7     nodes = node.NodesController()
 8     pods = pod.PodsController()
 9     rcs = rc.ReplicationControllersController()
10     services = service.ServicesController()
11     x509keypairs = x509keypair.X509KeyPairController()
12     certificates = certificate.CertificateController()
13 
14     @expose.expose(V1)
15     def get(self):
16         return V1.convert()
17 
18     ...      

上面這個Controoler也是繼承自restRestController。是以它的get函數表示,當通路的是GET/v1的時候,要做的處理。然後它還有很多類屬性,這些屬性分别表示不同的URL路徑的控制器:

1、/vq/bays   由bays處理

2、/v1baymodels  由baymodels處理

3、/v1/containers  由containers處理

其他的都是類似的。我們在繼續看bay.B安陽市Controller的代碼:

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中終結,這個類會處理如下請求:

1、GET /v1/bays

2、GET /v1/bays/{UUID}

3、POST /v1/bays

4、PATCH /v1/bays/{UUID}

5、DELETE /v1/bays/{UUID}

6、GET / v1/bays/detail/{UUID}

看到上面的3個controller之後,你應該能大概明白Pecan是如何對URL進行路由的,這種路由方式就是對象分發:(根據類屬性)、(包括資料屬性)和方法屬性來決定如何路由一個HTTP請求,Pecan的文檔中請求額路由有專門的描述,要想掌握Pecan的路由還是要完整的看一下官方文檔。

内置RESTful支援

我們上面舉例的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的差別 ,可以看官方文檔,有很清楚的示例。

對于RestController中沒有預先定義好的方法,我們可以通過控制器的_custom_actions屬性來指定其能處理的方法。

1 class RootController(rest.RestController):
2     _custom_actions = {
3         'test': ['GET'],
4     }
5 
6     @expose()
7     def test(self):
8         return 'hello'      

上面這個控制器是一個根控制器,指定了/test路徑支援GET方法,效果如下:

$ curl http://localhost:8080/test      

hello%

那麼HTTP請求和HTTP響應呢?

wsme

Pecan對請求和響應的處理

在開始提到WSME之前,我們吸納來看下Pecan自己對HTTP請求和響應的處理。這樣你能更好的了解為什麼會引入WSME庫。

Pecan架構為每個線程維護了單獨的請求和響應的對象,你可以直接在處理函數中通路。

pecan.requesr和pecan.response分别代表目前需要處理的請求和響應對象,你可以直接操作這兩個對象,比如指定響應的狀态碼,就像下面這個列子一樣:

1 @pecan.expose()
2 def login(self):
3     assert pecan.request.path == '/login'
4     username = pecan.request.POST.get('username')
5     password = pecan.request.POST.get('password')
6 
7     pecan.response.status = 403
8     pecan.response.text = 'Bad Login!'      

這個列子示範了通路POST請求的參數以及傳回403,你也可以重新構造一個pecan.Response對象作為傳回值:

1 from pecan import expose, Response
2 
3 class RootController(object):
4 
5     @expose()
6     def hello(self):
7         return Response('Hello, World!', 202)      

另外,HTTP請求參數的參數也會可以作為控制器方法的參數,還是來看幾個官方文檔的列子:

1 class RootController(object):
2     @expose()
3     def index(self, arg):
4         return arg
5 
6     @expose()
7     def kwargs(self, **kwargs):
8         return str(kwargs)      

這個控制器中的方法直接傳回了參數,示範了對GET請求參數的處理,效果是這樣的:

1 $ curl http://localhost:8080/?arg=foo
2 foo
3 $ curl http://localhost:8080/kwargs?a=1&b=2&c=3
4 {u'a': u'1', u'c': u'3', u'b': u'2'}      

有時候,參數也可能是URL的一部分,比如最後一段path作為參數,就像下面這樣:

1 class RootController(object):
2     @expose()
3     def args(self, *args):
4         return ','.join(args)      

效果是這樣的:

1 $ curl http://localhost:8080/args/one/two/three
2 one,two,three      

另外,我們還要看一下POST方法的參數 如何處理:

1 class RootController(object):
2     @expose()
3     def index(self, arg):
4         return arg      

效果如下,就是把HTTP body解析成了控制器方法的參數:

1 $ curl -X POST "http://localhost:8080/" -H "Content-Type: 
2 application/x-www-form-urlencoded" -d "arg=foo" foo      

傳回JSON還是HTML?

如果你不是明确的傳回一個Response對象,那麼Pecan中方法的傳回内容類型就是由expose()裝飾器決定的,預設情況下,控制器的方法傳回的content-type是HTML。

1 class RootController(rest.RestController):
2     _custom_actions = {
3         'test': ['GET'],
4     }
5 
6     @expose()
7     def test(self):
8         return 'hello'      

效果如下:

1 $ curl -v http://localhost:8080/test
 2 * Hostname was NOT found in DNS cache
 3 *   Trying 127.0.0.1...
 4 * Connected to localhost (127.0.0.1) port 8080 (#0)
 5 > GET /test HTTP/1.1
 6 > User-Agent: curl/7.38.0
 7 > Host: localhost:8080
 8 > Accept: */*
 9 >
10 * HTTP 1.0, assume close after body
11 < HTTP/1.0 200 OK
12 < Date: Tue, 15 Sep 2015 14:31:28 GMT
13 < Server: WSGIServer/0.1 Python/2.7.9
14 < Content-Length: 5
15 < Content-Type: text/html; charset=UTF-8
16 <
17 * Closing connection 0
18 hello%       

也可以讓他傳回JSON:

1 class RootController(rest.RestController):
2     _custom_actions = {
3         'test': ['GET'],
4     }
5 
6     @expose('json')
7     def test(self):
8         return 'hello'      
1 curl -v http://localhost:8080/test
 2 * Hostname was NOT found in DNS cache
 3 *   Trying 127.0.0.1...
 4 * Connected to localhost (127.0.0.1) port 8080 (#0)
 5 > GET /test HTTP/1.1
 6 > User-Agent: curl/7.38.0
 7 > Host: localhost:8080
 8 > Accept: */*
 9 >
10 * HTTP 1.0, assume close after body
11 < HTTP/1.0 200 OK
12 < Date: Tue, 15 Sep 2015 14:33:27 GMT
13 < Server: WSGIServer/0.1 Python/2.7.9
14 < Content-Length: 18
15 < Content-Type: application/json; charset=UTF-8
16 <
17 * Closing connection 0
18 {"hello": "world"}%       

甚至,你可以讓一個控制器方法根據URL path的來決定是傳回HTML還是JSON:

1 class RootController(rest.RestController):
2     _custom_actions = {
3         'test': ['GET'],
4     }
5 
6     @expose()
7     @expose('json')
8     def test(self):
9         return json.dumps({'hello': 'world'})      

傳回JSON:

1 $ curl -v http://localhost:8080/test.json
 2 * Hostname was NOT found in DNS cache
 3 *   Trying 127.0.0.1...
 4 * Connected to localhost (127.0.0.1) port 8080 (#0)
 5 > GET /test.json HTTP/1.1
 6 > User-Agent: curl/7.38.0
 7 > Host: localhost:8080
 8 > Accept: */*
 9 >
10 * HTTP 1.0, assume close after body
11 < HTTP/1.0 200 OK
12 < Date: Wed, 16 Sep 2015 14:26:27 GMT
13 < Server: WSGIServer/0.1 Python/2.7.9
14 < Content-Length: 24
15 < Content-Type: application/json; charset=UTF-8
16 <
17 * Closing connection 0
18 "{\"hello\": \"world\"}"%       

傳回HTML:

1 $ curl -v http://localhost:8080/test.html
 2 * Hostname was NOT found in DNS cache
 3 *   Trying 127.0.0.1...
 4 * Connected to localhost (127.0.0.1) port 8080 (#0)
 5 > GET /test.html HTTP/1.1
 6 > User-Agent: curl/7.38.0
 7 > Host: localhost:8080
 8 > Accept: */*
 9 >
10 * HTTP 1.0, assume close after body
11 < HTTP/1.0 200 OK
12 < Date: Wed, 16 Sep 2015 14:26:24 GMT
13 < Server: WSGIServer/0.1 Python/2.7.9
14 < Content-Length: 18
15 < Content-Type: text/html; charset=UTF-8
16 <
17 * Closing connection 0
18 {"hello": "world"}%       

這裡要注意一下;

1、同一個字元串作為JSON傳回和作為HTML傳回是不一樣的,仔細看一下HTTP響應的内容。

2、我們的列子中在URL的最後加上了.html字尾或者.json字尾,請嘗試一下不加字尾的變化是傳回什麼?然後,調換一下兩個expose()的順序再試一下。

從上面的列子可以看出,決定響應類型的主要是傳遞給expose()函數的參數,我們看下expose()函數的完整聲明:

1 pecan.decorators.expose(template=None,
2                         content_type='text/html',
3                         generic=False)      

template參數用來指定傳回值得末班,如果是json就會傳回json内容,這裡可以指定一個

 HTML檔案,或者指定一個mako模闆。

content_type指定響應的content-type,預設值是"text/html"

generic參數表明該方法是一個"泛型"方法,可以指定多個不同的函數對應同一個路徑的不同的HTTP方法。

看過參數的解釋後,你應該能大概了解expose()函數是如何控制HTTP響應的内容和類型的。