[python] pprika:基于werkzeug编写的web框架(7) ——restful的结构化与参数解析

Resource示例

from pprika import Api
from pprika import Resource

class Cat(Resource):
    def get(self):
        raise TypeError('这里没有猫')


v1 = Api('v1')
v1.add_resource(Cat, '/cats')

还记得上一篇错误处理的示例吗,与这段代码作用是一样的。可以看到,Resource的使用跟flask-restful的那个几乎一样,但pprika实现得特别精简,省略了一些东西,比如flask-restful中那个可以修改mime-type的representations

ps:flask-restful中关于这个Resource整得挺复杂,两个类多重继承,套来套去的,或许是有什么别的考虑吧...

 

pprika.restful.add_resource

该方法负责解析Resource对象,获取它所携带的路由信息,用处理好的参数调用add_url_rule,借此就可以通过Resource实现之前 api.route的功能。

    def add_resource(self, resource, path, **kwargs):
        endpoint = kwargs.pop('endpoint', None) or resource.__name__.lower()
        view_func = resource.as_view
        ...

其中这个as_view是Resource的一方法,作用是将参数转交给resource下各个函数处理,对外界来说它就是视图函数。

        for decorator in resource.decorators:
            view_func = decorator(view_func)
            ...

然后是用装饰器装饰函数,跟flask-restful一样。

        if not kwargs.get('methods'):
            kwargs['methods'] = resource.get_views()

        self.add_url_rule(path, endpoint, view_func, **kwargs)

而这个get_views会返回resource中的视图函数名(即methods),如本例中的get。

最后将处理好的参数交给add_url_rule,这是父类Blueprint的方法,最终还是会注册到app上的。

 

pprika.restful.Resource

class Resource(object):
    decorators = []

    @classmethod
    def get_views(cls):
        return list(filter(lambda m: '_' not in m and callable(getattr(cls, m)), dir(cls)))

decorators与get_views上述都提到了,其中get_views准确来说是会筛选出所有名称里不带下划线 _的方法(即排除保护、私有方法、单纯带下划线的、所有属性),因此原则上子类中不宜以非 http method 作为方法名,要的话就带上下划线。

 

    @classmethod
    def as_view(cls, *args, **kwargs):
        method = request.method.lower()

        func = getattr(cls, method, None)
        if func is None and method == 'head':
            func = getattr(cls, 'get', None)

        return func(cls(), *args, **kwargs)

然后是这个as_view,抓住核心:add_url_rule要且仅要一个函数作为view_func,但一个Resource中往往支持不止一种method,因此就需要一个代理来转发,那就是as_view。

可以看到无非就是根据method用getattr去获取resource的方法,方法为head时可以用get代替,最后注意到 func(cls(), *args, **kwargs) ,这是因为func是resource的方法,第一个参数需要为self,因此临时实例化一个给它。

ps:flask-restful中也是这么做的,不同的是它的as_view会返回一个函数,而且出于测试调试等目的,它会将类附加到该函数上,并支持类实例化时的参数传递

 

RequestParser示例

from pprika import RequestParser

reqparse = RequestParser()
reqparse.add_argument('age', type=str, required=True)


class Cat(Resource):
    def get(self):
        args = reqparse.parse_args()
        print(args.age)

(以Resource的例子为基础改的,略去了部分import与注册部分)

RequestParser在flask-restful中是很方便的一个类,它提供了参数的解析与验证功能,而pprika简单起见只复刻了部分功能,但即便如此这部分还是占了整个restful部分近三分之二...

本例中若请求中的参数name类型不是str或请求不包含该参数都会响应 http错误码400 BadRequest。

 

RequestParser.add_argument

    def add_argument(self, name, **kwargs):
        if isinstance(name, Argument):
            self.args.append(name)
        else:
            self.args.append(Argument(name, **kwargs))

        return self

参数name可以是请求中某个参数的名字,也可以是一个Argument对象,最后都是通过往RequestParser类的属性args中添加Argument对象。

若RequestParser在Resource.__init__内实例化,则视图函数内部进行add_argument操作所添加的参数解析规则仅适用于当前视图,这是因为每个请求到来Resource都会重新实例化一次,那么RequestParser对象也会重置。

ps:最后返回RequestParser实例本身是直接照抄flask-restful的,可能是为了链式调用...总感觉意义不大。

 

parse_args

def parse_args(self, req=request, strict=False, http_error_code=400):
        namespace = Namespace()
        errors = {}
        req.arg_keys = self.get_all_args(req) if strict else {}
        ...

第一个参数传入pprika.request或自定义的request都可,它就是被解析的包含请求信息的对象

其中Namespace是个增强的字典,它在字典的基础上实现了__getattr__与__setattr__,使之能够像对象获取属性一样获取键值,实际上可以省略,用字典dict代替。

errors用于之后记录出错信息

而get_all_args将以集合形式返回初始时,req对象中那些较有可能是客户端发送的,请求参数的键。它通过getattr对req循环获取,具体实现可参照源码,此处略过。

而这个req.arg_keys是为了实现strict的功能而设置的,原理后面提及,如果不需要strict可以无视它

http_error_code是后头参数解析错误时使用的响应码

 

        for arg in self.args:
            value, msg = arg.parse(req, self.bundle_errors)  # 若bundle_errors为False,异常将直接抛出

            if not isinstance(value, BaseException):
                namespace[arg.dest or arg.name] = value
            else:  # ValueError: value非法(如None),等其他异常
                errors.update(msg)
                ...

