Python 异步框架---Sanic

Python 异步框架—Sanic

简介

Sanic 是 Python3.7+ Web 服务器和 Web 框架(Sanic 不仅仅是一个 框架,它还是一个 Web 服务器),旨在提高性能。它允许使用 Python3.5 中添加的 async/await 语法,这使得您的代码有效的避免阻塞从而达到提升响应速度的目的。

Sanic(包括Vibora,Vibora声称比其它框架快几倍,比竞争对手Sanic还快两倍多。)与flask有点类似,但有不同。

开始

1 新建项目

项目名称为sanic_pro

在该目录下新建python package

config:系统配置文件
server:服务
utils:其他工具

2 项目配置

在异步框架中,异步orm有很多,在sanic中,有SanicDB等,sanicDB是使用原生SQL语句,此列中,使用类似于django-orm框架的tortoise-orm,详细信息见 https://tortoise.github.io/

pip install sanic
pip install tortoise-orm[aiomysql]

config目录新建settings.py文件,该文件为该项目主要配置文件路径

**注:此列所用数据库为MariaDB,它是由MySQL衍生除的版本,具体请看官网介绍 **

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent

# ============================== ProjectConfig start ==============================
# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = ''
TITLE = 'recharge_platform'
# =============================== ProjectConfig end ===============================

# ================================== MySQL start ==================================
default_user = ''  # 数据库用户
default_host = ''  # 数据库ip
default_port = 3306   # 数据库端口
default_password = ''   # 数据库密码
default_database = ''   # 数据库
TORTOISE_ORM = {
    'connections': {
        'default': {
            'engine': 'tortoise.backends.mysql',
            'credentials': {
                'host': default_host,
                'port': default_port,
                'user': default_user,
                'password': default_password,
                'database': default_database,
            }
        },
    },
    'apps': {
        'default': {
            'models': [
                'aerich.models',
            ],
            'default_connection': 'default',
        },
    },
    'use_tz': False,
    'timezone': 'Asia/Shanghai'
}
# ================================== MySQL end ====================================

# =============================== RedisConfig start ===============================
REDIS_HOST = ''
REDIS_PORT = 6379
REDIS_PASSWD = ''
REDIS_SYS_DB = 1
REDIS_NAME = ''
REDIS = {
    'host': REDIS_HOST,
    'port': REDIS_PORT,
    'password': REDIS_PASSWD,
    'db': REDIS_SYS_DB
}
# ================================ RedisConfig end ================================

这里,我们使用aerich 对数据库进行管理

pip install aerich

创建迁移目录并初始化数据库链接

aerich init -t config.settings.TORTOISE_ORM

执行以上命令后 会在config同目录下生成一个migrations目录和aerich.ini文件,该文件目录用来存放数据库迁移历史记录

3 新建服务

config目录下新建base_model.py

from tortoise import models, fields


class DBBaseModel(models.Model):
    class Meta:
        abstract = True

    id = fields.IntField(pk=True, allows_generated=True)
    create_time = fields.DatetimeField(auto_now_add=True, null=True, description='创建时间')
    update_time = fields.DatetimeField(auto_now=True, null=True, description='更新时间')

server目录中创建 python package 并命名为app_system,在该目录新建models.py

from tortoise import fields
from config.base_model import DBBaseModel


class Zoning(DBBaseModel):
    class Meta:
        table = 'sys_zoning'
        table_description = '行政区划'

    code = fields.CharField(max_length=64, null=False, description='行政区划码')
    name = fields.CharField(max_length=64, null=False, description='行政区划名称')
    short = fields.CharField(max_length=32, null=True, default=None, description='行政简称')
    level = fields.IntField(null=False, default=-1, description='等级')
    area = fields.DecimalField(max_digits=16, decimal_places=4, default=0, description='面积')
    longitude = fields.DecimalField(max_digits=16, decimal_places=13, default=0, description='经度')
    latitude = fields.DecimalField(max_digits=16, decimal_places=13, default=0, description='纬度')
    coordinates = fields.TextField(null=True, description='坐标集合')
    serial = fields.IntField(null=True, default=None, description='序号')
    z_list = fields.TextField(null=True, description='地域层级结构')
    is_active = fields.SmallIntField(default=1, description='状态')
    parent = fields.ForeignKeyField('default.Zoning',
                                    null=True,
                                    on_delete=fields.SET_NULL,
                                    description='父结点')

    
class Projects(DBBaseModel):
    class Meta:
        table = 'sys_projects'
        table_description = '项目信息'

    name = fields.CharField(max_length=128, null=False, description='名称')
    app_key = fields.CharField(max_length=64, unique=True, null=False, description='项目key')
    describe = fields.TextField(null=True, description='项目简介')
    image = fields.TextField(null=True, description='项目图片')
    is_active = fields.SmallIntField(default=1, description='状态')
    zoning = fields.ForeignKeyField('default.Zoning',
                                    null=True,
                                    on_delete=fields.SET_NULL,
                                    description='行政区划')

