python 64式: 第18式、python项目通用rest api框架搭建与编写

文彭祖
2023-12-01

1 用PasteScript创建项目
1.1 安装PasteScript
安装命令:
pip install PasteScript

查看可用的模板
[root@localhost 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 查看生成的项目目录结构
[root@localhost 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参数指定

总结:
此时的整体目录结构如下:
[root@localhost 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


 

 类似资料: