1. 程式人生 > >python 64式: 第18式、python專案通用rest api框架搭建與編寫

python 64式: 第18式、python專案通用rest api框架搭建與編寫

1 用PasteScript建立專案
1.1 安裝PasteScript
安裝命令:
pip install PasteScript

檢視可用的模板
[[email protected] test_project]# paster create --list-templates     
Available templates:
  basic_package:  A basic setuptools-enabled package
  paste_deploy:   A web application deployed through paste.deploy

1.2 建立專案
paster create -t basic_package myproject
回答一系列問題後,專案生成

1.3 檢視生成的專案目錄結構
[[email protected] test_project]# tree myproject
myproject
|-- myproject
|   `-- __init__.py
|-- myproject.egg-info
|   |-- dependency_links.txt
|   |-- entry_points.txt
|   |-- not-zip-safe
|   |-- PKG-INFO
|   |-- SOURCES.txt
|   `-- top_level.txt
|-- setup.cfg
`-- setup.py


2 修改setup.py,採用pbr部署
2.1 將setup.py中內容直接替換為如下內容
import setuptools

setuptools.setup(
    setup_requires=['pbr'],
    pbr=True)

2.2 修改setup.cfg內容為如下內容
[metadata]
name = myproject
version = 1.0
summary = myproject
description-file =
    README.rst
author = me
author-email = 
classifier =
    Intended Audience :: Developers
    Programming Language :: Python :: 2.7

[global]
setup-hooks =
    pbr.hooks.setup_hook

[files]
packages =
    myproject
data_files =
    /etc/myproject = etc/myproject/*
    /var/www/myproject = etc/apache2/app.wsgi
    /etc/httpd/conf.d = etc/apache2/myproject.conf

[entry_points]
wsgi_scripts =
    myproject-api = myproject.api.app:build_wsgi_app

oslo.config.opts =
    myproject = myproject.opts:list_opts

分析:
一 pbr和setup.cfg
1) pbr是setuptools的外掛,所以你必須使用setuptools,然後呼叫setuptools.setup()函式
2) pbr只需要最小化的setup.py 檔案,跟普通的使用setuptools的專案相比。這是因為設定都在setup.cfg裡面
3) setuptools的setup函式來進行設定時,如果與位於setup.cfg中的資訊產生衝突,則setup.cfg則優先。
4) setup.cfg文件。這個文件像ini 文件。它基於專案distutils2的setup.cfg檔案設定
5) setup.cfg檔案中有幾個段落:
metadata
files
entry_points
pbr

二 files段落定義了包中的檔案位置,有三個基本的設定鍵:packages,namespace_packages,以及data_files.
1) packages: 指定需要安裝的包的列表,若packages沒有指定,預設為metadata段落中的name的值

2) data_files: 列出了需要被安裝的檔案。格式是縮排後跟上鍵值對

例如aodh的setup.cfg中配置
[files]
packages =
    aodh
data_files =
    etc/aodh = etc/aodh/*

上述例子表明: etc/aodh/*中的檔案最終會被拷貝待/etc/aodh資料夾中


三 entry_points
作用:定義當前專案做為第三方python的庫可以被其他python程式呼叫的lib進入點
1) console_scripts控制指令碼
格式:
console_scripts = 
    指令碼名稱 = 模組:可匯入物件
例子:
console_scripts =
    aodh-evaluator = aodh.cmd.alarm:evaluator
解釋:
這裡會產生一個aodh-evaluator的指令碼,執行aodh.cmd.alarm中的evaluator
函式。

2) 外掛
格式:
名稱空間 =
  外掛名稱 = 模組:可匯入物件
例子:
ceilometer.compute.virt = 
    libvirt = ceilometer.compute.virt.libvirt.inspector:LibvirtInspector

pbr參考
https://www.cnblogs.com/yasmi/p/5183423.html
https://docs.openstack.org/pbr/latest/
https://docs.openstack.org/pbr/latest/user/using.html

3 新增requirements.txt檔案
該檔案和setup.py在同一個目錄
新增內容如下:
pbr<2.0,>=0.11
pecan
WSME

解釋:
新增requirements.txt是因為給專案新增所依賴的第三方庫,
使得專案在安裝過程中會安裝requirements.txt中的庫,從而
確保專案執行正常。


4 新增paste部署所需要的內容
具體在myproject目錄下新建一個目錄api
4.1 api目錄下新增一個檔案: __init__.py
裡面不要填寫任何內容

4.2 api目錄下新增一個檔案: api-paste.ini
向該檔案中寫入如下內容:
[composite:main]
use = egg:Paste#urlmap
/ = api

[app:api]
paste.app_factory = myproject.api.app:app_factory


4.3 api目錄下新增一個檔案: app.py
在該檔案中寫入如下內容:
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from paste import deploy
import pecan

PECAN_CONFIG = {
    'app': {
        'root': 'myproject.api.controllers.root.RootController',
        'modules': ['myproject.api'],
    },
}


def app_factory(global_config, **local_config):
    print "######### enter app_factory"
    # NOTE, it needs add the line below
    pecan.configuration.set_config(dict(PECAN_CONFIG), overwrite=True)
    app = pecan.make_app(
        PECAN_CONFIG['app']['root']
    )
    return app

def getUri():
    # TODO(), it needs to get real path of api-paste.ini
    # the path is setted by the data_files under [files] in setup.config
    configPath = "/etc/myproject/api-paste.ini"
    result = "config:" + configPath
    return result

def getAppName():
    return "main"

def build_wsgi_app():
    print "######### enter build_wsgi_app"
    uri = getUri()
    appName = getAppName()
    app = deploy.loadapp(uri, name=appName)
    return app

解釋:
上述通過pecan來構建一個app並返回,需要指定處理總入口,這裡通過root引數指定

總結:
此時的整體目錄結構如下:
[[email protected] test_project]# tree myproject
myproject
|-- myproject
|   |-- api
|   |   |-- api-paste.ini
|   |   |-- app.py
|   |   `-- __init__.py
|   `-- __init__.py
|-- myproject.egg-info
|   |-- dependency_links.txt
|   |-- entry_points.txt
|   |-- not-zip-safe
|   |-- PKG-INFO
|   |-- SOURCES.txt
|   `-- top_level.txt
|-- requirements.txt
|-- setup.cfg
`-- setup.py

5 新增pecan路由分發所需的內容
因為在4.3中指定了處理入口是:
"myproject.api.controllers.root.RootController"
因此,需要在api目錄下新建一個controllers目錄,
5.1 在controllers目錄下新增一個檔案: __init__.py, 檔案內容如下:

from oslo_config import cfg

# Register options for the service
OPTS = [
    cfg.StrOpt('paste_config',
               default="api-paste.ini",
               help="Configuration file for WSGI definition of API."),
    cfg.IntOpt('port',
               default=8043,
               help="The port for the myproject API server. (port value)."),
    ]


之所以要新增這個檔案,是因為:
__init__.py可以做為package的標識,如果沒有該檔案,該目錄不會被仍為是package
上述添加了一些配置,用於後續配置檔案的生成

參考:
https://www.cnblogs.com/AlwinXu/p/5598543.html

5.2 在controllers目錄下新增一個檔案: root.py,
該檔案內容如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pecan import rest
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan

from myproject.api.controllers.v1 import root as v1

class RootController(rest.RestController):
    v1 = v1.V1Controller()

    @wsme_pecan.wsexpose(wtypes.text)
    def get(self):
        return "####### RootController"

5.3 在controllers目錄下新增v1目錄
1) 在v1目錄下新增 __init__.py檔案
該檔案中不要填寫任何內容

2) 在v1目錄下新增root.py檔案
向該檔案中填入如下內容:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pecan import rest
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan

from myproject.api.controllers.v1 import students as v1Students

class V1Controller(rest.RestController):
    """Version 1 API controller root."""

    students = v1Students.StudentsController()

    @wsme_pecan.wsexpose(wtypes.text)
    def get(self):
        return '#######  V1Controller'

3) 在v1目錄下新增students.py檔案
向該檔案中填入如下內容:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pecan
from pecan import rest
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
import wsmeext.pecan

class Student(wtypes.Base):
    id = wtypes.wsattr(wtypes.text, mandatory=True)
    name = wtypes.text
    age = int

class Students(wtypes.Base):
    students = [Student]

class StudentController(rest.RestController):
    def __init__(self, id):
        self.id = id

    @wsmeext.pecan.wsexpose(Student)
    def get(self):
        # TODO(), get data from table of some database
        # and repliace those code below
        stud1 = {'id': '0001', 'name': 'chen', 'age': 20}
        student = Student(**stud1)
        return student

    @wsmeext.pecan.wsexpose(Student, body=Student)
    def put(self, data):
        # TODO(), update the corresponding column which belongs to
        # the table of some database and replace code below
        studDict = {
            'id': self.id,
            'name': data.name,
            'age': data.age
        }
        student = Student(**studDict)
        return student

    @wsmeext.pecan.wsexpose(None, status_code=204)
    def delete(self):
        # TODO(), delete the corresponding column which belongs to
        # the table of some database and replace code below
        print "delete id: {id}".format(id=self.id)

class StudentsController(rest.RestController):
    @pecan.expose()
    def _lookup(self, id, *remainder):
        return StudentController(id), remainder

    # @pecan.expose(Students)
    @wsmeext.pecan.wsexpose(Students)
    def get(self):
        # TODO(), get datas from table of some database
        # and replace those code below
        stud1 = {'id': '0001', 'name': 'chen', 'age': 20}
        stud2 = {'id': '0002', 'name': 'xiang', 'age': 30}
        students = []
        students.append(stud1)
        students.append(stud2)
        studList = [Student(**stud) for stud in students]
        result = Students(students=studList)
        return result

    @wsmeext.pecan.wsexpose(Student, body=Student, status_code=201)
    def post(self, data):
        # TODO(), insert it into table of some database
        # and return it
        return data

分析:
1) pecan.rest.RestController
含義: RestController實現了一系列路由方法,                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
作用: 之所以StudentsController類繼承pecan.rest.RestController類,
     是因為這個類定義每個請求型別對應的處理方法(例如:指定請求如果是get型別,呼叫對應RestController
     類的get方法進行處理)。它實現了預設的GET, POST, PUT, DELETE方法。

2) @pecan.expose()
作用: 之所以新增這個裝飾器是因為,如果方法沒有被expose()所裝飾,Pecan不會將一個請求分發給這個方法
引數: 預設被裝飾後返回的結果是html型別的,如果需要返回json格式的結果,需要用這種方式使用
    @pecan.expose('json')

3) pecan.rest.RestController._lookup(currentContent, **remainder)
含義: 提供了一種方式來處理一個url中的一部分,返回一個新的controller物件來路由到剩餘部分。
作用: 這個方法是用於根據當前url解析發現當前controller物件中沒有任何方法可以處理後,就會呼叫
     _lookup方法繼續向下進行路由分發。
引數: 
currentContent: 當前url處理的路徑中的部分,舉例如下: 假設請求的url是:
         /v1/students/001/addr
        先根據: /v1,進入V1Controller;
        然後根據: 剩餘url路徑中的/students,進入StudentsController
        然後根據: 剩餘url路徑中/001不對應任何StudentsController方法,那麼
                         currrentContent就是001這個內容,remainder部分就是剩餘的url路徑按照/分隔為一個數組
remainder: 剩餘url路徑按照/分隔為的一個數組。

4) 這裡使用了wsme庫
含義: Web Services Made Easy,實際就是檢查api中輸入引數和返回值型別的校驗
組成: 
wsme.types: 對引數進行型別校驗

wsme.wsattr(datatype, mandatory=False, name=None, default=Unset, readonly=False):
作用:允許你新增更多屬性,例如: mandatory=True 表示該屬性不能為空
引數:
datatype:資料型別,例如wtypes.text
mandatory: 是否不能為空,False表示可以為空

wsmeext.pecan: pecan使用wsme的介面卡

wsmeext.pecan.wsexpose(return_type, *arg_type, **options):
作用: 對pecan路由分發得請求得返回值和輸入引數進行型別檢查
引數:
return_type:方法返回值的型別
arg_type: 輸入引數的型別,可以有多個
options: 字典引數,例如用得較多得是status_code=204

參考:
https://blog.csdn.net/dingdingwolf/article/details/44486721
https://pecan.readthedocs.io/en/latest/routing.html
https://wsme.readthedocs.io/en/latest/types.html

6 安裝專案
進入myproject專案,然後執行:
python setup.py install
安裝專案


7 服務啟動以及curl命令驗證
7.1啟動服務
普通啟動方式,請執行如下命令:
/usr/bin/myproject-api --port=8043
或者如果想以httpd啟動(這裡在centos環境下),請執行如下命令:
systemctl restart httpd

7.2 curl命令驗證rest api框架搭建成功

獲取所有:
curl http://127.0.0.1:8043/v1/students

建立單個:
curl -X POST http://127.0.0.1:8043/v1/students -H "Content-Type: application/json" -d '{"id": "0003", "name": "ping", "age": 50}' -v
注意: 
1 students後面不要新增/,否則會繼續向下路由分發進入下一個controller,而不是用當前controller的post。
pecan對每個'/'都很細緻
2 POST和PUT請求需要新增: -H "Content-Type: application/json"
否則解析傳入的資料出錯,因為資料格式是json的,這裡要指定傳入的資料型別也是json

獲取單個:
curl http://127.0.0.1:8043/v1/students/0001

更新單個:
curl -X PUT http://127.0.0.1:8043/v1/students/0001 -H "Content-Type: application/json" -d '{"id": "0001","name": "dong", "age": 22}' -v

刪除單個:
curl -X DELETE http://127.0.0.1:8043/v1/students/0001 -H "Content-Type: application/json"

注意: -d中 {}最外面的是單引號,括號裡面的都是雙引號,否則會導致解析錯誤

解釋:
-X 指定請求方法
-H 增加頭部資訊
-d POST請求方法時傳送的資料
-v 詳細資訊

curl參考:
https://blog.csdn.net/danchu/article/details/72290092

8 生成配置檔案
8.1 配置檔案的生成
配置部分的設定在setup.cfg中,下面是aodh.setup.cfg中關於配置生成的部分
oslo.config.opts =
    aodh = aodh.opts:list_opts
    aodh-auth = aodh.opts:list_keystoneauth_opts

oslo.config.opts.defaults =
    aodh = aodh.conf.defaults:set_cors_middleware_defaults


分析:
1 oslo.config通用庫用於解析命令列和配置檔案中的配置選項
2 在aodh/opts.py檔案中定義如下內容
def list_opts():
    return [
        ('DEFAULT',
         itertools.chain(
             aodh.evaluator.OPTS,
             aodh.evaluator.event.OPTS,
             aodh.evaluator.threshold.OPTS,
             aodh.notifier.rest.OPTS,
             aodh.queue.OPTS,
             aodh.service.OPTS)),
        ('api',
         itertools.chain(
             aodh.api.OPTS,
             aodh.api.controllers.v2.alarm_rules.gnocchi.GNOCCHI_OPTS,
             aodh.api.controllers.v2.alarms.ALARM_API_OPTS)),
        ('coordination', aodh.coordination.OPTS),
        ('database', aodh.storage.OPTS),
        ('evaluator', aodh.service.EVALUATOR_OPTS),
        ('listener', itertools.chain(aodh.service.LISTENER_OPTS,
                                     aodh.event.OPTS)),
        ('notifier', aodh.service.NOTIFIER_OPTS),
        ('service_credentials', aodh.keystone_client.OPTS),
        ('service_types', aodh.notifier.zaqar.SERVICE_OPTS),
        ('notifier', aodh.notifier.OPTS),
        ('sms', aodh.notifier.sms.OPTS),
        ('email', aodh.notifier.email.OPTS),
        ('action_resources', aodh.action_resources.handler.OPTS),
        ('service_type', itertools.chain(aodh.chakra_client.OPTS,
                                         aodh.billing_client.OPTS))
    ]

分析: 上述應該是收集各個組別下的配置項。返回一個數組,陣列中的每個元素是
二元組(oslo.config中的組名,該組名下的所有配置項)

3 itertools.chain(*iterables)
作用:生成一個迭代器物件,用於不能將輸入一下子放入記憶體進行處理的情況
引數: *iterables:一個可迭代物件,例如多個數組
返回: 迭代器,本質是用yield將每次執行結果返回

4 生成配檔案
官網命令:
To generate a sample config file for an application myapp that has its own options and uses oslo.messaging, you would list both namespaces:

$ oslo-config-generator --namespace myapp \
    --namespace oslo.messaging > myapp.conf

示例命令:
oslo-config-generator --config-file=myproject-config-generator.conf


最終根據示例命令生成的結果如下:
生成myproject.con檔案,具體內容如下所示

[DEFAULT]


[api]

#
# From myproject
#

# Configuration file for WSGI definition of API. (string value)
#paste_config = api-paste.ini

# The port for the myproject API server. (port value). (integer value)
#port = 8043


參考:
配置檔案生成參考
https://blog.csdn.net/karamos/article/details/80121792
https://docs.openstack.org/oslo.config/latest/cli/generator.html
itertools參考:
https://blog.csdn.net/weixin_38104872/article/details/78826948