通過demo學習OpenStack開發所需的基礎知識 — API服務(3)

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

上一篇文章我們瞭解了一個巨囉嗦的框架:Paste PasteDeploy Routes WebOb。後來OpenStack社群的人受不了這麼囉嗦的程式碼了,決定換一個框架,他們最終選中了Pecan。Pecan框架相比上一篇文章的囉嗦框架有如下好處:

不用自己寫WSGI application了

請求路由很容易就可以實現了

總的來說,用上Pecan框架以後,很多重複的程式碼不用寫了,開發人員可以專注於業務,也就是實現每個API的功能。

Pecan

Pecan框架的目標是實現一個採用物件分發方式進行URL路由的輕量級Web框架。它非常專注於自己的目標,它的大部分功能都和URL路由以及請求和響應的處理相關,而不去實現模板、安全以及資料庫層,這些東西都可以通過其他的庫來實現。關於Pecan的更多資訊,可以檢視文件:https://pecan.readthedocs.org/en/latest/index.html。本文以OpenStack的magnum專案為例來說明Pecan專案在實際中的應用,但是本文不會詳細講解Pecan的各個方面,一些細節請讀者閱讀Pecan的文件。

專案中的程式碼結構

使用Pecan框架時,OpenStack專案一般會把API服務的實現都放在一個api目錄下,比如magnum專案是這樣的:

➜ ~/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專案時就會比較容易找到入口。

app.py 一般包含了Pecan應用的入口,包含應用初始化程式碼

config.py 包含Pecan的應用配置,會被app.py使用

controllers/ 這個目錄會包含所有的控制器,也就是API具體邏輯的地方

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

controllers/v1/ 這個目錄對應v1版本的API的控制器。如果有多個版本的API,你一般能看到v2等目錄。

程式碼變少了:application的配置

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()

別看這個類這麼長,我來解釋一下你就懂了。首先,你可以先忽略掉_route()

首先,你要記住,這個RootController對應的是URL中根路徑,也就是path中最左邊的/

RootController繼承自rest.RestController,是Pecan實現的RESTful控制器。這裡的get()

上面這個Controller也是繼承自rest.RestController。所以它的get函式表示,當訪問的是GET /v1的時候,要做的處理。然後,它還有很多類屬性,這些屬性分別表示不同URL路徑的控制器:

/v1/bays 由bays處理

/v1/baymodels 由baymodels處理

/v1/containers 由containers處理

其他的都是類似的。我們再繼續看bay.BaysController

這個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的路由還是要完整的看一下官方文件。

內建RESTful支援

我們上面舉例的controller都是繼承自pecan.rest.RestController

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

 $ curl http://localhost:8080/test
hello% 

那麼HTTP請求和HTTP響應呢?

上面講了這麼多,我們都沒有說明在Pecan中如何處理請求和如何返回響應。這個將在下一章中說明,同時我們會引入一個新的庫WSME

WSME

Pecan對請求和響應的處理

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

Pecan框架為每個執行緒維護了單獨的請求和響應物件,你可以直接在請求處理函式中訪問。pecan.requestpecan.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

另外,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

返回JSON還是HTML?

如果你不是明確的返回一個Response物件,那麼Pecan中方法的返回內容型別就是由expose()

效果如下:

 $ 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()

template引數用來指定返回值的模板,如果是’json’就會返回JSON內容,這裡可以指定一個HTML檔案,或者指定一個mako模板。

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

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

看過引數的解釋後,你應該能大概瞭解expose()

如果不提供引數,訪問會失敗:

$ 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,並且用程式碼來回答上面所有的問題。

相關文章

程式語言 最新文章