上一篇文章說到,咱們將以實例的形式來繼續講述這個API服務的開發知識,這裏會使用Pecan和WSME兩個庫。html
要開發REST API服務,咱們首先須要設計一下這個服務。設計包括要實現的功能,以及接口的具體規範。咱們這裏要實現的是一個簡單的用戶管理接口,包括增刪改查等功能。若是讀者對REST API不熟悉,能夠先從Wiki頁面瞭解一下。python
另外,爲了方便你們閱讀和理解,本系列的代碼會放在github上,diabloneo/webdemo。git
在OpenStack的項目中,都是在URL中代表這個API的版本號的,好比Keystone的API會有/v2.0和/v3的前綴,代表兩個不一樣版本的API;Magnum項目目前的API則爲v1版本。由於咱們的webdemo項目纔剛剛開始,因此咱們也把咱們的API版本設置爲v1,下文會說明怎麼實現這個version號的設置。github
咱們將要設計一個管理用戶的API,這個和Keystone的用戶管理的API差很少,這裏先列出每一個API的形式,以及簡要的內容說明。這裏咱們會把上面提到的version號也加入到URL path中,讓讀者能更容易聯繫起來。web
GET /v1/users 獲取全部用戶的列表。sql
POST /v1/users 建立一個用戶數據庫
GET /v1/users/<UUID> 獲取一個特定用戶的詳細信息。json
PUT /v1/users/<UUID> 修改一個用戶的詳細信息。segmentfault
DELETE /v1/users/<UUID> 刪除一個用戶。api
這些就是咱們要實現的用戶管理的API了。其中,<UUID>表示使用一個UUID字符串,這個是OpenStack中最常常被用來做爲各類資源ID的形式,以下所示:
In [5]: import uuid In [6]: print uuid.uuid4() adb92482-baab-4832-84bc-f842f3eabd66 In [7]: print uuid.uuid4().hex 29520c88de6b4c76ae8deb48db0a71e7
由於是個demo,因此咱們設置一個用戶包含的信息會比較簡單,只包含name和age。
接下來就要開始編碼工做了。首先要把整個服務的框架搭建起來。咱們會在軟件包管理這篇文件中的代碼基礎上繼續咱們的demo(全部這些代碼在github的倉庫裏都能看到)。
通常來講,OpenStack項目中,使用Pecan來開發API服務時,都會在代碼目錄下有一個專門的API目錄,用來保存API相關的代碼。好比Magnum項目的magnum/api,或者Ceilometer項目的ceilometer/api等。咱們的代碼也遵照這個規範,讓咱們直接來看下咱們的代碼目錄結構(#後面的表示註釋):
➜ ~/programming/python/webdemo/webdemo/api git:(master) ✗ $ tree . . ├── app.py # 這個文件存放WSGI application的入口 ├── config.py # 這個文件存放Pecan的配置 ├── controllers/ # 這個目錄用來存放Pecan控制器的代碼 ├── hooks.py # 這個文件存放Pecan的hooks代碼(本文中用不到) └── __init__.py
這個在API服務(3)這篇文章中已經說明過了。
爲了後面更好的開發,咱們須要先讓咱們的服務在本地跑起來,這樣能夠方便本身作測試,看到代碼的效果。不過要作到這點,仍是有些複雜的。
首先,先建立config.py文件的內容:
app = { 'root': 'webdemo.api.controllers.root.RootController', 'modules': ['webdemo.api'], 'debug': False, }
就是包含了Pecan的最基本配置,其中指定了root controller的位置。而後看下app.py文件的內容,主要就是讀取config.py中的配置,而後建立一個WSGI application:
import pecan from webdemo.api import config as api_config def get_pecan_config(): filename = api_config.__file__.replace('.pyc', '.py') return pecan.configuration.conf_from_file(filename) def setup_app(): config = get_pecan_config() app_conf = dict(config.app) app = pecan.make_app( app_conf.pop('root'), logging=getattr(config, 'logging', {}), **app_conf ) return app
而後,咱們至少還須要實現一下root controller,也就是webdemo/api/controllers/root.py這個文件中的RootController
類:
from pecan import rest from wsme import types as wtypes import wsmeext.pecan as wsme_pecan class RootController(rest.RestController): @wsme_pecan.wsexpose(wtypes.text) def get(self): return "webdemo"
爲了繼續開放的方便,咱們要先建立一個Python腳本,能夠啓動一個單進程的API服務。這個腳本會放在webdemo/cmd/目錄下,名稱是api.py(這目錄和腳本名稱也是慣例),來看看咱們的api.py吧:
from wsgiref import simple_server from webdemo.api import app def main(): host = '0.0.0.0' port = 8080 application = app.setup_app() srv = simple_server.make_server(host, port, application) srv.serve_forever() if __name__ == '__main__': main()
要運行這個測試服務器,首先須要安裝必要的包,而且設置正確的路徑。在後面的文章中,咱們將會知道,這個能夠經過tox這個工具來實現。如今,咱們先作個簡單版本的,就是手動建立這個運行環境。
首先,完善一下requirements.txt這個文件,包含咱們須要的包:
pbr<2.0,>=0.11 pecan WSME
而後,咱們手動建立一個virtualenv環境,而且安裝requirements.txt中要求的包:
➜ ~/programming/python/webdemo git:(master) ✗ $ virtualenv .venv New python executable in .venv/bin/python Installing setuptools, pip, wheel...done. ➜ ~/programming/python/webdemo git:(master) ✗ $ source .venv/bin/activate (.venv)➜ ~/programming/python/webdemo git:(master) ✗ $ pip install -r requirement.txt ... Successfully installed Mako-1.0.3 MarkupSafe-0.23 WSME-0.8.0 WebOb-1.5.1 WebTest-2.0.20 beautifulsoup4-4.4.1 logutils-0.3.3 netaddr-0.7.18 pbr-1.8.1 pecan-1.0.3 pytz-2015.7 simplegeneric-0.8.1 singledispatch-3.4.0.3 six-1.10.0 waitress-0.8.10
啓動服務須要技巧,由於咱們的webdemo尚未安裝到系統的Python路徑中,也不在上面建立virtualenv環境中,因此咱們須要經過指定PYTHONPATH這個環境變量來爲Python程序增長庫的查找路徑:
(.venv)➜ ~/programming/python/webdemo git:(master) ✗ $ PYTHONPATH=. python webdemo/cmd/api.py
如今測試服務器已經起來了,能夠經過瀏覽器訪問http://localhost:8080/ 這個地址來查看結果。(你可能會發現,返回的是XML格式的結果,而咱們想要的是JSON格式的。這個是WSME的問題,咱們後面再來處理)。
到這裏,咱們的REST API服務的框架已經搭建完成,而且測試服務器也跑起來了。
如今咱們來實現咱們在第一章設計的API。這裏先說明一下:咱們會直接使用Pecan的RestController來實現REST API,這樣能夠不用爲每一個接口指定接受的method。
如今,全部的OpenStack項目的REST API的返回格式都是使用JSON標準,因此咱們也要這麼作。那麼有什麼辦法可以讓WSME框架返回JSON數據呢?能夠經過設置wsmeext.pecan.wsexpose()
的rest_content_types參數來是先。這裏,咱們借鑑一段Magnum項目中的代碼,把這段代碼存放在文件webdemo/api/expose.py中:
import wsmeext.pecan as wsme_pecan def expose(*args, **kwargs): """Ensure that only JSON, and not XML, is supported.""" if 'rest_content_types' not in kwargs: kwargs['rest_content_types'] = ('json',) return wsme_pecan.wsexpose(*args, **kwargs)
這樣咱們就封裝了本身的expose
裝飾器,每次都會設置響應的content-type爲JSON。上面的root controller代碼也就能夠修改成:
from pecan import rest from wsme import types as wtypes from webdemo.api import expose class RootController(rest.RestController): @expose.expose(wtypes.text) def get(self): return "webdemo"
再次運行咱們的測試服務器,就能夠返現返回值爲JSON格式了。
這個其實就是實現v1這個版本的API的路徑前綴。在Pecan的幫助下,咱們很容易實現這個,只要按照以下兩步作便可:
先實現v1這個controller
把v1 controller加入到root controller中
按照OpenStack項目的規範,咱們會先創建一個webdemo/api/controllers/v1/目錄,而後將v1 controller放在這個目錄下的一個文件中,假設咱們就放在v1/controller.py文件中,效果以下:
from pecan import rest from wsme import types as wtypes from webdemo.api import expose class V1Controller(rest.RestController): @expose.expose(wtypes.text) def get(self): return 'webdemo v1controller'
而後把這個controller加入到root controller中:
... from webdemo.api.controllers.v1 import controller as v1_controller from webdemo.api import expose class RootController(rest.RestController): v1 = v1_controller.V1Controller() @expose.expose(wtypes.text) def get(self): return "webdemo"
此時,你訪問http://localhost:8080/v1就能夠看到結果了。
這個API就是返回全部的用戶信息,功能很簡單。首先要添加users controller到上面的v1 controller中。爲了避免影響閱讀體驗,這裏就不貼代碼了,請看github上的示例代碼。
上篇文章中,咱們已經提到了WSME能夠用來規範API的請求和響應的值,這裏咱們就要用上它。首先,咱們要參考OpenStack的慣例來設計這個API的返回值:
{ "users": [ { "name": "Alice", "age": 30 }, { "name": "Bob", "age": 40 } ] }
其中users是一個列表,列表中的每一個元素都是一個user。那麼,咱們要如何使用WSME來規範咱們的響應值呢?答案就是使用WSME的自定義類型。咱們能夠利用WSME的類型功能定義出一個user類型,而後再定義一個user的列表類型。最後,咱們就可使用上面的expose方法來規定這個API返回的是一個user的列表類型。
這裏咱們須要用到WSME的Complex types的功能,請先看一下文檔Types。簡單說,就是咱們能夠把WSME的基本類型組合成一個複雜的類型。咱們的類型須要繼承自wsme.types.Base這個類。由於咱們在本文只會實現一個user相關的API,因此這裏咱們把全部的代碼都放在webdemo/api/controllers/v1/users.py文件中。來看下和user類型定義相關的部分:
from wsme import types as wtypes class User(wtypes.Base): name = wtypes.text age = int class Users(wtypes.Base): users = [User]
這裏咱們定義了class User
,表示一個用戶信息,包含兩個字段,name是一個文本,age是一個整型。class Users
表示一組用戶信息,包含一個字段users,是一個列表,列表的元素是上面定義的class User
。完成這些定義後,咱們就使用WSME來檢查咱們的API是否返回了合格的值;另外一方面,只要咱們的API返回了這些類型,那麼就能經過WSME的檢查。咱們先來完成利用WSME來檢查API返回值的代碼:
class UsersController(rest.RestController): # expose方法的第一個參數表示返回值的類型 @expose.expose(Users) def get(self): pass
這樣就完成了API的返回值檢查了。
咱們如今來完成API的邏輯部分。不過爲了方便你們理解,咱們直接返回一個寫好的數據,就是上面貼出來的那個。
class UsersController(rest.RestController): @expose.expose(Users) def get(self): user_info_list = [ { 'name': 'Alice', 'age': 30, }, { 'name': 'Bob', 'age': 40, } ] users_list = [User(**user_info) for user_info in user_info_list] return Users(users=users_list)
代碼中,會先根據user信息生成User實例的列表users_list
,而後再生成Users實例。此時,重啓測試服務器後,你就能夠從瀏覽器訪問http://localhost:8080/v1/users,就能看到結果了。
這個API會接收用戶上傳的一個JSON格式的數據,而後打印出來(實際中通常是存到數據庫之類的),要求用戶上傳的數據符合User類型的規範,而且返回的狀態碼爲201。代碼以下:
class UsersController(rest.RestController): @expose.expose(None, body=User, status_code=201) def post(self, user): print user
可使用curl程序來測試:
~/programming/python/webdemo git:(master) ✗ $ curl -X POST http://localhost:8080/v1/users -H "Content-Type: application/json" -d '{"name": "Cook", "age": 50}' -v * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8080 (#0) > POST /v1/users HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.43.0 > Accept: */* > Content-Type: application/json > Content-Length: 27 > * upload completely sent off: 27 out of 27 bytes * HTTP 1.0, assume close after body < HTTP/1.0 201 Created < Date: Mon, 16 Nov 2015 15:18:24 GMT < Server: WSGIServer/0.1 Python/2.7.10 < Content-Length: 0 < * Closing connection 0
同時,服務器上也會打印出:
127.0.0.1 - - [16/Nov/2015 23:16:28] "POST /v1/users HTTP/1.1" 201 0 <webdemo.api.controllers.v1.users.User object at 0x7f65e058d550>
咱們用3行代碼就實現了這個POST的邏輯。如今來講明一下這裏的祕密。expose
裝飾器的第一個參數表示這個方法沒有返回值;第三個參數表示這個API的響應狀態碼是201,若是不加這個參數,在沒有返回值的狀況下,默認會返回204。第二個參數要說明一下,這裏用的是body=User
,你也能夠直接寫User
。使用body=User
這種形式,你能夠直接發送符合User規範的JSON字符串;若是是用expose(None, User, status_code=201)
那麼你須要發送下面這樣的數據:
{ "user": {"name": "Cook", "age": 50} }
你能夠本身測試一下區別。要更多的瞭解本節提到的expose參數,請參考WSM文檔Functions。
最後,你接收到一個建立用戶請求時,通常會爲這個用戶分配一個id。本文前面已經提到了OpenStack項目中通常使用UUID。你能夠修改一下上面的邏輯,爲每一個用戶分配一個UUID。
要實現這個API,須要兩個步驟:
在UsersController中解析出<UUID>的部分,而後把請求傳遞給這個一個新的UserController。從命名能夠看出,UsersController是針對多個用戶的,UserController是針對一個用戶的。
在UserController中實現get()
方法。
Pecan的_lookup()
方法是controller中的一個特殊方法,Pecan會在特定的時候調用這個方法來實現更靈活的URL路由。Pecan還支持用戶實現_default()
和_route()
方法。這些方法的具體說明,請閱讀Pecan的文檔:routing。
咱們這裏只用到_lookup()
方法,這個方法會在controller中沒有其餘方法能夠執行且沒有_default()
方法的時候執行。好比上面的UsersController中,沒有定義/v1/users/<UUID>如何處理,它只能返回404;若是你定義了_lookup()
方法,那麼它就會調用該方法。
_lookup()
方法須要返回一個元組,元組的第一個元素是下一個controller的實例,第二個元素是URL path中剩餘的部分。
在這裏,咱們就須要在_lookup()
方法中解析出UUID的部分並傳遞給新的controller做爲新的參數,而且返回剩餘的URL path。來看下代碼:
class UserController(rest.RestController): def __init__(self, user_id): self.user_id = user_id class UsersController(rest.RestController): @pecan.expose() def _lookup(self, user_id, *remainder): return UserController(user_id), remainder
_lookup()
方法的形式爲_lookup(self, user_id, *remainder)
,意思就是會把/v1/users/<UUID>中的<UUID>部分做爲user_id這個參數,剩餘的按照"/"分割爲一個數組參數(這裏remainder爲空)。而後,_lookup()
方法裏會初始化一個UserController實例,使用user_id做爲初始化參數。這麼作以後,這個初始化的控制器就能知道是要查找哪一個用戶了。而後這個控制器會被返回,做爲下一個控制被調用。請求的處理流程就這麼轉移到UserController中了。
實現前,咱們要先修改一下咱們返回的數據,裏面須要增長一個id字段。對應的User定義以下:
class User(wtypes.Base): id = wtypes.text name = wtypes.text age = int
如今,完整的UserController代碼以下:
class UserController(rest.RestController): def __init__(self, user_id): self.user_id = user_id @expose.expose(User) def get(self): user_info = { 'id': self.user_id, 'name': 'Alice', 'age': 30, } return User(**user_info)
使用curl來檢查一下效果:
➜ ~/programming/python/webdemo git:(master) ✗ $ curl http://localhost:8080/v1/users/29520c88de6b4c76ae8deb48db0a71e7 {"age": 30, "id": "29520c88de6b4c76ae8deb48db0a71e7", "name": "Alice"}
你可能會有疑問:這裏咱們修改了User類型,增長了一個id字段,那麼前面實現的POST /v1/users會不會失效呢?你能夠本身測試一下。(答案是不會,由於這個類型裏的字段都是可選的)。這裏順便講兩個技巧。
如何設置一個字段爲強制字段
像下面這樣作就能夠了(你能夠測試一下,改爲這樣後,不傳遞id的POST /v1/users會失敗):
class User(wtypes.Base): id = wtypes.wsattr(wtypes.text, mandatory=True) name = wtypes.text age = int
如何檢查一個可選字段的值是否存在
檢查這個值是否爲None是確定不行的,須要檢查這個值是否爲wsme.Unset。
這個和上一個API同樣,不過_lookup()
方法已經實現過了,直接添加方法到UserController中便可:
class UserController(rest.RestController): @expose.expose(User, body=User) def put(self, user): user_info = { 'id': self.user_id, 'name': user.name, 'age': user.age + 1, } return User(**user_info)
經過curl來測試:
➜ ~/programming/python/webdemo git:(master) ✗ $ curl -X PUT http://localhost:8080/v1/users/29520c88de6b4c76ae8deb48db0a71e7 -H "Content-Type: application/json" -d '{"name": "Cook", "age": 50}' {"age": 51, "id": "29520c88de6b4c76ae8deb48db0a71e7", "name": "Cook"}%
同上,沒有什麼新的內容:
class UserController(rest.RestController): @expose.expose() def delete(self): print 'Delete user_id: %s' % self.user_id
到此爲止,咱們已經完成了咱們的API服務了,雖然沒有實際的邏輯,可是本文搭建起來的框架也是OpenStack中API服務的一個經常使用框架,不少大項目的API服務代碼都和咱們的webdemo長得差很少。最後再說一下,本文的代碼在github上託管着:diabloneo/webdemo。
如今咱們已經瞭解了包管理和API服務了,那麼接下來就要開始數據庫相關的操做了。大部分OpenStack的項目都是使用很是著名的sqlalchemy庫來實現數據庫操做的,本系列接下來的文章就是要來講明數據庫的相關知識和應用。