class Account(DBBaseModel):
    class Meta:
        table = 'sys_account'
        table_description = '用户信息'

    username = fields.CharField(max_length=64, unique=True, null=False, description='用户名')
    password = fields.CharField(max_length=256, null=False, description='密码')
    telephone = fields.CharField(max_length=64, unique=True, null=False, description='手机号')
    real_name = fields.CharField(max_length=64, null=False, description='用户名称')
    is_superuser = fields.SmallIntField(default=1, description='是否是超管')
    is_active = fields.SmallIntField(default=1, description='状态')
    email = fields.CharField(max_length=64, null=True, default=None, description='邮箱')
    address = fields.CharField(max_length=256, null=True, default=None, description='联系地址')
    longitude = fields.DecimalField(max_digits=16, decimal_places=13, default=0, description='经度')
    latitude = fields.DecimalField(max_digits=16, decimal_places=13, default=0, description='纬度')
    z_list = fields.TextField(null=True, default=None, description='地域层级结构')
    project = fields.ForeignKeyField('default.Projects',
                                     null=True,
                                     on_delete=fields.SET_NULL,
                                     description='所属项目')
    zoning = fields.ForeignKeyField('default.Zoning',
                                    null=True,
                                    on_delete=fields.SET_NULL,
                                    description='行政区划')
...

修改settings.py文件

.....
'apps': {
        'default': {
            'models': [
                'aerich.models',
                'server.app_system.models',
            ],
            'default_connection': 'default',
        },
    },
....

第一次迁移使用以下命令进行数据库初始化

aerich init-db

执行完该命令,会在migrations生成default目录,该目录是默认数据库配置迁移版本文件,如果配置多数据库时,会创建相对应的目录。现在数据库中已经生成对应的数据表。

如果对数据表进行修改后,使用以下命令对数据库进行迁移

# 生成迁移文件
aerich migrate --name drop_column
# 执行迁移文件
aerich upgrade

system下新建views_area.py

from sanic import Blueprint
from server.app_system.models import Zoning
from utils.helper import return_result

area_router = Blueprint('行政区划', url_prefix='/area')


@area_router.get('/list')
async def area_list(request):
    parent_id = request.args.get('parent_id')
    code = request.args.get('code')
    name = request.args.get('name')
    page_size = int(request.args.get('page_size', '10'))
    page_number = int(request.args.get('page_number', '1'))
    offset = (page_number - 1) * page_size

    zoning_info = Zoning.filter()

    if parent_id:
        zoning_info = zoning_info.filter(id=parent_id)
    if code:
        zoning_info = zoning_info.filter(code__contains=code)
    if name:
        zoning_info = zoning_info.filter(name__contains=name)

    res = await zoning_info.select_related(
        'parent__name', 'parent__code'
    ).offset(offset).limit(page_size).values()

    count = await zoning_info.select_related('parent__name', 'parent__code').count()
    return return_result(data=res, count=count)

__init__.py

from sanic import Blueprint

from .views_area import area_router

auth_blue_group = Blueprint.group(auth_router)

sys_blue_group = Blueprint.group(
    area_router,
    url_prefix='/system'
)

在数据序列化时部分数据类型不支持序列化,因此对改部分数据需进行处理

utils下创建helper.py文件

import json as public_json

from decimal import Decimal
from datetime import datetime, date
from sanic.response import json as sanic_json


