# pecan

基础知识介绍：

**一. 文件中需要包含一个`config.py`文件，该文件用于标注pecan程序的起点等配置信息：**

```python
app = {
    'root': 'webdemo.api.controllers.root.RootController',
    'modules': ['webdemo.api'],
    'debug': True,
}
```

> 1. `modules`：其中包含的python包，是`app.py`文件所在的包，即`setup_app`方法所在的包
> 2. `root`：即`RootController`所在的路径，`/`路径
> 3. `debug`：是否开启调试，生产环境的话，将其置为`False`

**二. pecan可以实现对象分发式的路由**

当RootController继承`pecan.rest.RestController`时，存在URL映射关系如下表所示，当然也可以另外自己定义新的方式：

| Method      | Description                                  | Example Method(s) / URL(s)                      |
| ----------- | -------------------------------------------- | ----------------------------------------------- |
| get\_one    | Display one record.                          | GET /books/1                                    |
| get\_all    | Display all records in a resource.           | GET /books/                                     |
| get         | A combo of get\_one and get\_all.            | GET /books/ 或 GET /books/1                      |
| new         | Display a page to create a new resource.     | GET /books/new                                  |
| edit        | Display a page to edit an existing resource. | GET /books/1/edit                               |
| post        | Create a new record.                         | POST /books/                                    |
| put         | Update an existing record.                   | POST /books/1?\_method=put 或 PUT /books/1       |
| get\_delete | Display a delete confirmation page.          | GET /books/1/delete                             |
| delete      | Delete an existing record.                   | POST /books/1?\_method=delete 或 DELETE /books/1 |

在RootController类中，一般会写上如下的代码：

```python
from pecan import rest
import pecan


class RootController(rest.RestController):
    
    @pecan.expose()
    def get(self):
        return 'This is RootController GET.'
```

当存在以上代码时，使用以下命令调用就会有返回值：

```bash
curl -X GET http://127.0.0.1:8080
```

> 返回值为:
>
> ```
> This is RootController GET.
> ```

也就是说，curl中的`GET`对应了代码中的`get()`方法，若要增加POST方法，可以在代码中添加如下：

```python
@pecan.expose()
def post(self):
    return 'This is RootController POST.'
```

当然，如果有的方法，需要既可以使用`GET`，又可以使用`POST`，且对不同的方法有不同的回应，则可以写成如下：

```python
from pecan import rest
import pecan


class RootController(rest.RestController):
    
    _custom_actions = {
        'test': ['GET', 'POST'],
    }
    
    @pecan.expose()
    def test(self):
        if pecan.request.method == 'POST':
            return 'This is RootController test POST.'
        elif pecan.request.method == 'GET':
            return 'This is RootController test GET.'
```

可以使用如下命令进行请求：

```bash
curl -X GET http://127.0.0.1:8080/test
curl -X POST http://127.0.0.1:8080/test
```

\==下面重点介绍对象分发式路由：==

当存在以下代码的时候：

```python
class v1Controller(rest.RestController):
    @pecan.expose()
    def get(self):
        return 'This is v1Controller GET.'


class RootController(rest.RestController):
    
    v1 = v1Controller()
```

在`RootController`类中，生成了一个`v1Controller`对象，这个对象就是用来做分发式路由的，因此可以使用如下命令去调用它：

```bash
curl -X GET http://127.0.0.1:8080/v1
```

**三. 使用WSME来规范API的响应值**

wsme模块可以用来规范API的请求和响应值，并且可以和pecan模块很好的结合在一起，其支持的类型如下：

| Type          | Json type                    |
| ------------- | ---------------------------- |
| str           | String                       |
| unicode       | String                       |
| int           | Number                       |
| float         | Number                       |
| bool          | Boolean                      |
| Decimal       | String                       |
| date          | String (YYYY-MM-DD)          |
| time          | String (hh:mm:ss)            |
| datetime      | String (YYYY-MM-DDThh:mm:ss) |
| Arrays        | Array                        |
| None          | null                         |
| Complex types | Object                       |

