python web开发实战(5)--编写web框架

1、目前项目用的aiohttp比较底层的框架,使用不太友好,把它封装使用,可以大大方便后续开发维护。其实主要就是封装了路径route和处理函数handler之间对应关系,不仅仅是简单的对应还有参数的对应等等。一个web application基本流程就是request-->route--->handler-->reponse,中间两块就是框架的启作用的地方。

2、在www文件夹下,添加coroweb.py文件,这里添加的就是框架的主体部分

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# software: PyCharm
import asyncio
import functools
import inspect
import logging
import os
from urllib import parse
from aiohttp import web
from apis import APIError


# get方法装饰器,
# 添加__method__和__route__属性
def get(path):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            return func(*args, **kw)
        wrapper.__method__ = 'GET'
        wrapper.__route__ = path
        return wrapper
    return decorator


# post方法装饰器
# 添加__method__和__route__属性
def 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


# signature类似java里的反射,获取函数或者类的信息,这里主要用参数信息
# 例:def func(a, b, *var, c, d, e=3, **kw)-->
# *var 可以是多个参数func(a, b, s1, s2, c, d, e=3, **kw)
# **kw 可以是多个参数func(a, b, *var, c, d, e=3, s1=1, s2=2)
# POSITIONAL可以理解为位置固定,*var由于可以多个参数所以位置不固定
# 参数类型:POSITIONAL_OR_KEYWORD,    参数名:a, 参数默认值:<class 'inspect._empty'>
# 参数类型:POSITIONAL_OR_KEYWORD,    参数名:b, 参数默认值:<class 'inspect._empty'>
# 参数类型:VAR_POSITIONAL,           参数名:var, 参数默认值:<class 'inspect._empty'>
# 参数类型:KEYWORD_ONLY,             参数名:c, 参数默认值:<class 'inspect._empty'>
# 参数类型:KEYWORD_ONLY,             参数名:d, 参数默认值:<class 'inspect._empty'>
# 参数类型:KEYWORD_ONLY,             参数名:e, 参数默认值:3
# 参数类型:VAR_KEYWORD,              参数名:kw, 参数默认值:<class 'inspect._empty'>


def get_required_kw_args(fn):
    args = []
    params = inspect.signature(fn).parameters
    for name, param in params.items():
        # 参数类型为命名关键字参数且没有指定默认值
        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)


# 判断fn有没有命名关键字参数,有的话就返回True
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


# 判断fn有没有关键字参数,有的话就返回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


def has_request_arg(fn):
    sig = inspect.signature(fn)
    params = sig.parameters
    found = False
    for name, param in params.items():
        # 找到参数名为request的参数后把found设置为True
        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


# 请求处理类
class RequestHandler(object):
    def __init__(self, app, fn):
        self._app = app
        self._func = fn
        # 这些函数以及后续的处理都是在判断route和handler两者的信息是否匹配,参数是否都匹配
        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):
        kw = None
        if self._has_var_kw_arg or self._has_named_kw_args or self._required_kw_args:
            # post request content_type参数校验
            if request.method == 'POST':
                if not request.content_type:
                    return web.HTTPBadRequest(text='Missing Context-Type.')
                ct = request.content_type.lower()
                if ct.startswith('appliction/json'):
                    params = await request.json()
                    if not isinstance(params, dict):
                        return web.HTTPBadRequest(text='JSON body must be object.')
                    kw = params
                elif ct.startswith('appliction/x-www-form-urlencoded') or ct.startswith('multipart/form-data'):
                    params = await request.post()
                    kw = dict(**params)
                else:
                    return web.HTTPBadRequest(text='Unsupported Content-Type: %s' % request.content_type)
            # get request参数校验
            if request.method == 'GET':
                # 例子:https://www.baidu.com/s?&wd=&ie=utf-8 ->
                # keep_blank_values,true值表示空白应保留为空白字符串{'wd': '', 'ie': 'utf-8'}
                qs = request.query_string
                if qs:
                    kw = dict()
                    for k, v in parse.parse_qs(qs, keep_blank_values=True).items():
                        kw[k] = v[0]
        if kw is None:
            # request的patch和handler的匹配信息match_info
            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:
                    # 把命名关键字都提取出来,存入copy这个dict
                    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:
            # 如果有request参数,就把这个参数存入kw
            kw['request'] = request
        if self._required_kw_args:
            # 如果有未指定默认值的命名关键字参数
            for name in self._required_kw_args:
                # 用for循环迭代
                if name not in kw:
                    # kw必须包含全部未指定默认值的命名关键字参数,如果发现遗漏则说明有参数没传入
                    return web.HTTPBadRequest(text='Missing argument: %s' % name)
        logging.info('call with args: %s' % str(kw))
        # 参数完全匹配调用处理函数handler进行处理
        try:
            r = await self._func(**kw)
            return r
        except APIError as e:
            return dict(error=e.error, data=e.data, message=e.message)


