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方面,还需要一个方便的登录验证...
开始写这些博客的时候确实没想着写这么多,但总觉得既然要解释原理那就得说清楚一些,越写越多,手痛...