在下面这个例子中，可以看出：

```python
from wsmeext.pecan import wsexpose
from pecan import rest

class RootController(rest.RestController):
    
    @wsexpose(int, int)
    def get_one(self, arg):
        return 1
```

> `@wsexpose(int, int)`中第一个`int`表示返回值必须为int，第二个`int`表示请求参数必须为int

在这个例子中，可以使用curl去测试：

```bash
curl -X GET http://127.0.0.1:8081/1

# 返回消息如下：
1
```

若用以下的输入，则会错误：

```bash
curl -X GET http://127.0.0.1:8081/xxx

# 返回消息如下：
{"debuginfo": null, "faultcode": "Client", "faultstring": "Invalid input for field/attribute arg. Value: 'xxx'. unable to convert to int. Error: invalid literal for int() with base 10: 'xxx'"}
```

当然，wsme还可以用来检测更为复杂的类型，如类对象，这个在下面再做介绍。

**四. wsme检测类对象：**

在openstack中，使用Rest API返回的响应值经常会是以下格式：

```json
{
    "users": [
        {
            "name": "Alice",
            "age": 30
        }, 
        {
            "name": "Bob", 
            "age": 40
        }
    ]
}
```

针对于这种情况，我们可以使用WSME的自定义类型，下面就定义一个`user`类型和`users`类型：

```python
from wsme import types as wtypes

class User(wtypes.Base):
    name = wtypes.text
    age = int

class Users(wtypes.Base):
    users = [User]
```

> 注意： 这里没有使用`__init__`是因为，父类的初始化方法参数为`**kw`，因此没有特殊需求，这里可以不写

现在可以使用如下的方法去调用这两个类型：

```python
from wsmeext.pecan import wsexpose
from pecan import rest

class RootController(rest.RestController):
    
	@wsexpose(User, int)		# 返回值为User类对象
    def get_one(self, id):
        if id == 1:
            user_info = {
            	'name': 'yangsijie', 
            	'age': 23
        	}
        return User(**user_info)
    	# 或者也可以使用下面这种方式：
        # if id == 1:
        #     return User(name='yangsijie', age=23)
    
    @wsexpose(Users)	# 返回值为Users类对象
    def get_all(self):
        user_info_list = [
            {
            	'name': 'yangsijie', 
            	'age': 23
        	},
        	{
            	'name': 'panna', 
            	'age': 23
        	}
        ]
        users_list = [User(**user_info) for user_info in user_info_list]
        return Users(users=users_list)
    	# 或者也可以使用下面这种方式：
        # return Users(users=[User(name='yangsijie', age=23), User(name='panna', age=23)])
```

现在使用curl命令会出现以下现象：

```bash
curl http://127.0.0.1:8081/1
# 返回值为：
{"age": 23, "name": "yangsijie"}

curl http://127.0.0.1:8081
# 返回值为：
{"users": [{"age": 23, "name": "yangsijie"}, {"age": 23, "name": "panna"}]}
```

WSME还可以用于检测上传的参数是否为complex type类型（由于上传要用到`POST`操作，所以此处就以POST作例子）：

```python
class RootController(rest.RestController):
    
    @wsexpose(None, body=User)	# 检查参数必须为User类对象
    def post(self, user):
        print user.name
        print user.age
```

当使用curl访问时，程序的控制台会打印出访问的值：

```bash
curl -X POST http://127.0.0.1:8081 -H "Content-Type: application/json" -d '{"name": "yangsijie", "age": 30}'

# 程序控制台显示的是：
yangsijie
30
```

这里需要注意的是，如果这里不写成`body=User`，而是直接写成`User`，那么curl中的data字段就也需要进行相应的修改，需要用以下命令来实现：==经测试，这种方法好像根本行不通????==