# 静态文件加载路径www/static,app.py的同级目录下的static文件夹
def add_static(app):
    path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
    app.router.add_static('/static/', path)
    logging.info('add static %s => %s' % ('/static/', path))


# 注册route,route-->handler联系起来
def add_route(app, fn):
    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 asyncio.iscoroutinefunction(fn) and not inspect.isgeneratorfunction(fn):
        # handler函数即不是一个协程也不是生成器,那就把函数变成一个协程
        fn = asyncio.coroutine(fn)
    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))


# 多个route与handler的对应
def add_routes(app, module_name):
    # 因为handlers模块在当前目录下,所以在app.py中传入的module_name是handlers
    # 假设handlers模块在handler目录下,那传入的module_name就是handler.handlers
    n = module_name.rfind('.')
    # 找出module_name中.的索引位置
    if n == (-1):
        # -1表示没有找到,说明模块在当前目录下,直接导入
        # __import__的作用类似import,import是为当前模块导入另一个模块,而__import__则是返回一个对象
        mod = __import__(module_name, globals(), locals())
    else:
        # 当module_name为handler.handlers时,[n+1:]就是取.后面的部分,也就是handlers
        name = module_name[n+1:]
        mod = getattr(__import__(module_name[:n], globals(), locals(), [name]), name)
    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)

3、同时添加www/apis.py问题,添加一些api异常类

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# software: PyCharm


class APIError(Exception):
    def __init__(self, error, data='', message=''):
        super(APIError, self).__init__(message)
        self.error = error
        self.data = data
        self.message = message


class APIValueError(APIError):
    def __init__(self, filed, message=''):
        super(APIValueError, self).__init__('value:invalid', filed, message)


class APIResourceNotFoundError(APIError):
    def __init__(self, filed, message=''):
        super(APIResourceNotFoundError, self).__init__('value:notfound', filed, message)


class APIPressionError(APIError):
    def __init__(self, message=''):
        super(APIPressionError, self).__init__('permission:forbidden', 'permission', message)

4、配置文件编写,所需参数直接读取,方便修改维护config.py读取default和override内容组合新的配置

www/config.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# software: PyCharm
import config_default
# 读取配置文件,优先从conffig_override.py读取


class Dict(dict):
    def __init__(self, names=(), values=(), **kw):
        super(Dict, self).__init__(**kw)
        # 建立键值对关系
        for k, v in zip(names, values):
            self[k] = v

    # 定义描述符,方便通过点标记法取值,即a.b
    def __getattr__(self, key):
        try:
            return self[key]
        except KeyError:
            raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

    # 定义描述符,方便通过点标记法设值,即a.b=c
    def __setattr__(self, key, value):
        self[key] = value


# 将默认配置文件与自定义配置文件进行混合
def merge(defaults, override):
    r = {}
    # 创建一个空的字典,用于配置文件的融合,而不对任意配置文件做修改
    # 1) 从默认配置文件取key,优先判断该key是否在自定义配置文件中有定义
    # 2) 若有,则判断value是否是字典,
    # 3) 若是字典,重复步骤1
    # 4) 不是字典的,则优先从自定义配置文件中取值,相当于覆盖默认配置文件
    for k, v in defaults.items():
        if k in override:
            if isinstance(v, dict):
                r[k] = merge(v, override[k])
            else:
                r[k] = override[k]
        # 当前key只在默认配置文件中有定义的, 则从其中取值设值
        else:
            r[k] = v
    return r  # 返回混合好的新字典


# 将内建字典转换成自定义字典类型
def toDict(d):
    D = Dict()
    for k, v in d.items():
        # 字典某项value仍是字典的(比如"db"),则将value的字典也转换成自定义字典类型
        D[k] = toDict(v) if isinstance(v, dict) else v
    return D


# 取得默认配置文件的配置信息
configs = config_default.configs

try:
    # 导入自定义配置文件,并将默认配置与自定义配置进行混合
    import config_override

    configs = merge(configs, config_override.configs)
except ImportError:
    pass