# 对不能序列化的数据进行处理
class DateEncoder(public_json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.strftime("%Y-%m-%d %H:%M:%S")
        elif isinstance(obj, date):
            return obj.strftime("%Y-%m-%d")
        elif isinstance(obj, Decimal):
            return str(Decimal(obj))
        else:
            return public_json.JSONEncoder.default(self, obj)


def data_json_str(data):
    if not isinstance(data, str):
        return public_json.dumps(data, cls=DateEncoder)
    else:
        return data


def data_json_dict(data):
    if isinstance(data, str):
        return public_json.loads(data)
    else:
        return data


def return_result(code: int = 200, msg: str = 'OK', data=None, count: int = 0, access_token: str = '', *args, **kwargs):
    if data is None:
        data = {}
    return sanic_json({
        'code': code,
        'msg': msg,
        'data': data_json_str(data),
        'count': count,
        'args': args,
        'access_token': access_token,
        'kwargs': kwargs
    })

server目录下的__init__.py文件添加一下内容

from config.settings import TORTOISE_ORM
from sanic import Sanic
from server.app_system import auth_blue_group, sys_blue_group

from tortoise.contrib.sanic import register_tortoise


app = Sanic(name='recharge_platform')

app.blueprint(sys_blue_group, url_prefix='/api')


# 数据库
register_tortoise(
    app=app,
    config=TORTOISE_ORM,
    generate_schemas=False
)

在项目根目录创建manager.py文件

from server import app

if __name__ == '__main__':
    app.run(port=1000, auto_reload=True, debug=False)

运行manager.py文件
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

FastAPI中,有这完善的api文档,Sanic借助sanic_openapi亦可实现

pip install sanic_openapi

修改serve.__init__.py文件

from config.settings import TORTOISE_ORM

from sanic_openapi import swagger_blueprint, openapi3_blueprint
from sanic import Sanic
from server.app_system import auth_blue_group, sys_blue_group

from tortoise.contrib.sanic import register_tortoise


app = Sanic(name='recharge_platform')

# 接口文档
# http://127.0.0.1:1000/swagger
app.blueprint(swagger_blueprint)
# app.blueprint(openapi3_blueprint)

app.blueprint(auth_blue_group)
app.blueprint(sys_blue_group, url_prefix='/api')


# 数据库
register_tortoise(
    app=app,
    config=TORTOISE_ORM,
    generate_schemas=False
)

修改app_system.views_area.py文件

注:这里需要注意参数类型

@area_router.get('/list')
@doc.summary('获取行政区划列表')
@doc.consumes({'page_number': '1'}, {'page_size': '10'}, location='query', required=True, )
@doc.consumes({'parent_id': int}, {'name': str}, {'code': str}, location='query')
async def area_list(request):
    parent_id = request.args.get('parent_id')
    code = request.args.get('code')
    name = request.args.get('name')
    page_size = int(request.args.get('page_size', '10'))
    page_number = int(request.args.get('page_number', '1'))
    offset = (page_number - 1) * page_size

    zoning_info = Zoning.filter()

    if parent_id:
        zoning_info = zoning_info.filter(id=parent_id)
    if code:
        zoning_info = zoning_info.filter(code__contains=code)
    if name:
        zoning_info = zoning_info.filter(name__contains=name)

    res = await zoning_info.select_related(
        'parent__name', 'parent__code'
    ).offset(offset).limit(page_size).values()

    count = await zoning_info.select_related('parent__name', 'parent__code').count()
    return return_result(data=res, count=count)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4 鉴权

sanic对接口鉴权(登录验证)方式有很多,详情见 https://github.com/mekicha/awesome-sanic/blob/master/README.md#authentication

config下新建account_conf.py文件

import hashlib
import jwt
from config.settings import SECRET_KEY, TITLE
from config.database_pool.redis_pool import RedisPool
from functools import wraps
from utils.helper import return_result


# 生成密码
def make_password(password):
    pass_str = password + hashlib.new('sha256', SECRET_KEY.encode()).hexdigest()
    encryption = hashlib.new('md5', pass_str.encode()).hexdigest()
    return encryption


# 验证密码
def check_password(login_pass, user_pass):
    encryption = make_password(login_pass)
    if encryption == user_pass:
        return True
    else:
        return False


# 生成token
def create_token(user_name, user_id):
    payload = {
        'sub': user_id,
        'user_name': user_name
    }
    herder = {
        'key': SECRET_KEY,
        'title': TITLE,
    }
    token = jwt.encode(
        payload=payload,
        key=SECRET_KEY,
        headers=herder,
        algorithm='HS256'
    )
    return token


# 验证token
def check_token(token, user_id):
    if not user_id or not token:
        return False
    try:
        payload_data = jwt.decode(
            jwt=token,
            key=SECRET_KEY,
            algorithms=['HS256']
        )
        if payload_data.get('sub') != user_id:
            return False
        else:
            return True
    except jwt.exceptions.InvalidTokenError:
        return False


# 接口认证
def protected(wrapped):
    def decorator(func):
        @wraps(func)
        async def decorated_function(request, *args, **kwargs):
            try:
                token = request.ctx.token
                user_id = request.ctx.user.get('id')
                is_authenticated = check_token(user_id=user_id, token=token)
                if is_authenticated:
                    _redis = RedisPool(db=0)
                    user_token = await _redis.get_redis_info(_key=str(user_id))
                    if token == user_token:
                        response = await func(request, *args, **kwargs)
                        return response
                    else:
                        return return_result(code=401, msg='用户登录已过期,请重新登录')
                else:
                    return return_result(code=401, msg='用户登录已过期,请重新登录')
            except Exception as e:
                return return_result(code=401, msg='用户还未登录')

        return decorated_function

    return decorator(wrapped)


if __name__ == '__main__':
    _token = create_token('user', 1)
    data = jwt.decode(
        jwt=_token,
        key=SECRET_KEY,
        algorithms=['HS256']
    )
    print(data, type(data))

修改app_system.views_area.py文件

@area_router.get('/list')
@doc.summary('获取行政区划列表')
@doc.consumes({'page_number': '1'}, {'page_size': '10'}, location='query', required=True, )
@doc.consumes({'parent_id': int}, {'name': str}, {'code': str}, location='query')
@protected
async def area_list(request):
    parent_id = request.args.get('parent_id')
    code = request.args.get('code')
    name = request.args.get('name')
    page_size = int(request.args.get('page_size', '10'))
    page_number = int(request.args.get('page_number', '1'))
    offset = (page_number - 1) * page_size

    zoning_info = Zoning.filter()

    if parent_id:
        zoning_info = zoning_info.filter(id=parent_id)
    if code:
        zoning_info = zoning_info.filter(code__contains=code)
    if name:
        zoning_info = zoning_info.filter(name__contains=name)

    res = await zoning_info.select_related(
        'parent__name', 'parent__code'
    ).offset(offset).limit(page_size).values()

    count = await zoning_info.select_related('parent__name', 'parent__code').count()
    return return_result(data=res, count=count)

在这里插入图片描述

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值