这部分的内容比较琐碎,有几处没有看懂,先记下来,完成后再转回来看看。
更新app.py:
# -*- coding: utf-8 -*-
import logging; logging.basicConfig(level=logging.INFO)
import asyncio, os, json, time
from datetime import datetime
from aiohttp import web
import orm
from jinja2 import Environment, FileSystemLoader
from coroweb import add_routes, add_static
def init_jinja2(app, **kw):
logging.info('Init jinja2...')
options = dict(
# 字典的get()方法返回指定键的值, 如果值不在字典中返回默认值
autoescape = kw.get('autoescape', True), # 自动转义
block_start_string = kw.get('block_start_string', '%{'),
block_end_string = kw.get('block_end_string','%}'),
variable_start_string = kw.get('variable_start_string', '{{'),
variable_end_string = kw.get('variable_end_string', '}}'),
auto_reload = kw.get('auto_reload', True) # 自动重新加载模板
)
path = kw.get('path', None)
if path is None:
# __file__获取当前执行脚本
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates')
logging.info('Set jinja2 template path: %s' % path)
# Environment(loader=PackageLoader('path'), 其他高级参数...)
# 创建一个默认设定下的模板环境和一个在path目录下寻找模板的加载器
env = Environment(loader=FileSystemLoader(path), **options)
filters = kw.get('filters', None)
if filters is not None:
for name, f in filters.items():
env.filters[name] = f
app['__templating__'] = env
# middlewar把通用的功能从每个URL处理函数中拿出来, 集中放到一个地方
# 接受一个app实例, 一个handler(URL处理函数, 如index), 并返回一个新的handler
async def logger_factory(app, handler):
async def logger(request):
logging.info('Request: %s %s' % (request.method, request.path))
return (await handler(request))
return logger
async def data_factory(app, handler):
async def parse_data(request):
if request.method == 'POST':
if request.content_type.startswith('application/json'):
request.__data__ = await request.json()
logging.info('Request json: %s' % str(request.__data__))
elif request.content_type.startswith('application/x-www-form-urlencoded'):
request.__data__ = await request.post()
logging.info('Request form: %s' % str(request.__data__))
return (await handler(request))
return parse_data
# 转化得到response对象的middleware
async def response_factory(app, handler):
async def response(request):
logging.info('Response handler...')
r = await handler(request)
# web.StreamResponse是HTTP响应处理的基类
# 包含用于设置HTTP响应头,Cookie,响应状态码,写入HTTP响应BODY等的方法
if isinstance(r, web.StreamResponse):
return r
if isinstance(r, bytes):
# 转换为web.Response对象
resp = web.Response(body=r)
# .*( 二进制流,不知道下载文件类型)
resp.content_type = 'application/octet-stream'
return resp
if isinstance(r, str):
if r.startswith('redirect:'):
return web.HTTPFound(r[9:])
resp = web.Response(body=r.encode('utf-8'))
# .html
resp.content_type = 'text/html;charset=utf-8'
return resp
if isinstance(r, dict):
template = r.get('__template__')
if template is None:
resp = web.Response(body=json.dumps(r, ensure_ascii=False, default=lambda o: o.__dict__).encode('utf-8'))
# 序列化后的JSON字符串
resp.content_type = 'application/json;charset=utf-8'
return resp
else:
# 调用get_template()方法环境中加载模板,调用render()方法用若干变量来渲染它
resp = web.Response(body=app['__templating__'].get_template(template).render(**r).encode('utf-8'))
resp.content_type = 'text/html;charset=utf-8'
return resp
# HTTP状态码和响应头部
if isinstance(r, int) and r >= 100 and r < 600:
return web.Response(r)
if isinstance(r, tuple) and len(r) == 3:
t, m = r
if isinstance(t, int) and t >= 100 and t < 600:
return web.Response(t, str(m))
resp = web.Response(body=str(r).encode('utf-8'))
# .txt
resp.content_type = 'text/plain;charset=utf-8'
return resp
return response
def datetime_filter(t):
# time.time()返回当前时间的时间戳(1970纪元后经过的浮点秒数)
delta = int(time.time() - t)
if delta < 60:
return u'1分钟前'
if delta < 3600:
return u'%s分钟前' % (delta // 60)
if delta < 86400:
return u'1小时前' % (delta // 3600)
if delta < 604800:
return u'%s天前' % (delta // 86400)
# 把timestamp转换为datetime
dt = datetime.fromtimestamp(t)
return u'%s年%s月%s日' % (dt.year, dt.month, dt.day)
# 参数aiohttp.web.request实例,包含了所有浏览器发送过来的HTTP协议里面的信息
# def index(request):
# return web.Response(body=b'Day01 of My WebAPP
') # 构造一个HTTP响应
# 协程,不能直接运行,需要把协程加入到事件循环(loop), 由后者在适当的时候调用
async def init(loop):
await orm.create_pool(loop=loop, host='192.168.179.140', port=3306, user='www', password='www', db='awesome')
# 创建Web服务器,即aiohttp.web.Application类的实例,作用是处理URL、HTTP协议
# 添加middleware时自动变成倒序
app = web.Application(loop=loop, middlewares=[logger_factory, response_factory])
init_jinja2(app, filters=dict(datetime=datetime_filter)) # 没有参数t?
# import handlers.py
add_routes(app, 'handlers')
add_static(app)
# 用协程创建TCP服务(这里写的是我的虚拟机地址,为了本机也能访问)
srv = await loop.create_server(app.make_handler(), '192.168.179.140', 9000)
logging.info('Server started at http://192.168.179.140:9000...')
return srv
# 创建一个事件循环
loop = asyncio.get_event_loop()
# 将协程注册到事件循环,并启动事件循环
loop.run_until_complete(init(loop))
# run_forever会一直运行,直到stop在协程中被调用
loop.run_forever()
新建apis.py:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
JSON API defination
'''
import json, logging, inspect, functools
# Base APIError, 包含error(必需), data和message
class APIError(Exception):
def __init__(self, error, data='', message=''):
super(APIError, self).__init__(message)
self.error = error
self.data = data
self.message = message
# 输入值有错或无效, data指定输入表单的错误字段。
class APIValueError(APIError):
def __init__(self, field, message=''):
super(APIValueError, self).__init__('value:invalid', field, message)
# 资源未找到, data指定资源名称。
class APIResourceNotFoundError(APIError):
def __init__(self, field, message=''):
super(APIResourceNotFoundError, self).__init__('vallue:notfound', field, message)
# API没有权限。
class APIPermissionError(APIError):
def __init__(self, message=''):
super(APIPermissionError, self).__init__('permission:forbidden', 'permission', message)
新建coroweb.py:
# -*- coding: utf-8 -*-
import asyncio, os, inspect, logging, functools
from urllib import parse
from aiohttp import web
from apis import APIError
# 装饰器, 在函数前面写上@get的话, 将执行装饰器返回的函数
# 作用是一个函数通过@get()的装饰就附带了URL信息
# 由于decorator本身需要传入参数, 需要编写一个返回decorator的高阶函数
def get(path):
'''
Define decorator @get('/path')
'''
def decorator(func):
# @functools.wraps(func)用来把原始函数的__name__等属性复制到decorator()函数中
@functools.wraps(func)
# (*args, **kw)表示wrapper()函数可以接受任意参数的调用
def wrapper(*args, **kw):
return func(*args, **kw)
wrapper.__method__ = 'GET'
wrapper.__route__ = path
return wrapper
return decorator
def post(path):
'''
Define decorator @post('/path')
'''
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
return func(*args, **kw)
wrapper.__method__ = 'POST'
wrapper.__route__ = path
return wrapper
return decorator
# 运用inspect模块,创建几个函数用以获取URL处理函数与request参数之间的关系
# 收集没有默认值的命名关键字参数
def get_required_kw_args(fn):
args = []
params = inspect.signature(fn).parameters
# 如果纯使用for..in则只能取得每一对元素的key值
for name, param in params.items():
# 只能用关键字KEYWORD来传参,不可以用位置传参,因为位置参数全让前面的VAR_POSITIONAL类型参数接收完了
if param.kind == inspect.Parameter.KEYWORD_ONLY and param.default == inspect.Parameter.empty:
args.append(name)
return tuple(args)
# 获取命名关键字参数
def get_named_kw_args(fn):
args = []
params = inspect.signature(fn).parameters
for name, param in params.items():
if param.kind == inspect.Parameter.KEYWORD_ONLY:
args.append(name)
return tuple(args)
# 判断有没有命名关键字参数
def has_named_kw_args(fn):
params = inspect.signature(fn).parameters
for name, param in params.items():
if param.kind == inspect.Parameter.KEYWORD_ONLY:
return True
# 判断有没有关键字参数
def has_var_kw_arg(fn):
params = inspect.signature(fn).parameters
for name, param in params.items():
if param.kind == inspect.Parameter.VAR_KEYWORD:
return True
# 判断是否含有名叫'request'参数,且该参数是否为最后一个参数
def has_request_arg(fn):
sig = inspect.signature(fn)
params = sig.parameters
found = False
for name, param in params.items():
if name == 'request':
found = True
continue
if found and (param.kind != inspect.Parameter.VAR_POSITIONAL and param.kind != inspect.Parameter.KEYWORD_ONLY and param.kind != inspect.Parameter.VAR_KEYWORD):
raise ValueError('request parameter must be the last named parameter in function: %s%s' % (fn.__name__, str(sig)))
return found
# 正式向request参数获取URL处理函数所需的参数
class RequestHandler(object):
# 接受app参数
def __init__(self, app, fn):
self._app = app
self._func = fn
self._has_request_arg = has_request_arg(fn)
self._has_var_kw_arg = has_var_kw_arg(fn)
self._has_named_kw_args = has_named_kw_args(fn)
self._named_kw_args = get_named_kw_args(fn)
self._required_kw_args = get_required_kw_args(fn)
async def __call__(self, request): # request?
kw = None
if self._has_var_kw_arg or self._has_named_kw_args or self._required_kw_args:
if request.method == 'POST':
if not request.contnt_type:
return web.HTTPBadRequest(text='Missing Content-Type.')
ct = request.content_type
if ct.startswith('application/json'):
params = await request.json() # .json()?
if not isinstance(params, dict):
return web.HTTPBadRequest(text='JSON body must be object.')
kw = params
# 键值对或表单
elif ct.startswith('application/x-www-form-urlencoded') or ct.startswith('multipart/form-data'):
params = await request.post() # .post()?
# 内置函数dict(**kwarg)从一个字典参数构造一个新字典
kw = dict(**params)
else:
return web.HTTPBadRequest(text='Unsupported Content-Type: %s'%request.content_type)
if request.method == 'GET':
qs = request.query_string
if qs:
kw = dict()
# urllib.parse.parse_qs(qs, keep_blank_values, strict_parsing=False)
# 解析qs给出的查询字符串(例如'foo=bar&baz=qux'), 数据作为字典返回,保留空白字符串。
for k, v in parse.parse_qs(qs, True).items():
kw[k] = v[0]
if kw is None:
kw = dict(**request.match_info)
else:
if not self._has_var_kw_arg and self._named_kw_args: # ?
# 去掉不是命名关键字参数的参数
copy = dict()
for name in self._named_kw_args:
if name in kw:
copy[name] = kw[name]
kw = copy
# 检查命名关键字参数
for k, v in request.match_info.items(): # ?
if k in kw:
logging.warning('Duplicate arg name in named arg and kw args: %s' % k)
kw[k] = v
if self._has_request_arg:
kw['request'] = request
# 检查没有默认值的命名关键字参数
if self._required_kw_args:
for name in self._required_kw_args:
if name not in kw:
return web.HTTPBadRequest(text='Missing argument: %s' % name)
logging.info('Call with arg: %s' % str(kw))
try:
r = await self._func(**kw)
return r
except APIError as e:
return dict(error=e.error, data=e.data, message=e.message)
# 添加静态资源路径
def add_static(app):
# os.path.abspath()返回绝对路径, os.path.dirname()返回path的目录名, os.path.join()将多个路径组合
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
# add_static(处理的静态资源的URL路径前缀, 文件系统中包含处理的静态资源的文件夹路径)
app.router.add_static('/static/', path)
logging.info('add static %s => %s' % ('/static/', path))
# 注册URL处理函数
def add_route(app, fn):
# getattr(self, key, default值)
method = getattr(fn, '__method__', None)
path = getattr(fn, '__route__', None)
if path is None or method is None:
raise ValueError('@get or @post not defined in %s.' % str(fn))
if not asynico.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn):
fn = asyncio.coroutine(fn)
# inspect.signature(fn)将返回一个inspect.Signature类型的对象, 值为fn这个函数的所有参数
# inspect.Signature对象的paramerters属性是一个mappingproxy(映射)类型的对象,值为一个有序字典(Orderdict)。
# 这个字典里的key即为参数名,str类型; value是一个inspect.Parameter类型的对象,包含的一个参数的各种信息
logging.info('add route %s %s => %s(%s)' % (method, path, fn.__name__, ', '.join(inspect.signature(fn).parameters.keys())))
app.router.add_route(method, path, RequestHandler(app, fn))
def add_routes(app, module_name):
# rfind(字符串)返回字符串最后一次出现的位置, 如果没有匹配项则返回-1
n = method_name.rfind('.')
if n == -1:
# __import__(name[, globals[, locals[, fromlist[, level]]]])函数用于动态加载类和函数
# globals()返回全局变量的字典, locals()返回当前局部变量的深拷贝(新建对象,不改变原值)
mod = __import__(module_name, globals(), locals())
else:
name = module_name[n+1:]
mod = getattr(__import__(module_name[:n], globals(), locals(), [name]), name)
# dir()函数可以查看对象内所有属性及方法
for attr in dir(mod):
if attr.startswith('_'):
continue
fn = getattr(mod, attr)
if callable(fn):
method = getattr(fn, '__method__', None)
path = getattr(fn, '__route__', None)
if method and path:
add_route(app, fn)