```bash
curl -X POST http://127.0.0.1:8081 -H "Content-Type: application/json" -d '{"user": {"name": "yangsijie", "age": 30}}'
```

当类中的属性没有传入值的时候，那么这个类变量是`wsme.types.UnsetType`对象：

```python
from wsme import types as wtyps

user = User(name='test1')	# 这里没有给user对象的age赋值

if user.age is wtypes.Unset:
    return True
```

结果会返回True，因为此处的age为`wsme.types.UnsetType`类型。

**五. 使用wsme设置status\_code**

使用wsme默认返回的状态码为200、400和500。

| 状态码 | 含义              |
| --- | --------------- |
| 200 | 成功              |
| 400 | 客户端输入出错（参数错误等等） |
| 500 | 服务器端错误          |

如果将之前的例子做略微的修改，可以让其返回的状态值不一样：

```python
from wsmeext.pecan import wsexpose
from pecan import rest

class RootController(rest.RestController):
    
    @wsexpose(int, int, status_code=201)	# 之前如果成功的话，返回的是200，现在将其改为201
    def get_one(self, arg):
        return 1
```

在现在这个例子中，使用如上一样的curl命令，会发现返回的状态码变成了`201`

但是在平时的使用中，我们肯定需要根据不同的判断，返回不同的status\_code，那怎么办呢？我们可以使用如下的方式去实现：

* 使用`wsme.api.Response`在返回值的同时指定status\_code
* 抛出`wsme.exc.ClientSideError`错误的同时，指定错误原因及status\_code
* 抛出`自定义错误`，并且在自定义错误中指定错误原因及status\_code

> 后面两种方式，是遇到错误的时候返回的

下面就看看例子中究竟是如何使用的：

```python
import wsme
from wsme import types as wtypes
from wsmeext.pecan import wsexpose
from pecan import rest


class BookNotFound(Exception):		# 自定义错误
    message = 'Book with ID={id} Not Found'		# 定义错误原因
    code = 404									# 定义status_code

    def __init__(self, id):
        message = self.message.format(id=id)
        super(BookNotFound, self).__init__(message)


class Book(wtypes.Base):
    id = int
    name = wtypes.text


class BookController(rest.RestController):
	@wsexpose(Books, int)
    def get_one(self, id):
        if id == 1:
            raise BookNotFound(id=id)	# 使用自定义错误返回
        elif id == 2:
            raise wsme.exc.ClientSideError('ID: \'1\' is wrong!!!', status_code=403)										# 使用wsme.exc.ClientSideError抛出错误
        else:
            return wsme.api.Response(Books(), status_code=204)																# 使用wsme.api.Response返回值
```

#### 案例：

该项目实现了用户的查找，添加和删除等（没有真正的结合数据库实现，只是一个demo）

项目的架构图如下：

```bash
webdemo2/
├── api
│   ├── app.py					# 存放WSGI application的入口
│   ├── config.py				# 存放Pecan的配置
│   ├── controllers				# 存放Pecan控制器的代码
│   │   ├── __init__.py
│   │   ├── root.py
│   │   └── v1
│   │       ├── controller.py
│   │       ├── __init__.py
│   │       └── users.py
│   ├── expose.py
│   └── __init__.py
├── cmd
│   ├── api.py
│   └── __init__.py
└── __init__.py
```

本项目实现了以下几个功能：

> GET /v1/users 获取所有用户的列表
>
> POST /v1/users 创建一个用户
>
> GET /v1/users/ 获取一个指定用户的详细信息
>
> PUT /v1/users/ 修改一个指定用户的详细信息
>
> DELETE /v1/users/ 删除一个指定用户
>
> POST /v1/users//kill 杀死一个指定用户

**api/app.py：**

```python
import pecan
from webdemo2.api import config as api_config


def get_pecan_config():
    filename = api_config.__file__.replace('.pyc', '.py')   # get the absolute path of the pecan config.py
    return pecan.configuration.conf_from_file(filename)


def setup_app():      # the main functhing, start listening
    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
```