接下来会遍历之前添加的所有args(即那些个Argument对象),调用其上的parse方法完成每个参数单独的解析,其中bundle_errors决定该过程发生错误时是直接抛出错误还是将错误暂存等待所有args处理完毕再一起抛出

通过返回的value类型判断是否错误,若value是解析后的值则msg为None

 

        if errors:
            raise ApiException(status=http_error_code, message=errors)  # errors将以json响应
        if strict and req.arg_keys:
            msg = '未知参数: %s' % ', '.join(req.arg_keys)
            raise ApiException(status=400, message=msg)

        return namespace

errors有值说明错误需要bundle送出去的,否则早就直接抛出了

在右strict要求的前提下判断arg_keys是否有剩余,由于arg_keys在每次参数成功解析时都会抛出对应的键,因此若请求不含多余参数在解析完它应当是空集。

ps:flask-restful中关于strict的实现原理大致相同,但它通过一个source方法完成解析工作,返回的unparsed_arguments为字典形式

 

Argument类

一个该类对象代表了一个参数,或者说,对一个参数的要求,算是整个参数解析中的灵魂部分

class Argument(object):
    def __init__(self, name, dest=None, default=None, required=False,
                 type=str, location=('json', 'values',), nullable=True):
        self.name = name
        self.dest = dest
        self.default = default
        self.required = required
        self.type = type
        self.location = (location,) if isinstance(location, str) else location
        self.nullable = nullable

其中dest指定参数解析后在Namespace里对应的键名,相当于可以给参数换个名字;nullable表示允不允许请求中的参数值为Null(None)

与flask-restful不同,location一开始就处理成可迭代对象,方便后续操作

 

parse

    def parse(self, req, bundle_errors=False):
        values = []  # 同一个loc、多个loc都可能造成一键多值
        result = None  # 但仅返回所有合法值的最后一个

        for loc in self.location:
            value = getattr(req, loc, None)
            ...

values暂存req中关于某个键的所有值,正如注释所说,可能一键多值,因此以列表存储

result是最终的返回值,后解析的优先度高,这意味着同样是合法值,但loc靠后的将覆盖前面的。当然,非法的将被略过,除非一个合法的都没有

而此时value是一个存储了该位置所有参数的dict或MultiDict(似乎还可能是callable)

 

            if callable(value):
                value = value()
            if not value:  # 该location无任何参数
                continue

            if hasattr(value, "getlist"):  # 此时value为werkzeug.datastructures.MultiDict
                value = value.getlist(self.name)  # 返回列表
            else:
                value = value.get(self.name)
                value = [value] if value else []  # value为一般dict
                ...

就一件事:从value中拿到所需参数的值,并整理成列表

此时value变为了值的列表

 

            req.arg_keys.discard(self.name)
            # 将处理过的参数删去,便于 parse_args 判断请求是否带有多余参数
            values.extend(value)
            # value为None说明请求中该name对应的value就是None
            ...

然后就将成功解析的参数从arg_keys中删去,并将值合并入values

 

        for value in values:
            try:
                value = self.convert(value)
            except Exception as e:
                return self.handle_validation_error(e, bundle_errors)
            result = value or result
            ...

通过self.conver尝试将值转化为要求的type,若转化出错则交由handle_validation_error处理,这两个方法之后会稍微提及

 

        if not result and self.required:  # 必需参数缺失
            locations = [_friendly_location.get(loc, loc) for loc in self.location]
            msg = f"Missing in {' or '.join(locations)}"
            return self.handle_validation_error(KeyError(msg), bundle_errors)

        if not result:  # 非必需缺失,以默认值代替
            if callable(self.default):
                return self.default(), None
            else:
                return self.default, None

        return result, None  # None表示无错误信息

按照上面将value转化后可能会出现result为None,即不存在合法值的情况,那么会根据self.required分两个方案处理,抛出错误或以默认值代替。

而第2行中的_friendly_location照搬flask-restful,就是location的说明的字典,如 'args' 将得到  'the query string' ,可以略去

 

convert

    def convert(self, value):
        if value is None:
            if not self.nullable:
                raise ValueError('该参数不可为null')
            else:
                return None
        elif isinstance(value, FileStorage) and self.type == FileStorage:
            return value

        if self.type is Decimal:
            return self.type(str(value))
        else:
            return self.type(value)

 

判断值是否为None,依据nullable决定是否抛出错误,此处的错误将被parse中的try...except捕捉,不用担心bundle的问题

其中当类型为Filestorage、Decimal不可直接转化,需要特殊处理,其他的直接self.type强制转换

 

handle_validation_error

    def handle_validation_error(self, error, bundle_errors):
        msg = {self.name: str(error)}
        if bundle_errors:
            return error, msg
        raise ApiException(status=400, message=msg)

msg指明出错的参数及错误原因,再根据bundle_errors决定是否立即抛出错误

 

结语

这样restful的参数处理就完全实现了,同时pprika整个框架至此就介绍完毕,真是太好了...

而实际上原本还打算完成session、current_app等上下文,请求处理前后的钩子函数、简单包装sqlalchemy实现数据库模块...

在restful方面,还需要一个方便的登录验证...

开始写这些博客的时候确实没想着写这么多,但总觉得既然要解释原理那就得说清楚一些,越写越多,手痛...

 

posted @ 2020-05-31 21:42  NoNoe  阅读(191)  评论(0编辑  收藏  举报