# 最后将混合好的配置字典专程自定义字典类型,方便取值与设值
configs = toDict(configs)

www/config_default.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# software: PyCharm

# 定义数据库相关信息
configs = {
    'db': {
        "host": "127.0.0.1",
        "port": 3306,
        "user": "root",
        "password": "xxxx",
        "db": "awesome"
        },
    # 定义会话信息
    "session": {
        "secret": "awesome"
        }
    }

www/config_override.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# software: PyCharm

# 重载的数据库信息,将会覆盖默认的数据库相关配置信息
configs = {
    "db": {
        "host": "127.0.0.1"
        }
    }

5、添加测试html页面

www/templates/test.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Test users - Awesome Python Webapp</title>
</head>
<body>
    <h1>All users</h1>
    {% for u in users %}
    <p>{{ u.name }} / {{ u.email }}</p>
    {% endfor %}
</body>
</html>

 

6、扩充www/app.py,加入jinja2的渲染模块等等

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# software: PyCharm
import logging
import asyncio
import os
import json
import time
import orm
from config import configs
from datetime import datetime
from aiohttp import web
from jinja2 import Environment, FileSystemLoader
from coroweb import add_routes, add_static
logging.basicConfig(level=logging.INFO)


# 初始化jinja2
def init_jinja2(app, **kw):
    logging.info("init jinja2...")
    # 定义页面插入代码的标识,代码块、变量
    options = dict(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)
                   )
    # 加载templates下的页面
    path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates') \
        if kw.get('path', None) is None else kw.get('path', None)
    logging.info('set jinja2 template path: %s' % 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


# 中间件,打印请求的方法和路径
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


# 只有当请求方法为POST的时候这个函数才起作用
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


# 对handler返回的response进行处理
async def response_factory(app, handler):
    async def response(request):
        logging.info('Request handler...')
        r = await handler(request)
        if isinstance(r, web.StreamResponse):
            return r
        if isinstance(r, bytes):
            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:])
            # 响应结果不是重定向,则以utf-8对字符串进行编码,作为body.设置相应的响应类型
            resp = web.Response(body=r.encode("utf-8"))
            resp.content_type = "text/html;charset=utf-8"
            return resp
        if isinstance(r, dict):
            template = r.get("__template__")
            # 若不存在对应模板,则将字典调整为json格式返回,并设置响应类型为json
            if template is None:
                resp = web.Response(
                    body=json.dumps(r, ensure_ascii=False, default=lambda o: o.__dict__).encode("utf-8"))
                resp.content_type = "application/json;charset=utf-8"
                return resp
            # 存在对应模板的,则将套用模板,用request handler的结果进行渲染
            else:
                resp = web.Response(body=app["__templating__"].get_template(template).render(**r).encode("utf-8"))
                resp.content_type = "text/html;charset=utf-8"
                return resp
        if isinstance(r, int) and 100 <= r < 600:
            return web.Response

        if isinstance(r, tuple) and len(r) == 2:
            t, m = r
            # t为http状态码,m为错误描述
            # 判断t是否满足100~600的条件
            if isinstance(t, int) and 100 <= t < 600:
                # 返回状态码与错误描述
                return web.Response(t, str(m))
            # 默认以字符串形式返回响应结果,设置类型为普通文本
        resp = web.Response(body=str(r).encode("utf-8"))
        resp.content_type = "text/plain;charset=utf-8"
        return resp
    return response


def datetime_filter(t):
    delta = int(time.time() - t)
    if delta < 60:
        return u'1分钟前'
    if delta < 3600:
        return u'%s分钟前' % (delta // 60)
    if delta < 86400:
        return u'%s小时前' % (delta // 3600)
    if delta < 604800:
        return u'%s天前' % (delta // 86400)
    dt = datetime.fromtimestamp(t)
    return u'%s年%s月%s日' % (dt.year, dt.month, dt.day)


async def init(loop):
    await orm.create_pool(loop=loop, **configs.db)
    app = web.Application(loop=loop, middlewares=[
        logger_factory, response_factory
    ])
    init_jinja2(app, filters=dict(datetime=datetime_filter))
    add_routes(app, 'handlers')
    add_static(app)
    runner = web.AppRunner(app)
    await runner.setup()
    srv = web.TCPSite(runner, '127.0.0.1', 9000)
    logging.info('server started at http://127.0.0.1:9000...')
    await srv.start()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(init(loop))
    loop.run_forever()

7、执行app.py,如数据库中有user数据,则有如下效果

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值