**api/config.py:**

```python
app = {
    'root': 'webdemo2.api.controllers.root.RootController',
    'modules': ['webdemo2.api'],
    'debug': True,
}
```

**api/expose.py:**

```python
import wsmeext.pecan as wsme_pecan


def expose(*args, **kwargs):
    if 'rest_content_types' not in kwargs:
        kwargs['rest_content_types'] = ('json',)
    return wsme_pecan.wsexpose(*args, **kwargs)
```

> 该函数用来让API返回JSON格式的数据

**api/controllers/root.py:**

```python
from pecan import rest, expose
from wsme import types as wtypes
from webdemo2.api.controllers.v1 import controller as v1_controller
from webdemo2.api.expose import expose as wsexpose


class RootController(rest.RestController):

    # All supported API versions
    _versions = ['v1']

    # The default API version
    _default_version = 'v1'

    v1 = v1_controller.V1Controller()

    @wsexpose(wtypes.text)
    def get(self):
        return 'webdemo2'

    @expose()
    def _route(self, args, request=None):
        """When the API version is not specified in the url, v1 is used as the default version."""
        if args[0] and args[0] not in self._versions:
            args = [self._default_version] + args
        return super(RootController, self)._route(args)
```

> 当URL中未指定版本号时，`_route`函数将版本号默认置为v1

**api/controllers/v1/controller.py:**

```python
from pecan import rest
from wsme import types as wtypes
from webdemo2.api.expose import expose as wsexpose
from webdemo2.api.controllers.v1.users import UsersController


class V1Controller(rest.RestController):

    users = UsersController()

    @wsexpose(wtypes.text)
    def get(self):
        return 'webdemo2 v1controller'
```

**api/controllers/v1/users.py:**

```python
from wsme import types as wtypes
from pecan import rest, expose
from webdemo2.api.expose import expose as wsexpose


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


class Users(wtypes.Base):
    users = [User]


class UsersController(rest.RestController):

    # HTTP GET /users/
    @wsexpose(Users)
    def get(self):
        user_info_list = [
            {
                'id': '1',
                'name': 'Alice',
                'age': 30
            },
            {
                'id': '2',
                'name': 'Bob',
                'age': 40
            }
        ]
        users_list = [User(**user_info) for user_info in user_info_list]
        return Users(users=users_list)

    # HTTP POST /users
    @wsexpose(None, body=User, status_code=201)
    def post(self, user):
        print user

    @expose()
    def _lookup(self, user_id, *remainder):
        return UserController(user_id), remainder


class UserController(rest.RestController):

    _custom_actions = {
        'kill': ['POST']
    }

    def __init__(self, user_id):
        self.user_id = user_id

    # HTTP GET /users/123456/
    @wsexpose(User)
    def get(self):
        user_info = {
            'id': self.user_id,
            'name': 'Alice',
            'age': 30
        }
        return User(**user_info)

    # HTTP PUT /users/123456/
    @wsexpose(User, body=User)
    def put(self, user):
        user_info = {
            'id': self.user_id,
            'name': user.name,
            'age': user.age + 1
        }
        return User(**user_info)

    # HTTP DELETE /users/123456/
    @wsexpose()
    def delete(self):
        print ('Delete user_id: %s' % self.user_id)

    # HTTP POST /users/123456/kill
    @wsexpose(status_code=202)
    def kill(self):
        print ('Kill user_id: %s' % self.user_id)
```

**cmd/api.py:**

```python
from wsgiref import simple_server
from webdemo2.api import app


def main():
    application = app.setup_app()

    srv = simple_server.make_server('', 8081, application)
    print ('Server on port 8081, listening...')

    srv.serve_forever()

    
if __name__ == '__main__':
    main()
```

> 运行该文件，即可开始监听
