✨基于 Bottle 框架开发API 之 从零到简单封装到应用到上线部署(4)-开始撸码(长文)

1,952 阅读1小时+

逼逼叨叨

关于我自己: 纯粹的是打杂一个,似乎啥活都干过,小公司里搬砖久了,似乎啥都懂一些,所以博文有些地方估计有纰漏,还请各位大侠,多多担待! 这一系列的文字算不上是文章,反倒像我个人的日常的开发笔记,难免也有些事我自己的个人想法,如有什么问题或错误欢迎各位指出,如有打扰的地方,也希望各位多多海涵。

ps: 如文章有打扰之处,还请多多原谅!我也只是想记录一下而已没别的!

回望过去

上一期系列提到了关于本博文bottle相关的封装的起源,如有意可以回顾看看:

前言

上一小节梳理完了我们的代码结构后,我们就可以针对我们的代码结构进行撸码了!

撸码的开始

对于一个基础库而言,我们希望它能做到通用,被其他项目当做基础来调用,那就是需要我们把一些项目常用的东西进行归档整理到指定地方。

1 、application as BasciApplication的封装

BasciApplication的作用不言而喻,它就是整个应用的需要启动的实例对象,在Bottle里面,它可以让我们自己创建实例也可以使用默认的default_app()的实例.

如启动示例1:

# 创建应用实例
# app = Bottle(autojson=False)

如启动示例2:

# 创建应用实例
# app = default_app()

再这里,我是直接的使用了它默认的default_app()来创建我们的基础App示例.

1.1 安装Bottle和Beaker依赖库

pycharm--->file--->settings---> 点击蓝色处

点击加号--->输入bottle 和 搜索 Beaker 安装:

1.2 开始application的封装

在我们的core的application下的__init__开始撸码:

#!/usr/bin/evn python
# coding=utf-8
from bottle import Bottle, default_app, run, response, request, hook, route, install,get, error, HTTPError, HTTPResponse, TEMPLATE_PATH,static_file
from beaker.middleware import SessionMiddleware

def create_default_app_application():
    # 配置Session管理
    # 配置相关的session的服务对象
    session_opts = {
        'session.type': 'file',
        'session.cookie_expires': 3600,
        'session.data_dir': '/tmp/sessions/order',
        'session.auto': True
    }
    # 创建应用实例
    # app = Bottle(autojson=False)
    # 使用默认的实例
    application = SessionMiddleware(default_app(), session_opts)
    return application

关于 beaker主要是用于我们的应用对Session的管理,相关的文档,我们可以参考beaker的官方文档!

在app_demo 下 新建一个app.py文件,通过引入的方式来使用App实例对象,使用AppDemo启动示例程序

from bottle import run,route
from core.basicapp import create_default_app_application


@route('/nihao/<name>')
def index(name):
    return '你好 %s '% (name)

if __name__ == '__main__':
    app = create_default_app_application()
    run(app=app, debug=True, host="127.0.0.1", port=8889, reloader=True)

关于启动示例中的参数含义说明:

run(app=app, debug=True, host="127.0.0.1", port=8889, reloader=True)

  • app - 只当前的wsgi实例对象
  • debug - 是否开启debug输出日志
  • host - 是指我再本地的那个地址上
  • port - 端口号
  • reloader - 是否开启自动重启(修改代码后自动的重启)

启动程序访问相关的路由地址:

http://127.0.0.1:8889/nihao/df 返回: 你好 df

以上就是简单的包含了Session的处理的示例。

(此小节完)

2 、参数校验

通常我们的一个对外的API肯定是需要用户提交相关的参数来访问我们的接口,但是对于一个服务来说,外置输入永远是不可靠的, 这时候我们就需要对用户提交的参数信息进行校验,比如提交的字段是比传的字段,比如的这个字段的长度不能超过多少数字等····

关于参数的校验库其实还是挺多的,我这个列举几个:

暂时列举这么多吧,其实很多的库大同小异,都是对我们的参数进行常规的校验处理。

2.1 引入wtforms校验参数

安装库:

pip install wtforms

然后我们在core基础库封装处理下,写一个BasicForms,便于后续的其他Form继承,这里需要注意的是,我们获取的相关用户提交的参数,这里直接使用了request来获取,因为request本身它是一个LocalReqeust对象,所以我们这里可以直接导入。

具体的代码如下:

ffrom wtforms import Form as WTForm, IntegerField
from wtforms.validators import StopValidation

from functools import wraps
from bottle import request,abort

class BaseForm(WTForm):
  def __init__(self):
      data = request.json
      args = request.params
      if not args:
          args = request.forms
      # 自定义新得对象,不过滤其他需要校验得参数 get_json 这个函数默认情况下只对 mime 为 application/json 的请求可以正确解析。
      #  使用 request.get_json(force=True) 忽略mimetype
      super(BaseForm, self).__init__(data=data, **args)

  def validate_for_api(self):
      valid = super(BaseForm, self).validate()
      if not valid:
          error_first = ''
          for field in self.errors:
              error_first = self.errors[field][0]
              break
          # 直接抛出异常,交给全局异常处理
          abort(400, error_first)
      # if not valid:
      #     if isinstance(self.errors, dict):
      #         print(type(self.errors))
      #         print(self.errors)
      # raise ParameterException(msg=self.errors)
      return self

封装好我们基础Form之后我, 我们就可以通过继承方式来实现我们自定义的校验。

我们在demo示例下建立一个专门的包,用于定义相关的表单:

代码如下:


   from core.forms import BaseForm
   from wtforms import DateTimeField, PasswordField, FieldList, IntegerField, StringField
   from wtforms.validators import DataRequired, Regexp, EqualTo, length, Optional, NumberRange

   class LoginForm(BaseForm):
       username = StringField(validators=[DataRequired(message='username 必须传入')])
       password = StringField(validators=[DataRequired(message='password 必须传入')])

   class SysPermissionForm(BaseForm):
       token = StringField(validators=[DataRequired(message='token 必须传入')])

之后我们再我们的相关的路由之前进行表单的引入和使用。 我们,再user.py路由下进行演示:


from bottle import route
from ..validators import LoginForm

@route('/user/action/',method=['POST', 'GET', 'OPTIONS'])
def index():
   #  直接的输出图片流
   form = LoginForm().validate_for_api()
   print(form)
   return '我是用户接口'

然后我们使用POSTMAN来访问一下接口看看具体的情况: 没提交相关的参数的情况下: 再没有提交相关的LoginForm需要提交的相关的参数下,我们报了400错误!说明校验已经按我们的预想的结果执行了!

此时我们按校验规则约定,进行提交一下相关的参数后,发现我们程序正常校验通过了:

get提交

POST提交

2.2 使用装饰器形式进行校验

通过使用表单验证器,我们很快的就可以验证我们的用户提交的参数,这个感觉还是比较方便的! 此时我们的还有一个疑问,我们可以使用装饰器来进行改造一下我们的校验吗,每次引入的时候 直接使用装饰器就可以验证,不需要每次再我们的路由中这样进行校验:

   form = LoginForm().validate_for_api()

那肯定是可以的啦,在我们的基础库中定义一下装饰器,新增两个方法:

完整的代码如下:

from wtforms import Form as WTForm, IntegerField
from wtforms.validators import StopValidation

from functools import wraps
from bottle import request,abort
from core.rsp import CustomizeApiResponse

class BaseForm(WTForm):
   def __init__(self):
       data = request.json
       args = request.params
       print(request.is_xhr)
       print(request.is_ajax)
       if not args:
           args = request.forms
       # 自定义新得对象,不过滤其他需要校验得参数 get_json 这个函数默认情况下只对 mime 为 application/json 的请求可以正确解析。
       #  使用 request.get_json(force=True) 忽略mimetype
       super(BaseForm, self).__init__(data=data, **args)

   def validate_for_api(self):
       valid = super(BaseForm, self).validate()
       if not valid:
           error_first = ''
           for field in self.errors:
               error_first = self.errors[field][0]
               break
           # 直接抛出异常,交给全局异常处理
           # return CustomizeApiResponse(msg=error_first)
           # raise  CustomizeApiResponse(msg=error_first)
           abort(900, error_first)
       return self

from functools import wraps

def validate_form(form_cls):
   '''
   传入的是当前From对象
   :param form_cls:
   :return:
   '''
   def decorator(fn):
       @wraps(fn)
       def wrapper(*args, **kwargs):
           form = form_cls().validate_for_api()
           return fn(*args, **kwargs)
       return wrapper
   return decorator

def validate_back_form(form_cls):
   '''
     传入的是当前From对象
   :param form_cls:
   :return: 返回当前的表单对象
   '''
   def decorator(fn):
       @wraps(fn)
       def wrapper(*args, **kwargs):
           form = form_cls().validate_for_api()
           return fn(form, *args, **kwargs)
       return wrapper
   return decorator

然后我们再改造一下我们的接口请求:


from bottle import route
from app_demo.validators import LoginForm,validate_back_form

@route('/user/action/',method=['POST', 'GET', 'OPTIONS'])
@validate_back_form(LoginForm)
def index(form):
   print(form.username.data)
   return '我是用户接口'

再测试,一样能完美的处理,也开始用装饰器来进行参数校验了!!!

PS:关于带参的装饰器,切返回数据的装饰器的传递的时候,需要注意,我们通过装饰器返回的参数如:

def decorator(fn):
      @wraps(fn)
      def wrapper(*args, **kwargs):
          form = form_cls().validate_for_api()
          return fn(form, *args, **kwargs)
      return wrapper
  return decorator

它返回了一个form对象实例,这样我们就不需要多次初始化,但是在传递返回到我们的函数的时候,需要注意,必须把返回的参数放在第一个位置哟!

3 、API请求的日志记录

关于应用的日志重要性不言而喻,可以帮我们定位到系统运行情况,和排查各种错误的信息。再python中有自带的日志logging模块,虽然感觉logging是足够满足我们当前系统日志统计相关的信息,但是使用起来似乎还是不太方便多少,所以我这里使用是其他第三方的日志库来处理,当然这个库也是比较知名的,loguru。

因为loguru的使用也非常的简便,如果其实也可以不必要封装的再基础库core中去集成。但是为了让基础库似乎丰满一些,我这里也把它独立的放置再基础库中。

3.1 安装logur

pip install loguru 

关于loguru的使用 可以看牛逼崔神的文章:cuiqingcai.com/7776.html

3.2 集成到当前应用

在我们的core下对应的logger下__init__开始编写代码:


import time
from bottle import request,response
from datetime import datetime
from loguru import logger

def creat_customize_log_loguru(pro_path=None):
    # 多个记录分别记录
    import os
    if not pro_path:
        # BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
        pro_path = os.path.split(os.path.realpath(__file__))[0]
    # 定义info_log文件名称
    log_file_path = os.path.join(pro_path, 'log/info_{time:YYYYMMDD}.log')
    # 定义err_log文件名称
    err_log_file_path = os.path.join(pro_path, 'log/error_{time:YYYYMMDD}.log')
    # 错误日志不需要压缩
    format = " {time:YYYY-MM-DD HH:mm:ss:SSS} | process_id:{process.id} process_name:{process.name} | thread_id:{thread.id} thread_name:{thread.name} | {level} |\n {message}"
    # enqueue=True表示 开启异步写入
    # 使用 rotation 参数实现定时创建 log 文件,可以实现每天 0 点新创建一个 log 文件输出了
    logger.add(err_log_file_path, format=format, rotation='00:00', encoding='utf-8', level='ERROR', enqueue=True)  # Automatically rotate too big file
    # 对应不同的格式
    format2 = " {time:YYYY-MM-DD HH:mm:ss:SSS} | process_id:{process.id} process_name:{process.name} | thread_id:{thread.id} thread_name:{thread.name} | {level} | {message}"
    # 记录的充值通知的日志
    # enqueue=True表示 开启异步写入
    # 使用 rotation 参数实现定时创建 log 文件,可以实现每天 0 点新创建一个 log 文件输出了
    logger.add(log_file_path, format=format2, rotation='00:00', compression="zip", encoding='utf-8', level='INFO', enqueue=True)  # Automatically rotate too big file

上述的代码说明:

我们新建了两个日志对象,一个用户记录错误的日志,一个用户记录常规的INFO 两个的日志的记录的格式是:

format = " {time:YYYY-MM-DD HH:mm:ss:SSS} | process_id:{process.id} process_name:{process.name} | thread_id:{thread.id} thread_name:{thread.name} | {level} |\n {message}"

format2 = " {time:YYYY-MM-DD HH:mm:ss:SSS} | process_id:{process.id} process_name:{process.name} | thread_id:{thread.id} thread_name:{thread.name} | {level} | {message}"

然后我们开始再我们之前appdemo示例里引入调用一下:

import os
from bottle import run, route
from core.basicapp import create_default_app_application
from core.config import config, register_init_config
from core.logger import logger,creat_customize_log_loguru


@route('/nihao/<name>')
def index(name):
    return '你好 %s ' % (name)



def curr_creat_customize_log_loguru():
    '''
    配置日志路径和配置信息--定义到当前项目目录内进行创建log文件
    :return:
    '''
    creat_customize_log_loguru(pro_path=os.path.split(os.path.realpath(__file__))[0])

def test_customize_log_loguru_witer():
    '''
    配置日志路径和配置信息--定义到当前项目目录内进行创建log文件
    :return:
    '''
    logger.info('测试日志写入')



if __name__ == '__main__':
   
    # 生产的log文件存在路径,并写入测试日志文件信息
    curr_creat_customize_log_loguru()
    # 测试写入日志
    test_customize_log_loguru_witer()

    # 创建应用的实例
    app = create_default_app_application()
    # 运行App实例
    run(app=app, debug=True, host="127.0.0.1", port=8889, reloader=True)

运行之后,我们会看到我的项目文件结构下自动生产了一个log日志文件夹:

对应的我们查看一下info 日志文件内容:

 2020-03-11 17:53:07:591 | process_id:20032 process_name:MainProcess | thread_id:17404 thread_name:MainThread | INFO | 测试日志写入
 2020-03-11 17:53:08:378 | process_id:21804 process_name:MainProcess | thread_id:18304 thread_name:MainThread | INFO | 测试日志写入

说明,我们的日志记录已经没什么问题,那我们此时试一试,记录一下异常错误的信息看看。

修改我们的app.py


def test_customize_error_log_loguru_witer():
    '''
    配置日志路径和配置信息--定义到当前项目目录内进行创建log文件
    :return:
    '''
    try:
        45/0
    except Exception as e:
        logger.error(e)

if __name__ == '__main__':
    # 读取配置信息处理
    curr_register_init_config()
    # 测试打印读取的配置信息
    test_read_config()

    # 生产的log文件存在路径,并写入测试日志文件信息
    curr_creat_customize_log_loguru()
    # 测试写入日志
    test_customize_log_loguru_witer()
    # 尝试写入错误的日志
    test_customize_error_log_loguru_witer()

    # 创建应用的实例
    app = create_default_app_application()
    # 运行App实例
    run(app=app, debug=True, host="127.0.0.1", port=8889, reloader=True)

然后我们的查看一下我们的error_20200311文件信息似乎记录的信息不够详细:

 2020-03-11 17:57:23:730 | process_id:16612 process_name:MainProcess | thread_id:21804 thread_name:MainThread | ERROR |
 division by zero
 2020-03-11 17:57:23:973 | process_id:14172 process_name:MainProcess | thread_id:15792 thread_name:MainThread | ERROR |
 division by zero

我们把logger.error(e)修改为:logger.exception(e)

再查看我们的错误内容信息:

 2020-03-11 17:57:23:730 | process_id:16612 process_name:MainProcess | thread_id:21804 thread_name:MainThread | ERROR |
 division by zero
 2020-03-11 17:57:23:973 | process_id:14172 process_name:MainProcess | thread_id:15792 thread_name:MainThread | ERROR |
 division by zero
 2020-03-11 18:05:06:132 | process_id:2976 process_name:MainProcess | thread_id:6652 thread_name:MainThread | ERROR |
 An error has been caught in function '<module>', process 'MainProcess' (2976), thread 'MainThread' (6652):
Traceback (most recent call last):

> File "D:/code/python/local_python/ZBotte/app_demo/app.py", line 132, in <module>
    test_customize_error_log_loguru_witer()
    └ <function test_customize_error_log_loguru_witer at 0x0000022C2B203950>

  File "D:/code/python/local_python/ZBotte/app_demo/app.py", line 118, in test_customize_error_log_loguru_witer
    45/0

ZeroDivisionError: division by zero
 2020-03-11 18:05:06:881 | process_id:18308 process_name:MainProcess | thread_id:19860 thread_name:MainThread | ERROR |
 An error has been caught in function '<module>', process 'MainProcess' (18308), thread 'MainThread' (19860):
Traceback (most recent call last):

> File "D:/code/python/local_python/ZBotte/app_demo/app.py", line 132, in <module>
    test_customize_error_log_loguru_witer()
    └ <function test_customize_error_log_loguru_witer at 0x000001A2CDED3950>

  File "D:/code/python/local_python/ZBotte/app_demo/app.py", line 118, in test_customize_error_log_loguru_witer
    45/0

ZeroDivisionError: division by zero

其实之处的错误位置停详尽的。所以个人觉得此库还是挺不错的咧!

(此小节完)

4 、全局错误异常中间件

在之前,我们再定义我们的基础校验数据的时候,我们的使用 abort(400,error_first)主要的抛出的我们的400异常之后,我们会看到我们的异常信息打印的是一堆的HTML信息,但是我们的接口API开发中,一般我会统一规范返回固定的JSON格式的提示。

再统一规范之前,我们需要处理就是,如果捕获到我们的相关的系统异常的信息呐?

之前我们介绍过可以使用系统自带的error装饰器来捕获异常@!

error(code)这个装饰decorator可以捕捉到具体Http错误码,将错误处理程序安装到当前默认应用程序。 比如:


@error(404)
def callback():
    print('拦截http404错误处理')

@error(500)
def callback():
    print('拦截http500错误处理')

@error(403)
def callback():
    print('拦截http403错误处理')

@error(405)
def callback():
    print('拦截http405错误处理')

如不需要分散开来处理相关的异常,全局进行统一的全局的异常捕获也是可以的,而且后期我们的对日志请求和错误日志请求捕获也是非常有益的,所以我们的还是需要做一个全局异常处理。

分析了bottle源码后,其实我们的可以,使用插件的方式来定义我们的全局错误处理异常。我们再我们的基础库core的exceptions下定义了一个插件

class ErrorsRestPlugin():
    name = 'ErrorsRestPlugin'
    api = 2
    def __init__(self, dumps=None,error_handler=None):
        self.json_dumps = dumps
        self.error_handler=error_handler

    def setup(self, app):
        def default_error_handler(res):
            return self.error_handler(res)
        app.default_error_handler = default_error_handler

    # def apply(self, callback, route):
    #     return callback
    def apply(self, callback, route):
        def wrapper(*a, **ka):
            try:
                rv = callback(*a, **ka)
                return rv
            except Exception as e:
                return self.error_handler(e)
        return wrapper

然后我们的再我们的app.py里面注册一下我们的插件,并且启用一下。

代码如下:

import os
from bottle import run, route,HTTPError,install
from core.basicapp import create_default_app_application
from core.config import config, register_init_config
from core.logger import logger,creat_customize_log_loguru
from core.exceptions import ErrorsRestPlugin


@route('/nihao/<name>')
def index(name):
    return '你好 %s ' % (name)

def curr_creat_customize_log_loguru():
    '''
    配置日志路径和配置信息--定义到当前项目目录内进行创建log文件
    :return:
    '''
    creat_customize_log_loguru(pro_path=os.path.split(os.path.realpath(__file__))[0])

def test_customize_log_loguru_witer():
    '''
    配置日志路径和配置信息--定义到当前项目目录内进行创建log文件
    :return:
    '''
    logger.info('测试日志写入')


from app_demo import api

def register_global_error_catch(e, is_send_to_wx=True):
    """
      注册全局异常捕获
    """
    if isinstance(e, HTTPError):
        code = e.status_code
        # 处理参数校验异常的情况
        if code == 400:
            # 参数校验异常
            # return ParameterException(code=code, msg=e.body)
            print('400-参数校验异常:',e.body)
        elif code == 401:
            # 请求不允许
            # return UnauthorizedException()
            print('401-请求不允许')
        elif code == 403:
            # 访问权限受限
            # return ForbiddenException()
            print('403-访问权限受限')
        elif code == 404:
            # 找不到此访问地址
            # return NotfoundException()
            print('404-找不到此访问地址')
        elif code == 405:
            # 请求方式异常
            # return MethodnotallowedException()
            print('405-请求方式异常')
        elif code == 429:
            # 限流异常
            # return RateLimitApiException()
            print('429-限流异常')
        else:
            # 判断异常类型
            # if isinstance(e, RateLimitException):
            # 429异常
            # return RateLimitApiException()
            # return OtherException(code=code)
            print('其他异常')
    else:
        # 如果是Debug
        # 打印错误日志,并且记录到日志中
        print('500-系统崩溃异常')


if __name__ == '__main__':
    # 读取配置信息处理
    curr_register_init_config()
    # 测试打印读取的配置信息
    test_read_config()

    # 生产的log文件存在路径,并写入测试日志文件信息
    curr_creat_customize_log_loguru()

    # 配置全局异常
    # install(GlobalHTTPError(back_callback=register_global_error_catch))
    install(ErrorsRestPlugin(error_handler=register_global_error_catch))

    # 创建应用的实例
    app = create_default_app_application()
    # 运行App实例
    run(app=app, debug=True, host="127.0.0.1", port=8889, reloader=True)

我们再访问一下我们的之前的接口看看,不提交相关的参数:

从上面的打印输出结果看,我们的全局错误拦截是以及能正常捕获到系统异常信息了。

下面就是我们的我们的错误信息进行统一规范返回格式进行封装一下。

(此小节完)

5 、规范API接口JSON格式

5.1 初步定义错误响应体

上节中提到在异常错误捕获到需要统一返回JSON格式给前端,这里我们不得不说怎么样统一得格式JSON才是比较规范得讷?我们我们可以查阅一切别人得提供得好得示例,比如微信:

从上面可以看得出,定制自己的业务错误码,在实际开发是非常有意义得: 错误码,错误类型,错误说明 业务开发过程中,我们可以根据错误类型是判断是哪种哪种类型以便做相应的逻辑处理,通过定制的错误码很容易就可以定位到问题点了!

于是乎我们得需要统一规范我们得返回得JSON格式信息且定制自己得错误码:

首先统一我们得JSON格式规范:

这里我们就涉及到一个问题,自定义我们自己的响应HTTPResponse,这样我们就可以直接的返回给用户响应体信息,但是注意一点,如果我们使用了我们自定义的HTTPResponse,就会覆盖我们bottle内置的response。

注:如果自定义了Response对象的话,则会覆盖全局实例中的当前线程 bottle.response 。

相关的跨域请求设置,我们就会出现失效的问题。

我们先看看怎么自定义我们的响应体:

新增一个rsp模块和一个工具类模块:

然后我们的再rsp下定义我们的响应体:

其中:

msg是错误类型

code是是HTTP错误响应码

error_code看情况可以选择性不需要,大部分的情况我们的返回我们的自定义的错误类型的时候,系统响应码都是再200的情况下响应的,所以我们的可以忽略,也可以定制一个通用的响应体信息。

from bottle import HTTPResponse, response, request
from ..helper import json_helper

class _BasicResponse(HTTPResponse):

    msg = '抱歉,服务器未知错误'
    error_code = 999
    code = 500

    # 默认支持跨域
    customize_headers = {
        'Access-Control-Allow-Origin': '*'
    }
    def __init__(self, status=code, body=None,**options):
        if not body:
            body = dict(
                msg=self.msg,
                code=self.code,
                error_code=self.error_code,
            )
        # 设置返回响应体信息
        self.body = json_helper.dict_to_json_ensure_ascii_indent(body)
        if self.customize_headers:
            super(_BasicResponse, self).__init__(body=self.body, headers=self.customize_headers, status=status, **options)
        else:
            # 默认的允许进行跨域请求处理
            self.headers['Access-Control-Allow-Origin'] = '*'
            self.headers['Access-Control-Allow-Credentials'] = 'true'
            self.headers['Access-Control-Allow-Origin'] = '*'
            self.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,OPTIONS,DEL'
            self.headers['Access-Control-Allow-Headers'] = "*"
            super(_BasicResponse, self).__init__(body=self.body, headers=self.headers, status=status, **options)





class ApiResponse(_BasicResponse):
    # 定义返回响应码--如果不指定的话则默认都是返回200
    code = 200
    error_code = 0
    data = None  # 结果可以是{} 或 []
    msg = '成功'

    def __init__(self,  data=None,error_code=None,msg=None, **options):

        if error_code:
            self.error_code = error_code
        if data:
            self.data = data
        if msg:
            self.msg = msg
            
        # 返回内容体
        body = dict(
            msg=self.msg,
            code=self.code,
            data=self.data,
            error_code=self.error_code,
        )
        if not self.data:
            del body['data']

        if not self.error_code:
            del body['error_code']

        response.content_boby_text = body
        #
        self.body = body
        # 返回的数据格式
        # self.body = json_helper.dict_to_json(body)
        # 注意两者的有区别--返回自身定义的--返回传入的code响应码
        super(ApiResponse, self).__init__(status=self.code,body=self.body, **options)
        # 返回父类定义--返回200的响应码
        # super(ApiResponse, self).__init__(status=code, body=body, **options)



class BadrequestException(ApiResponse):
    code = 400
    msg = '错误的请求'
    error_code = 10031

class ParameterException(ApiResponse):
    code = 400
    msg = '参数校验错误'
    error_code = 10031

class UnauthorizedException(ApiResponse):
    code = 401
    msg = '未经许可授权'
    error_code = 10032

class ForbiddenException(ApiResponse):
    code = 403
    msg = '当前访问没有权限'
    error_code = 10033

class NotfoundException(ApiResponse):
    code = 404
    msg = '访问地址不存在'
    error_code = 10034

class MethodnotallowedException(ApiResponse):
    code = 405
    msg = '不允许使用此方法提交访问'
    error_code = 10034

class OtherException(ApiResponse):
    code = 800
    msg = '未知的其他HTTPEOOER异常'
    error_code = 10034


class InternalErrorException(ApiResponse):
    code = 500
    data = None  # 结果可以是{} 或 []
    # msg = 'Internal Server Error'
    msg = ' 服务崩溃异常'

class RateLimitApiException(ApiResponse):
    code = 429
    data = None  # 结果可以是{} 或 []
    # msg = 'Internal Server Error'
    msg = '请求次数受限'

定义好我们的自定义的响应体后,我们回归到之前一定的全局错误拦截处理,再那地方我们使用我们的自定义的响应体返回错误信息。

具体代码如下:

from app_demo import api
from core.rsp import *
def register_global_error_catch(e, is_send_to_wx=True):
    """
      注册全局异常捕获
    """
    if isinstance(e, HTTPError):
        code = e.status_code
        # 处理参数校验异常的情况
        if code == 400:
            # 参数校验异常
            return ParameterException(code=code, msg=e.body)
        elif code == 401:
            # 请求不允许
            return UnauthorizedException()
        elif code == 403:
            # 访问权限受限
            return ForbiddenException()
        elif code == 404:
            # 找不到此访问地址
            return NotfoundException()
        elif code == 405:
            # 请求方式异常
            return MethodnotallowedException()
        elif code == 429:
            # 限流异常
            return RateLimitApiException()
        else:
            # 判断异常类型
            # if isinstance(e, RateLimitException):
            # 429异常
            # return RateLimitApiException()
            return OtherException(code=code)
    else:
        # 如果是Debug
        # 打印错误日志,并且记录到日志中
        # logger.error(e)
        logger.exception(e)
        # if isinstance(e,RateLimitException):
        # 429异常
        # return RateLimitApiException()
        return InternalErrorException()

此时我们再运行一下我们的接口:

定义自己的错误码,返回的时候统一HTTP响应码是200: 在rsp下新增一个通用类型的:

class CustomizeApiResponse(ApiResponse):
    code = 200
    data = None  # 结果可以是{} 或 []
    # msg = 'Internal Server Error'
    msg = '成功'

然后我们修改一下,当HTTP响应码是500的时候,返回修改为200响应码且是自己定义的信息:

from core.rsp import *
def register_global_error_catch(e, is_send_to_wx=True):
    """
      注册全局异常捕获
    """
    if isinstance(e, HTTPError):
        code = e.status_code
        # 处理参数校验异常的情况
        if code == 400:
            # 参数校验异常
            print('e.body',e.body)
            return ParameterException(code=code, msg=e.body)
        elif code == 401:
            # 请求不允许
            return UnauthorizedException()
        elif code == 403:
            # 访问权限受限
            return ForbiddenException()
        elif code == 404:
            # 找不到此访问地址
            return NotfoundException()
        elif code == 405:
            # 请求方式异常
            return MethodnotallowedException()
        elif code == 429:
            # 限流异常
            return RateLimitApiException()
        else:
            # 判断异常类型
            # if isinstance(e, RateLimitException):
            # 429异常
            # return RateLimitApiException()
            return OtherException(code=code)
    else:
        # 如果是Debug
        # 打印错误日志,并且记录到日志中
        # logger.error(e)
        logger.exception(e)
        # if isinstance(e,RateLimitException):
        # 429异常
        # return RateLimitApiException()

        return CustomizeApiResponse(msg='系统崩溃了,程序员哥哥肯定是没睡好!')

再主动触发生成一个触发500bug,

5.2 完善规范API接口业务响应码

在上面的定义中,我们定义的code是对应的HTTP的响应码,这种情况其实对我们自定义的业务响应码似乎不太友好,这里还是有必要的再次完善一下。 所以需要重新修改我们的rsp 和新增最基础的自定义响应码,在这里我直接使用枚举的方式来定意思我们的业务响应码信息,具体示例如代码示:


from bottle import HTTPResponse, response, request
from ..helper import json_helper

class _BasicResponse(HTTPResponse):

    msg = '抱歉,服务器未知错误'
    http_status_code = 500
    code = 500

    # 默认支持跨域
    customize_headers = {
        'Access-Control-Allow-Origin': '*'
    }
    def __init__(self,code = code, status=http_status_code, body=None,**options):
        if not body:
            body = dict(
                msg=self.msg,
                code=self.code,
            )
        # 设置返回响应体信息
        self.body = json_helper.dict_to_json_ensure_ascii_indent(body)
        if self.customize_headers:
            super(_BasicResponse, self).__init__(body=self.body, headers=self.customize_headers, status=status, **options)
        else:
            # 默认的允许进行跨域请求处理
            self.headers['Access-Control-Allow-Origin'] = '*'
            self.headers['Access-Control-Allow-Credentials'] = 'true'
            self.headers['Access-Control-Allow-Origin'] = '*'
            self.headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,OPTIONS,DEL'
            self.headers['Access-Control-Allow-Headers'] = "*"
            super(_BasicResponse, self).__init__(body=self.body, headers=self.headers, status=status, **options)


from core.logger import register_link_end_log_record_handler


class ApiResponse(_BasicResponse):
    # 定义返回响应码--如果不指定的话则默认都是返回200
    http_status_code = 200
    # 默认成功
    code = 0
    data = None  # 结果可以是{} 或 []
    msg = '成功'

    def __init__(self,  data=None,msg=None, **options):


        if data:
            self.data = data
        if msg:
            self.msg = msg

        # 返回内容体
        body = dict(
            msg=self.msg,
            code=self.code,
            data=self.data,
        )

        # if not self.data:
        #     del body['data']


        # 注意这里是把返回消息体写入,用于日志的记录
        self.body = body
        response.content_boby_text = body
        # 返回的数据格式
        # self.body = json_helper.dict_to_json(body)
        # 请求完成最终的日志处理
        # print(2)
        if not self.code in [405, 404]:
            register_link_end_log_record_handler()
        # 注意两者的有区别--返回自身定义的--返回传入的code响应码
        super(ApiResponse, self).__init__(status=self.http_status_code,code=self.code,body=self.body, **options)
        # 返回父类定义--返回200的响应码
        # super(ApiResponse, self).__init__(status=code, body=body, **options)





class BadrequestException(ApiResponse):
    http_status_code = 400
    #  error_code = 10032
    code = 10032
    msg = '错误的请求'


class ParameterException(ApiResponse):
    http_status_code = 400
    code = 400
    msg = '参数校验错误'

class UnauthorizedException(ApiResponse):
    http_status_code = 401
    code = 401
    msg = '未经许可授权'


class ForbiddenException(ApiResponse):
    http_status_code = 403
    code = 403
    msg = '当前访问没有权限'


class NotfoundException(ApiResponse):
    http_status_code = 404
    code = 404
    msg = '访问地址不存在'


class MethodnotallowedException(ApiResponse):
    http_status_code = 405
    code = 405
    msg = '不支持使用此方法提交访问'


class OtherException(ApiResponse):
    http_status_code = 800
    code = 800
    msg = '未知的其他HTTPEOOER异常'
    error_code = 10034


class InternalErrorException(ApiResponse):
    http_status_code = 500
    code = 500
    data = None  # 结果可以是{} 或 []
    # msg = 'Internal Server Error'
    msg = ' 服务崩溃异常'

class RateLimitApiException(ApiResponse):
    http_status_code = 429
    code = 429
    data = None  # 结果可以是{} 或 []
    # msg = 'Internal Server Error'
    msg = '请求次数受限'


class CustomizeApiResponse(ApiResponse):
    http_status_code = 200
    code = 200
    data = None  # 结果可以是{} 或 []
    # msg = 'Internal Server Error'
    msg = '成功'


class CustomizeParameterException(ApiResponse):
    http_status_code = 200
    code = 200
    msg = '参数校验错误'
    error_code = 10031


# 定义基础的
from enum import Enum, unique


@unique
class BasicStatus(Enum):
    OK = (0, '成功')
    FAIL = (-1, '未知异常')
    # 1 错误类型================================================
    PARAM_IS_NULL = (1000,"请求参数异常")
    # 决绝方案:method参数不能为空,必须传入API接口名称参数。对于下单接口,调整参数后,可重新下单,也可失败订单。
    PARAM_ILLEGAL = (1001, "无效的API接口名称")
    # 决绝方案:timestamp参数不能为空,必须传入时间戳参数。对于下单接口,调整参数后,可重新下单,也可失败订单。
    PARAM_IS_NULL_TIME = (1002, "必须传入时间戳")
    # 解决方案:XXXXXXXXXXXXXXXXX
    SQL_ERROR = (1003, "数据库异常")
    NETWORK_ERROR = (1004, "网络异常")
    UNKNOWN_ERROR = (1005, "未知异常")


    # 2 错误类型 ================================================
    # 解决方案:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    # APP_PARAM_IS_NULL = (2000: "无效的商户或应用")


    def get_code(self):
        """
        根据枚举名称取状态码code
        :return: 状态码code
        """
        return self.value[0]

    def get_msg(self):
        """
        根据枚举名称取状态说明message
        :return: 状态说明message
        """
        return str(self.value[1])


class CustomizeEnumType200ApiResponse(ApiResponse):
    enmu_msg = None
    def __init__(self, enmu_msg:Enum = None,msg =None,**options):
        if enmu_msg:
            self.enmu_msg = enmu_msg
            self.http_status_code = 200
            self.code = enmu_msg.get_code()
            self.msg = enmu_msg.get_msg().join('-'+ msg) if msg else enmu_msg.get_msg()
        super(CustomizeEnumType200ApiResponse, self).__init__(**options)

分析我们上面的CustomizeEnumType200ApiResponse,意思就是当我返回这个响应体信息的时候,不伦是什么情况,我的响应码信息都是200,且可以传入自定义的业务错误码,此时 我们再调用返回的时候,就可以直接的使用 CustomizeEnumType200ApiResponse来返回我们的内部自定义业务码了!

@route('/user/check',method=['POST', 'GET', 'OPTIONS'])
# @validate_back_form(SysPermissionForm)
def index():
    # 34564/0
    # 用户登入
    # return CustomizeApiResponse(msg='认证通过了!')
    return CustomizeEnumType200ApiResponse(enmu_msg=BasicStatus.PARAM_IS_NULL_TIME)

6 、完善的API请求日志记录

6.1 统计详尽的请求信息

上面几个流程下来,基本我们一个APi请求是已经完成了,但是我们的怎么记录到用户提交的相关的信息,我们怎么把用户提交的信息写入到日志中,在前面我们已封装了日志记录模块。 关于日志,我们一般需要统计的有哪些方面呐?我个人的话,主要是看业务情况,有些时候我们的接口也涉及到使用其他第三方的请求URL,这时候我们一般也需要记录一下请求第三方的地址的结果和响应。

所以我这里觉得有必要的几个记录维度是:

  • 用户提交的参数信息和提交的响应头信息或提交方法等
  • 在API请求过程中出现的异常信息
  • 在API请求第三方接口的时候请求和响应信息
  • 完成此请求接口的返回给前端响应体信息

所以这里我们就需要加入增加更加详尽的日志记录。


# 封装一下关于记录序号的日志记录用于全链路的日志请求的日志
def _link_init_log_record():
    '''
    用于在request添加自定义的字段信息用于记录一个请求的上下文的信息
    :return:
    '''
    request.request_links_logs = []
    request.request_links_index = 0


# 封装一下关于记录序号的日志记录用于全链路的日志请求的日志
def link_add_log_record(event_des='',msg_dict={},remarks=''):
    '''

    :param event_des: 日志记录事件描述
    :param msg_dict: 日志记录信息字典
    :param remarks: 日志备注信息
    :return:
    '''
    request.request_links_index = request.request_links_index + 1
    log = {
        'link_index': request.request_links_index,
        'event_des': event_des,
        'msg_dict': msg_dict,
        'remarks': remarks
    }
    if not remarks:
        log.pop('remarks')
    if not msg_dict:
        log.pop('msg_dict')

    request.request_links_logs.append(log)


def register_link_init_log_record_handler():
    '''
    过滤指定一些路由的请求直接得不做相关的请求日志记录处理
    :return:
    '''
    path_info = request.environ.get("PATH_INFO")
    # 如果指定的相关的不做记录的路由处理
    if path_info not in ['/favicon.ico', 'health']:
        response.content_boby_text = None
        # 配置日志初始化
        _link_init_log_record()
        # 计算时间
        request.request_start_time = time.time()
        # 开始记录日志
        link_add_log_record(event_des='request-start')


def register_link_end_log_record_handler():
    '''
    一个请求结束的时候,我们需要记录下这次请求的整个过程和返回响应体信息
    :return:
    '''
    # 日志记录
    path_info = request.environ.get("PATH_INFO")
    if path_info not in ['/favicon.ico', '/health']:
        if request.method != 'OPTIONS':
            link_add_log_record(event_des='request-end')
            # print('request.headers',request.headers.get('Host'))
            # 统筹记录最后的请求日志信息
            log_msg = {
                # 'headers': str(request.headers),
                # 'user_agent': str(request.headers.),
                'headers': str(json_helper.dict_to_json_ensure_ascii_indent(request.headers)),
                'host': request.headers.get('Host'),
                'ip': http_helper.get_ip(),
                'url': request.url,
                'method': request.method,
                # 'path': request.path,
                'params': {
                    'query_string':  '' if not request.query_string else http_helper.do_unquote(request.query_string),
                    'query': '' if not request.query else request.query.decode("utf-8"),
                    'forms': '' if not request.forms else request.forms,
                    'body': '' if not request.body else request.body,
                },
                'req_stime': str(datetime.fromtimestamp(request.request_start_time)),
                # 'req_start_time': datetime.fromtimestamp(g.request_start_time).isoformat(),
                # 'req_time': str(datetime.now())
                'req_links_logs': request.request_links_logs
            }
            try:
                req_json_data = request.json
                if req_json_data:
                    log_msg['params']['req_json_data'] = req_json_data
            except:
                pass

            try:
                log_msg['rsp_data'] = response.content_boby_text
            except:
                resp_json_data = json_helper.json_to_dict(response.content_boby_text)
                log_msg['rsp_data'] = resp_json_data

            # 去除请求拼的限制的日志请求记录
            try:
                if log_msg['rsp_data'].get('code') == 429 or len(log_msg['req_links_logs']) == 2:
                    if log_msg['req_links_logs']:
                        log_msg.pop('req_links_logs')
                # 删除默认的记录
            except:
                pass

            # 计算请求完成消耗的时间--保留两位小数点
            log_msg['cost_time'] = str((float("%.3f" % (time.time() - request.request_start_time)) * 1000) + 5) + ''
            # 单行输出
            log_msg = json_helper.dict_to_json_ensure_ascii(log_msg)  # 返回文本
            logger.info(log_msg)

加上上面的一下日志的文档信息后,我们就可以直接再示例App里面的钩子函数进行调用和处理了。

首先我们的知道一个请求进来的时候,我们需要首先初始化一个事件记录的ID索引,所以我们需要初始化一下日志记录信息:

如:


from core.logger import logger,creat_customize_log_loguru,register_link_init_log_record_handler,register_link_end_log_record_handler

@hook('before_request')
def before_request():
    pass
    # # 让bottle框架支持jquery ajax的RESTful风格的PUT和DELETE等请求
    REQUEST_METHOD = request.environ.get('REQUEST_METHOD')
    HTTP_ACCESS_CONTROL_REQUEST_METHOD = request.environ.get('HTTP_ACCESS_CONTROL_REQUEST_METHOD')
    if REQUEST_METHOD == 'OPTIONS' and HTTP_ACCESS_CONTROL_REQUEST_METHOD:
        request.environ['REQUEST_METHOD'] = HTTP_ACCESS_CONTROL_REQUEST_METHOD


    # 忽略路由尾部的'/'斜杠,统一不需要多个路由来处理
    request.environ['PATH_INFO'] = request.environ['PATH_INFO'].rstrip('/')

    # 初始化日志记录
    register_link_init_log_record_handler()



if __name__ == '__main__':
    # 读取配置信息处理
    curr_register_init_config()
    # 测试打印读取的配置信息
    test_read_config()

    # 生产的log文件存在路径,并写入测试日志文件信息
    curr_creat_customize_log_loguru()

    # 配置全局异常
    # install(GlobalHTTPError(back_callback=register_global_error_catch))
    install(ErrorsRestPlugin(error_handler=register_global_error_catch))

    # 创建应用的实例
    app = create_default_app_application()
    # 运行App实例
    run(app=app, debug=True, host="127.0.0.1", port=8889, reloader=True)


因为我们的自定义了响应体信息,如果我们需要统计到用户响应体的内容的话,那么我们在定义的响应地方进行日志结尾记录处理

所以我们还需要修改rsp下的响应信息的记录没在返回信息之前记录下日志:


class ApiResponse(_BasicResponse):
    # 定义返回响应码--如果不指定的话则默认都是返回200
    code = 200
    error_code = 0
    data = None  # 结果可以是{} 或 []
    msg = '成功'

    def __init__(self,  data=None,error_code=None,msg=None, **options):

        if error_code:
            self.error_code = error_code
        if data:
            self.data = data
        if msg:
            self.msg = msg

        # 返回内容体
        body = dict(
            msg=self.msg,
            code=self.code,
            data=self.data,
            error_code=self.error_code,
        )
        if not self.data:
            del body['data']

        if not self.error_code:
            del body['error_code']


        # 注意这里是把返回消息体写入,用于日志的记录

        #
        self.body = body
        response.content_boby_text = body
        # 返回的数据格式
        # self.body = json_helper.dict_to_json(body)
        # 请求完成最终的日志处理
        # print(2)
        if not self.code in [405, 404]:
            register_link_end_log_record_handler()
        # 注意两者的有区别--返回自身定义的--返回传入的code响应码
        super(ApiResponse, self).__init__(status=self.code,body=self.body, **options)
        # 返回父类定义--返回200的响应码
        # super(ApiResponse, self).__init__(status=code, body=body, **options)

注意关键代码点在于:

if not self.code in [405, 404]:
            register_link_end_log_record_handler()

然后运行我们的程序,随意请求一个地址,则可以查看对于的日志记录信息如下:

查看日志文件最后的内容是:

{
    "headers":"{
    "Content-Length": "0",
    "Content-Type": "text/plain",
    "User-Agent": "PostmanRuntime/7.21.0",
    "Accept": "*/*",
    "Cache-Control": "no-cache",
    "Postman-Token": "dc9d570b-185d-45b3-9025-f0f92ea9fd0d",
    "Host": "127.0.0.1:8889",
    "Accept-Encoding": "gzip, deflate",
    "Connection": "keep-alive"
}",
    "host":"127.0.0.1:8889",
    "ip":"127.0.0.1",
    "url":"http://127.0.0.1:8889/user/action",
    "method":"POST",
    "params":{
        "query_string":"",
        "query":"",
        "forms":""
    },
    "req_stime":"2020-03-12 15:44:23.632648",
    "rsp_data":{
        "msg":"系统崩溃了,程序员哥哥肯定是没睡好!",
        "code":200
    },
    "cost_time":"29.0"
}

经过上面之后,我们就可以统计到一个请求的过程返回给用户的一些东西,都记录到我们的日志中了。

6.2 统计API中包含其他第三方请求API的日志

当我们的API涉及第三方的API请求我们也需要记录下来,那我们就需要封装一个简单的HTTP请求方法,那我们需要再core基础库下的HTTP,来处理一下我们的相关的封装,首选需要安装requests:

pip install requests

燃煤我封装一个HTTP类:

import requests
import urllib3
import json
from core.logger import link_add_log_record
from core.helper.datetime_helper import get_datetime_heng
# 禁用安全请求警告 py3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# 导入信号处理库

from ..helper.json_helper import dict_to_json_ensure_ascii_indent

class Http:

    @staticmethod
    def post(url=None, params=None, headers=None, params_is_json=False, isback_json=True, verify=False, timeout=15,is_log = True,is_send_wx_exceptions =False):
        """向指定接口发送Post请求"""
        result = {}
        r = ''
        remarks = ''
        request_ok = False
        if not headers:
            headers = {"Content-Type": "application/x-www-form-urlencoded"}
        try:
            if params_is_json:
                r = requests.post(url, json=params, headers=headers, verify=verify, timeout=timeout)
            else:
                r = requests.post(url, data=params, headers=headers, verify=verify, timeout=timeout)
            if r.content and r.encoding is None:
                result = r.content.decode('utf-8')
            elif r.content and r.encoding.lower() == 'gbk':
                result = r.content.decode('gbk')
            elif r.content and r.encoding.lower() == 'gb2312' or r.apparent_encoding == 'GB2312':
                result = r.content.decode('gb2312')
            elif r.content:
                result = r.content.decode('utf-8')
            else:
                result = {}

            if r.status_code == 502 or r.status_code == 500:
                remarks = '第三方接口请求【FAIL】,status_code:%s 错误' % (r.status_code)
            elif r.status_code >= 400 and r.status_code <= 499:
                # return return_msg(r.status_code, '语法格式有误,服务器无法理解此请求', result)
                remarks = '第三方接口请求【FAIL】,status_code:%s 错误' % (r.status_code)
            else:
                # 把字典转换成为string
                if isback_json:
                    result = json.loads(result)
                remarks = '第三方接口请求【OK】'
                request_ok = True

        except requests.exceptions.ReadTimeout:
            result = {}
            # 读取超时
            remarks = '第三方接口请求【FAIL】,服务器在指定时间内没有应答,服务超时(ReadTimeout)'

        except requests.exceptions.ConnectTimeout:
            # 服务器在指定时间内没有应答,链接超时
            result = {}
            remarks = '第三方接口请求【FAIL】,服务器在指定时间内没有应答,链接超时(ConnectTimeout)'
        except requests.exceptions.ConnectionError:
            # 未知的服务器
            result = {}
            remarks = '第三方接口请求【FAIL】,未知的服务器(ConnectionError)'
        except requests.exceptions.ChunkedEncodingError:
            result = {}
            remarks = '第三方接口请求【FAIL】,ChunkedEncodingError异常'
        except requests.exceptions.Timeout:
            result = {}
            remarks = '第三方接口请求【FAIL】,Timeout异常'
        except:
            result = {}
            remarks = '第三方接口请求【FAIL】,其他异常(ChunkedEncodingError)'
        finally:
            info_interface = {
                'url': url,
                'method': 'POST',
                'params':params ,
                'params_str': str(params),
                'this_time_out': str(timeout) + 's',
                'req_stime': str(get_datetime_heng()),
                'cost_time': r.elapsed.microseconds / 1000,
                'state_code': str(r.status_code),
                'result': result,
            }
            if is_log:
                link_add_log_record(event_des='第三方接口请求日志', msg_dict=info_interface, remarks=remarks)

            # 第三方接口请求异常的时候是否发送广播通知到微信中
            # if not request_ok and is_send_wx_exceptions:
                # signal_send('wx_notify_third_inters_error', source='ThirdInternalErrorException', traceback=info_interface)

            return request_ok,result

    @staticmethod
    def get(url=None, params=None, headers=None, isback_json=True, verify=False, timeout=15,is_log = True,is_send_wx_exceptions =False,logger =  None):
        """向指定接口发送Get请求"""
        result = {}
        r = ''
        remarks = ''
        request_ok = False
        if not headers:
            headers = {"Content-Type": "application/x-www-form-urlencoded"}
        try:
            r = requests.get(url, params=params, headers=headers, verify=verify, timeout=timeout)
            if r.content and r.encoding is None:
                result = r.content.decode('utf-8')
            elif r.content and r.encoding.lower() == 'gbk':
                result = r.content.decode('gbk')
            elif r.content and r.encoding.lower() == 'gb2312' or r.apparent_encoding == 'GB2312':
                result = r.content.decode('gb2312')
            elif r.content:
                result = r.content.decode('utf-8')
            else:
                result = {}

            if r.status_code == 502 or r.status_code == 500:
                remarks = '第三方接口请求【FAIL】,status_code:%s 错误' % (r.status_code)
            elif r.status_code >= 400 and r.status_code <= 499:
                # return return_msg(r.status_code, '语法格式有误,服务器无法理解此请求', result)
                remarks = '第三方接口请求【FAIL】,status_code:%s 错误'%(r.status_code)
            else:
                # 将返回内容转为json
                if isback_json:
                    result = json.loads(result)
                # 返回josn
                remarks = '第三方接口请求【OK】'
                request_ok = True

        except requests.exceptions.ReadTimeout:
            result = {}
            # 读取超时
            remarks = '第三方接口请求【FAIL】,服务器在指定时间内没有应答,服务超时(ReadTimeout)'
        except requests.exceptions.ConnectTimeout:
            # 服务器在指定时间内没有应答,链接超时
            result = {}
            remarks = '第三方接口请求【FAIL】,服务器在指定时间内没有应答,链接超时(ConnectTimeout)'
        except requests.exceptions.ConnectionError:
            # 未知的服务器
            result = {}
            remarks = '第三方接口请求【FAIL】,未知的服务器(ConnectionError)'
        except requests.exceptions.ChunkedEncodingError:
            result = {}
            remarks = '第三方接口请求【FAIL】,ChunkedEncodingError异常'
        except requests.exceptions.Timeout:
            result = {}
            remarks = '第三方接口请求【FAIL】,Timeout异常'
        except:
            result = {}
            remarks = '第三方接口请求【FAIL】,其他异常(ChunkedEncodingError)'
        finally:
            info_interface = {
                'url': url,
                'method':'GET',
                'params': params,
                'params_str': str(params),
                'this_time_out':str(timeout)+'s',
                'req_stime': str(get_datetime_heng()),
                'cost_time': r.elapsed.microseconds / 1000,
                'state_code': str(r.status_code),
                'result': result,
            }
            if is_log:
                # print("第三方接口请求日志")
                link_add_log_record(event_des='第三方接口请求日志', msg_dict=info_interface, remarks=remarks)

            if logger:
                from core.helper.json_helper import dict_to_json_ensure_ascii
                log_msg = dict_to_json_ensure_ascii(info_interface,ensure_ascii=False)  # 返回文本
                logger.info(log_msg)

            # 第三方接口请求异常的时候是否发送广播通知到微信中
            # if not request_ok and is_send_wx_exceptions:
            #     signal_send('wx_notify_third_inters_error', source='ThirdInternalErrorException', traceback=info_interface)

            return request_ok,result

上述代码中关键的地方是:

link_add_log_record(event_des='第三方接口请求日志', msg_dict=info_interface, remarks=remarks)

我们试一试再一个简答的API请求又请求一次百度的地址看看我们的日志记录的情况:

from bottle import route
from app_demo.validators import LoginForm,validate_back_form
from core.http import Http as httper

@route('/user/action',method=['POST', 'GET', 'OPTIONS'])
# @validate_back_form(LoginForm)
def index():
    # print(form.username.data)
    # 89/0
    # 第三方请求的日志处理--示例
    request_ok, _result = httper.get(url='https://www.baidu.com', isback_json=False, is_log=True)

    return '我是用户接口'

再次查看我们的产生的日志文件内容信息:

{
    "headers":"{
    "Content-Length": "0",
    "Content-Type": "text/plain",
    "User-Agent": "PostmanRuntime/7.21.0",
    "Accept": "*/*",
    "Cache-Control": "no-cache",
    "Postman-Token": "ba217275-fdc8-42a1-a851-d59a259be885",
    "Host": "127.0.0.1:8889",
    "Accept-Encoding": "gzip, deflate",
    "Cookie": "beaker.session.id=641b2a4c4de540089f0209b386026c2f",
    "Connection": "keep-alive"
}",
    "host":"127.0.0.1:8889",
    "ip":"127.0.0.1",
    "url":"http://127.0.0.1:8889/user/action",
    "method":"POST",
    "params":{
        "query_string":"",
        "query":"",
        "forms":""
    },
    "req_stime":"2020-03-12 16:14:10.342569",
    "req_links_logs":[
        {
            "link_index":1,
            "event_des":"request-start"
        },
        {
            "link_index":2,
            "event_des":"第三方接口请求日志",
            "msg_dict":{
                "url":"https://www.baidu.com",
                "method":"GET",
                "params":null,
                "params_str":"None",
                "this_time_out":"15s",
                "req_stime":"2020-03-12 16:14:10",
                "cost_time":241.673,
                "state_code":"200",
                "result":"<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus=autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn" autofocus></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=https://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');
                </script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>©2017 Baidu <a href=http://www.baidu.com/duty/>使用百度前必读</a>  <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a> 京ICP证030173号  <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>
"
            },
            "remarks":"第三方接口请求【OK】"
        },
        {
            "link_index":3,
            "event_des":"request-end"
        }
    ],
    "rsp_data":null,
    "cost_time":"257.0"
}

基于以上一些日志统计,我们基本上完成的了一个日志统计的需求:

而且我们的可以从我们的日志中获取到对于的进程IP信息等。

2020-03-12 16:19:09:065 | process_id:18280 process_name:MainProcess | thread_id:18556 thread_name:MainThread | INFO |

(本小节完)

7 、API自动导入

我们的一个项目肯定是包含了非常的多的接口的,通常我们需要把我们的一些接口都放在同一个文件夹下去管理。 如我们的需要示例项目新增的api包:

对应的API文件夹下相关的文件如下:

order.py

    
    from bottle import route

    @route('/order/action/',method=['POST', 'GET', 'OPTIONS'])
    def index():
        #  直接的输出图片流
        return '我是订单接口'

pay.py

    
    from bottle import route

    @route('/pay/action/',method=['POST', 'GET', 'OPTIONS'])
    def index():
        #  直接的输出图片流
        return '我是支付接口'

user.py

    
    from bottle import route

    @route('/user/action/',method=['POST', 'GET', 'OPTIONS'])
    def index():
        #  直接的输出图片流
        return '我是用户接口'

如何能一次性的导入我们再api下定义所有的路由,而不需要一个一个的去导入的呐?使用__import__导入相关的,再包被引入的时候会执行__init__,我们可以利用此机制自动导入当前包下的相关模块:

import os, sys
from loguru import logger

def auto():
    pro_path = os.path.split(os.path.realpath(__file__))[0]
    sys.path.append(pro_path)
    for root, dirs, files in os.walk(pro_path):
        for file in files:
            name, ext = os.path.splitext(file)
            if ext == '.py' and name != '__init__' and pro_path == root:
                __import__(name)
        for dir in dirs:
            if dir != '.svn':
                try:
                    __import__(__name__ + '.' + dir)
                except Exception as e:
                    logger.exception(e)
                    # log_helper.error('初始化导入py文件出现异常:' + str(e.args))
        break
    pass

#自动化
auto()

然后我们再我们的app.py直接的导入api包就可以自动的导入api下的所有的路由模块了:

from app_demo import api

  
    from app_demo import api

    if __name__ == '__main__':
        # 读取配置信息处理
        curr_register_init_config()
        # 测试打印读取的配置信息
        test_read_config()

        # 生产的log文件存在路径,并写入测试日志文件信息
        curr_creat_customize_log_loguru()
        

        # 创建应用的实例
        app = create_default_app_application()
        # 运行App实例
        run(app=app, debug=True, host="127.0.0.1", port=8889, reloader=True)

此时我们的随意访问某一个模块下的路由地址就都有响应了!

没毛病!可以访问!

(此小节完)

8 、API 身份验证,JWT鉴权实现

用户鉴权的重要性,其实我们也都知道它的重要性,再我们的一些后台开发中也都经常是需要对用户的权限信息进行验证。在开始对我们的JWT展开之前,首先我们需要了解一下一些基础。首先是什么样的环境下催生了JWT的需求?

在传统的开发中,我们经常需要解决用户登入和后期身份鉴权认证的问题,早期的实现方案主要是依靠于:cookie、session。

那这里我们需要了解cookie、session一些基础:

  • Cookie 保留在客户端中,通常会随着浏览器的关闭而消失,当然这种Cookie事存在内存中的。它是由服务端生产,发送到浏览器,浏览器会已KV的形式保持再某个目录下,当下一次请求的同一个网站的时候,会把cookie再一同的发送给服务器。

  • Session 是用于标记身份,通常我们开发一些API是无状态的API,那既然是无状态的话,如何识别用户身份,则就需要通过session的机制了。 通常为了区分开当前请求是谁提交的,我们的服务端一般会给客户端标记一个身份,然后每次客户端向服务端发起请求的时候,都需要携带这个身份标记。这样服务端就好识别是谁来请求接口了!至于发送给客户端,客户端通过什么形式保持,这个我们服务端一般不管,当然通常肯就是上面说到的Cookie居多。

使用Session标记识别身份的缺点主要是:因为Session保存再服务器本地上,如果我们做了负载均衡的话,你想想,那当请求分发到其他个服务器上的时候,那肯定是找不到之前生产的身份标记,当然我们的可以使用我们的redis,memcached缓存中间件来实现session的共享。

关于我们的本小节说的Token,Token的意思就是令牌。关于JWT的组成,这里我不想细聊太多,主要关注更多的是如何应用,一般再什么场景下使用? 一般我们的JWT多数应用于: 单点登录授权,用户身份验证通过后,分发给用户token,用户在请求的token中包含了该令牌允许授权信息和资源。Token的身份认证是无状态的,也能很好的解决了session加负载均衡后产生的问题。

我们后续的前端后分类的系统其实也需要用到这个知识点。 所以我们这里先简单实现以备后续使用,这里我直接使用PYJWT的这个可以来处理

pip install pyjwt

然后再我们的core基础库下的help模块定义一个jwt_helper.py


import jwt
import datetime


# 1000天有效期
def create_token_by_data(sub='', data={}, secret='', scopes=['open'], is_back_data=False, exp_time=60 * 60 * 24 * 5):
    """
    生成对应的JWT的token值
    :param sub:    参数名称
    :param data:      参与签名的参数信息
    :param secret:   是否要求进行空检测,True必须检测
    :param exp_time:  token过期时间,按秒来计算
    :return:        返回处理后的参数
    """

    # 签名密钥的判断
    if not secret:
        return False, {'access_token': '', 'meg': '密匙不能为空'}

    if not data:
        return False, {'access_token': '', 'meg': '需要签名信息不能为空'}

    payload = {
        "iss": "xiaozhong.com",  # iss: 该JWT的签发者,是否使用是可选的;
        "exp": datetime.datetime.utcnow() + datetime.timedelta(days=0, seconds=exp_time),  # 什么时候过期,这里是一个Unix时间戳,是否使用是可选的;
        "iat": datetime.datetime.utcnow(),  # 在什么时候签发的(UNIX时间),是否使用是可选的;
        "aud": "www.xiaozhong.com",  # 接收该JWT的一方,是否使用是可选的;#  如果在生成token的时候使用了aud参数,那么校验的时候也需要添加此参数
        "sub": sub,  # sub: 该JWT所面向的用户,是否使用是可选的;
        "scopes": scopes,  # 用户授权的作用域,使用逗号(,)分隔
        "data": data
    }
    # 不参与进行签名计算
    if not sub:
        payload.pop('sub')
    # token生成处理
    token = jwt.encode(payload, secret, algorithm='HS256')
    # 返回授权token
    if is_back_data:
        back_result = {
            'access_token': str(token, 'utf-8'),
            'data': data
        }
    else:
        back_result = {
            'access_token': str(token, 'utf-8'),
        }
    return True, back_result


def verify_bearer_token(ischecck_sub=False, secret='', sub_in='', token=''):
    #  如果在生成token的时候使用了aud参数,那么校验的时候也需要添加此参数
    try:
        payload = jwt.decode(token, secret, audience='www.xiaozhong.com', algorithms=['HS256'])
        if ischecck_sub and sub_in != '':
            sub = payload['sub']
            if sub != sub_in:
                return False, "无效的Token"

        if payload and ('data' in payload):
            # 验证通过返回对应的参与签名的字段信息
            return True, payload['data']
        else:
            raise jwt.InvalidTokenError

    except jwt.ExpiredSignatureError:
        return False, "Token过期"

    except jwt.InvalidTokenError:
        return False, "无效的Token"
    except:
        return False, "无效的Token"


def verify_bearer_token_state(ischecck_sub=False, secret='', sub_in='', token=''):
    #  如果在生成token的时候使用了aud参数,那么校验的时候也需要添加此参数
    try:
        payload = jwt.decode(token, secret, audience='www.xiaozhong.com', algorithms=['HS256'])
        if ischecck_sub and sub_in != '':
            sub = payload['sub']
            if sub != sub_in:
                return False, 1,"无效的Token"

        if payload and ('data' in payload):
            # 验证通过返回对应的参与签名的字段信息
            return True, 0,payload['data']
        else:
            raise jwt.InvalidTokenError

    except jwt.ExpiredSignatureError:
        return False, 2,"Token过期"

    except jwt.InvalidTokenError:
        return False, 1,"无效的Token"
    except:
        return False, 1,"无效的Token"

所以我们的需要两个接口的示例来演示:



@route('/user/login',method=['POST', 'GET', 'OPTIONS'])
@validate_back_form(LoginForm)
def index(form):
    # 用户登入
    if form.username.data == 'xiaozhong' and form.password.data == 'xiaozhong':
        # 用户密码校验通过返回一个token给用户,让用户后期提交此token来验证
        data = {
            'userid':100001,
            'username': '小钟同学',
        }
        isok,back_result = jwt_helper.create_token_by_data(secret='woshimiyao',data=data,exp_time=60 * 60 * 24 * 5)
        return CustomizeApiResponse(data={'access_token': back_result.get('access_token')})
    else:
        return CustomizeApiResponse(msg='密码错误啦!')



@route('/user/check',method=['POST', 'GET', 'OPTIONS'])
@validate_back_form(SysPermissionForm)
def index(form):
    # 用户登入
    print(form.token.data)
    isok, back_result=jwt_helper.verify_bearer_token(token=form.token.data,secret='woshimiyao')
    if isok:
        return CustomizeApiResponse(msg='认证通过了!')
    else:
        return CustomizeApiResponse(msg=back_result)

首先我们先生成我们的token:

我们测试接口验证下:

(本小节完)

8 、信号广播,业务解耦

信号广播机制主要类似我们的事件广播机制,主要方便于我们的一些业务解耦,和一些事件通知处理

引入信号机制,我们的可以解耦一些业务,比如在API请求过程中出现异常的时候,我们发送一个广播通知进行微信异常通知。

信号机制的实现我们需要安装一个信号库:blinker---信号订阅库(广播机制)

安装依赖库:

> pip install blinker

具体实现再我们的core下的signals定义相关以下函数:

# 参数:信号名,信号的发送者
# 作用:根据信号名创建信号,然后发送信号,参数是发送者
from blinker import signal


def signal_send(name, source='anoymous', **kw):
  """
  创建信号并发送消息
  参数:name 信号名,source:信号的发送者
  作用:根据信号名创建信号,然后发送信号,参数是发送者
  """
  name_signal = signal(name)  # 这里根据name创建一个信号
  ret = name_signal.send(source, **kw)  # 发送一个信号
  return ret


def signal_listen(name, handler):
  """
  接受信号
  参数:信号名,订阅者(用于收到信号就调用函数)
  作用:得到信号对象,绑定订阅者
  """
  name_signal = signal(name)  # 这里其实没有创建新的信号,就是之前的name信号\
  name_signal.connect(handler, weak=False)  # 绑定了订阅者


def signal_handle(name, *args, **kw):
  """
  信号接受装饰器:
  参数:信号名
  针对的func:装饰器原理是针对func的,传入的func就是这个信号的订阅者
  """
  s_name = name
  def wrapper(func):
      def inner(*args, **kw):
          func(*args, **kw)
      signal_listen(s_name, func)
      return inner
  return wrapper

定义了我们的相关的信号功能之后,我们就可以在默认的应用里 默认注册我们的一个基础App应该具备的一些基础的东西,比如默认我要接收到500异常和第三方接口错误的异常的微信提醒功能:



def _register_default_signal_handle():
    @signal_handle("wx_notify_error_500")
    def recv_wx_notify(sender, **kw):
        """
         接收500异常的时候是否发送相关异常信息到微信公众号上
        """
        pass
        # if DEBUG:
        print("接收到了异常信号通知 by %r, data %r" % (sender, kw))
        # content_msg = '温馨提示=====》系统出现异常' + str(kw.get('traceback'))
        # weixin_helper.send_users_msg(users='XiaoZhongTongXue', content=content_msg, agentid='1000007', WXKEY='ww7651ba25a3995026', WXSECRET='5k1K25qrdS4P6V87Dehsk6VgL4jJNWFJpKiPHTEia1U')

    @signal_handle("wx_notify_third_inters_error")
    def wx_notify_third_inters_error(sender, **kw):
        """
         接收500异常的时候是否发送相关异常信息到微信公众号上
        """
        pass
        # if DEBUG:
        print("接收到了第三方接口请求处理异常信号通知 by %r, data %r" % (sender, kw))
        # content_msg = '温馨提示=====》系统请求第三方接口处理异常' + str(json_helper.dict_to_json_ensure_ascii_indent(kw.get('traceback')))
        # weixin_helper.send_users_msg(users='XiaoZhongTongXue', content=content_msg, agentid='1000007', WXKEY='ww7651ba25a3995026', WXSECRET='5k1K25qrdS4P6V87Dehsk6VgL4jJNWFJpKiPHTEia1U')



def create_default_app_application():
    '''
    # 默认的应用实例对象
    :return:
    '''

    # 注册默认的接收的广播
    _register_default_signal_handle()
    # 注册默认的健康检测路由
    _register_default_routes()
    #  默认注册全局错误处理异常
    # 配置全局异常
    install(ErrorsRestPlugin(error_handler=_register_global_error_catch))

    # 创建应用实例
    # app = Bottle(autojson=False)
    # 使用默认的实例
    # 配置相关的session的服务对象
    session_opts = {
        'session.type': 'file',
        'session.cookie_expires': 3600,
        'session.data_dir': '/tmp/sessions/order',
        'session.auto': True
    }
    application = SessionMiddleware(default_app(), session_opts)

    return application


关键的地方是:

_register_default_signal_handle

然后我们的需要广播消息的地方进行信息的广播,如程序发生500异常的时候,进行事件的广播:

我们再我们的全局错误错误异常里添加:

def _register_global_error_catch(e, is_send_to_wx=True):
    """
      注册全局异常捕获
    """
    if isinstance(e, HTTPError):
        code = e.status_code
        # 处理参数校验异常的情况
        if code == 400:
            # 参数校验异常
            return ParameterException(code=code, msg=e.body)
        elif code == 401:
            # 请求不允许
            return UnauthorizedException()
        elif code == 403:
            # 访问权限受限
            return ForbiddenException()
        elif code == 404:
            # 找不到此访问地址
            return NotfoundException()
        elif code == 405:
            # 请求方式异常
            return MethodnotallowedException()
        elif code == 429:
            # 限流异常
            return RateLimitApiException()
        elif code == 900:
            # 确实比传的参数异常
            return CustomizeParameterException(msg=e.body)
        else:
            # 判断异常类型
            # if isinstance(e, RateLimitException):
            # 429异常
            # return RateLimitApiException()
            return OtherException(code=code)
    else:
        # 如果是Debug
        # 打印错误日志,并且记录到日志中
        # logger.error(e)
        logger.exception(e)
        # if isinstance(e,RateLimitException):
        # 429异常
        # return RateLimitApiException()
        if is_send_to_wx and not DEBUG:
            # 发送名字,source信号的发送者---详细堆栈信息
            #  signal_send('wx_notify_error', source='InternalErrorException', traceback=except_helper.get_detailtrace())
            signal_send('wx_notify_error_500', source='InternalErrorException', traceback=e)
            pass
        # 500异常---也返回友好的200
        return InternalErrorException(msg='系统崩溃了,程序员哥哥肯定是没睡好!')
        # return CustomizeApiResponse(msg='系统崩溃了,程序员哥哥肯定是没睡好!')
        # return InternalErrorException()

测试一下当我们的程序异常的的时候,我们的是否收到事件通知:

(本小节完)

9 、缓存redis实现(redis)

关于reids在我们得API中得重要性是非常重要得,它可以提高我们得整体应用得性能和QPS。

9.1 应用配置信息的读取,如redis配置信息读取

ps 考虑后续扩展的话,可以放在core,因为比较简单,其实也不一定要放在这个基础库定义,完全可以放在应用中去读取即可 configparser库是python中自带的模块,该模块适用于配置文件的格式与windows ini文件类似,可以包含一个或多个节点,每个节可以有多个参数键值对的方式进行配置。

configparser配置文件的格式:


#  定义config分组
[redis]
host=localhost
post=6379
password=
db=3

[config]
platformName=Android
appPackage=com.romwe
appActivity=com.romwe.SplashActivity

[token]
TOKEN_SECRETKEY=3wBp4LQsaG3JfFS5

一般情况下我们只涉及到服务配置文件的信息,很少主动的写入修改,除非特殊的情况下,比如动态的修改配置信息等。

关于configparser的读取,我们可以封装到我们的core下,以一个单例的模式来进行提供实例对象,如:

import warnings
# warnings.filterwarnings("ignore")
import configparser, os  # 实例化configParser对象 config = configparser.ConfigParser()

#  实例化configParser对象,单例模式的进行实例化
config = configparser.ConfigParser()

def register_init_config(configini_pro_path=None, files_name=None):
    # -read读取ini文件
    try:
        if not configini_pro_path:
            warnings.warn('服务配置信息文件为空!')
        else:
            if not configini_pro_path:
                warnings.warn('服务配置信息文件文件名还没定义')
            else:
                if isinstance(files_name, list):
                    for item in files_name:
                        config.read(configini_pro_path + '\\' + item, encoding='utf-8')
                else:
                    config.read(configini_pro_path + '\\' + files_name, encoding='utf-8')
    except Exception as e:
        print(e)
        warnings.warn('服务配置信息文件读取错误')

基于之前的appdemo示例,测试我们的配置文件的读取,并且打印出相关的配置文件内的信息:

config.ini 文件内容

#  定义config分组
[redis]
host=localhost
post=6379
password=
db=3

[config]
platformName=Android
appPackage=com.romwe
appActivity=com.romwe.SplashActivity

[token]
TOKEN_SECRETKEY=3wBp4LQsaG3JfFS5

新增的文件结构:

测试读取 文件内容


import os
from bottle import run, route
from core.basicapp import create_default_app_application
from core.config import config, register_init_config


@route('/nihao/<name>')
def index(name):
    return '你好 %s ' % (name)


def curr_register_init_config():
    # 指定到当前项目的某个包下的进行文件配置读取
    pro_path = os.path.split(os.path.realpath(__file__))[0] + '\\' + 'config\\'
    print(pro_path)
    register_init_config(configini_pro_path=pro_path, files_name='config.ini')

def test_read_config():
    redis_config_const = {
        # 服务地址
        'host': config.get('redis', 'host'),
        # 服务端口
        'post': config.getint('redis', 'post'),
        # 服务密码
        'password': config.get('redis', 'password'),
        # 数据库序号
        'db': config.getint('redis', 'db')
    }
    print(redis_config_const)


if __name__ == '__main__':
    # 读取配置信息处理
    curr_register_init_config()
    # 测试打印读取的配置信息
    test_read_config()
    # 创建应用的实例
    app = create_default_app_application()
    # 运行App实例
    run(app=app, debug=True, host="127.0.0.1", port=8889, reloader=True)


运行的一下当前代码示例,则看到相关的数据信息和我们的配置文件config.ini里面的是一致的:


{'host': 'localhost', 'post': 6379, 'password': '', 'db': 3}

9.2 redis缓存的使用

对于redis,有五种得数据类型得支持,常用得一些使用场景如下:

  • string:可以包含任何数据(jps图片或者经过序列化的对象,如json,单个key最大可以存储512M的数据),具有全局统计功能的数据,如全局ID生成器、集群配置信息等;

  • hash :用于存储对象结构的数据,多个field绑定到一个key上(对比使用string类型存储对象的优势在于hash类型可以直接update具体field的值而不影响其他field),如实现SSO,cookie为key,用户信息为value,并有指定过期时间;

  • list :用于存储需要基于队列或者栈操作的系列数据,如消息队列;

  • set : 用于存储需要维护一个全局不重复的集合,如服务注册发现,可以实现全局去重的功能,如访问网页的独立IP,共同好友等;

  • zset :用于存储需要维护一个全局不重复但有权重排序的列表可以使用SORTED SET,如积分排行榜、带权重的消息队列。

在我们的API开发中一般常用的有,存贮我们的接口请求数据(固定),防止多次穿透的后端进行数据库的查询。 这类也有一个专业名称就是《缓存穿透》。

缓存穿透本质是:就是再内存找不到数据的时候,只能跑到数据库上去查询。 缓存穿透解决方案有:

  • 空值缓存 :再第一次查询数据库查询不到数据的时候,标记此查询没值,那空值写入到redis中。

  • 布隆过滤器:这个的话,找个时间仔细研究一下才行!

再这里,我们需要把reids引入到我们的API中,比如对查询用户信息的缓存等也都使用上!

先安装上我的redis,注意关于一个reids版本的问题,我个人在使用过程,感觉高版本的有点问题,所以建议你使用低版本的来使用。

pip install redis2.10.6 -rapcache依赖于redis2.10.6 ---X

然后使用单例的模式初始化一个我们的reids:


import redis


class RedisDBConfig:
    HOST = '127.0.0.1'
    PORT = 6379
    DBID = 2
    PASSWORD = ''


def Singleton(cls):
    _instance = {}

    def _singleton(*args, **kargs):
        if cls not in _instance:
            _instance[cls] = cls(*args, **kargs)
        return _instance[cls]

    return _singleton


def operator_status(func):
    '''get operatoration status
    '''

    def gen_status(*args, **kwargs):
        error, result = None, None
        try:
            result = func(*args, **kwargs)
        except Exception as e:
            error = str(e)
        return {'result': result, 'error': error}

    return gen_status


# 使用单列模式来创建redis实例
@Singleton
class RedisCache(object):
    def __init__(self, _redis=None):
        if _redis:
            pool = redis.ConnectionPool(host=_redis.get('host', ''), port=_redis.get('post', ''), db=_redis.get('db', ''), password=_redis.get('password', ''), socket_timeout=1, socket_connect_timeout=1, decode_responses=True)
            self._connection = redis.Redis(connection_pool=pool)

    def init_app(self, _redis):
        if _redis:
            pool = redis.ConnectionPool(host=_redis.get('host', ''), port=_redis.get('post', ''), db=_redis.get('db', ''), password=_redis.get('password', ''), socket_timeout=1, socket_connect_timeout=1,decode_responses=True)
        else:
            pool = redis.ConnectionPool(host=RedisDBConfig.HOST, port=RedisDBConfig.PORT, db=RedisDBConfig.DBID, password=RedisDBConfig.PASSWORD, socket_timeout=1, socket_connect_timeout=1,decode_responses=True)

        self._connection = redis.Redis(connection_pool=pool)


    def get_connection(self, _redis=None):
        if _redis:
            pool = redis.ConnectionPool(host=_redis.get('host', ''), port=_redis.get('post', ''), db=_redis.get('db', ''), password=_redis.get('password', ''), socket_timeout=1, socket_connect_timeout=1, decode_responses=True)
        else:
            pool = redis.ConnectionPool(host=RedisDBConfig.HOST, port=RedisDBConfig.PORT, db=RedisDBConfig.DBID, password=RedisDBConfig.PASSWORD, socket_timeout=1, socket_connect_timeout=1, decode_responses=True)
        self._connection = redis.Redis(connection_pool=pool)

        return self._connection

    
    # @operator_status
    def set(self, key, value, ex=None):
        '''set data with (key, value)
        ex 过期时间(秒)
        px 过期时间(豪秒)
        nx 如果设置为True,则只有name不存在时,当前set操作才执行 (新建)
        print(r.set('fruit', 'watermelon', nx=True))    # True--不存在# 如果键fruit不存在,那么输出是True;如果键fruit已经存在,输出是None
        xx 如果设置为True,则只有name存在时,当前set操作才执行 (修改)
        print((r.set('fruit', 'watermelon', xx=True)))   # True--已经存在# 如果键fruit已经存在,那么输出是True;如果键fruit不存在,输出是None
        '''
        return self._connection.set(key, value, ex=ex)

    # @operator_status
    def get(self, key):
        '''get data by key
        '''
        return self._connection.get(key)

    # @operator_status
    def delkey(self, key):
        '''delete cache by key
        '''
        return self._connection.delete(key)

        # @operator_status

    def setnx(self, key, value):
        '''set data with (key, value)
       设置值,只有name不存在时,执行设置操作(添加)
       print(r.setnx('fruit1', 'banana'))  # fruit1不存在,输出为True
        '''
        return self._connection.setnx(key, value)

    def setex(self, key, value, time):
        '''set data with (key, value)
       time,过期时间(数字秒 或 timedelta对象)
        '''
        return self._connection.setex(name=key, value=value, time=time)

    def psetex(self, key, value, time_ms):
        '''set data with (key, value)
       time_ms,过期时间(数字毫秒 或 timedelta对象)
       r.psetex("fruit3", 5000, "apple")time.sleep(5)print(r.get('fruit3'))  # 5000毫秒后,取值就从apple变成None
        '''
        return self._connection.psetex(name=key, value=value, time_ms=time_ms)

    # 批量设置值
    def mset(self, *args, **kwargs):
        '''set data with (key, value)
       r.mget({'k1': 'v1', 'k2': 'v2'})r.mset(k1="v1", k2="v2") # 这里k1 和k2 不能带引号 一次设置对个键值对print(r.mget("k1", "k2"))   # 一次取出多个键对应的值print(r.mget("k1"))
        '''
        return self._connection.mset(*args, **kwargs)

    # 批量获取值
    def mget(self, *args, **kwargs):
        '''set data with (key, value)
       r.mget({'k1': 'v1', 'k2': 'v2'})r.mset(k1="v1", k2="v2") # 这里k1 和k2 不能带引号 一次设置对个键值对print(r.mget("k1", "k2"))   # 一次取出多个键对应的值print(r.mget("k1"))
        '''
        return self._connection.mget(*args, **kwargs)

        # 批量获取值

    # 设置新值并获取原来的值
    def getset(self, key, value):
        '''set data with (key, value)
     print(r.getset("food", "barbecue"))  # 设置的新值是barbecue 设置前的值是beef
        '''
        return self._connection.getset(name=key, value=value)


    # 获取子序列(根据字节获取,非字符)
    def getrange(self, key, value):
        '''参数:
            name,Redis 的 name
            start,起始位置(字节)
            end,结束位置(字节)
            如: "君惜大大" ,0-3表示 "君"
        '''
        pass


    #  自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。
    def incr(self, key, amount=1):
        '''参数:
            自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。
            name,Redis的name
            amount,自增数(必须是整数)
            注:同incrby

        应用场景 – 页面点击数
        假定我们对一系列页面需要记录点击次数。例如论坛的每个帖子都要记录点击次数,而点击次数比回帖的次数的多得多。如果使用关系数据库来存储点击,可能存在大量的行级锁争用。所以,点击数的增加使用redis的INCR命令最好不过了。
        当redis服务器启动时,可以从关系数据库读入点击数的初始值(12306这个页面被访问了34634次)
        每当有一个页面点击,则使用INCR增加点击数即可。
        r.incr("visit:12306:totals")r.incr("visit:12306:totals")
        '''
        return self._connection.incr(name=key, amount=amount)

    def incrbyfloat(self, key, amount=1.0):
        '''参数:
            自增 name对应的值,当name不存在时,则创建name=amount,否则,则自增。
            name,Redis的name
            amount,amount,自增数(浮点型)
            注:同incrby

        应用场景 – 页面点击数
        假定我们对一系列页面需要记录点击次数。例如论坛的每个帖子都要记录点击次数,而点击次数比回帖的次数的多得多。如果使用关系数据库来存储点击,可能存在大量的行级锁争用。所以,点击数的增加使用redis的INCR命令最好不过了。
        当redis服务器启动时,可以从关系数据库读入点击数的初始值(12306这个页面被访问了34634次)
        每当有一个页面点击,则使用INCR增加点击数即可。
        r.incr("visit:12306:totals")r.incr("visit:12306:totals")
        '''
        return self._connection.incrbyfloat(name=key, amount=amount)


    def decr(self, key, amount=1):
        '''参数:
            自增 name对应的值,当name不存在时,则创建name=amount,否则,则自减。
            name,Redis的name
            amount,amount,则自减(整数)
        '''
        return self._connection.decr(name=key, amount=amount)




# 创建单列对象
redisCache = RedisCache()

然后我们的再我们的应用DEMO里面进行初始化:

# 注册缓存处理
def register_cache(config):
    # 导入缓存处理库
    redis_config_const = {
        # 服务地址
        'host': config.get('redis', 'host'),
        # 服务端口
        'post': config.getint('redis', 'post'),
        # 服务密码
        'password': config.get('redis', 'password'),
        # 数据库序号
        'db': config.getint('redis', 'db')
    }
    from core.cache.reids_pool_helper import redisCache
    redisCache.init_app(_redis=redis_config_const)
    # 启动前都先把缓存清理一次,仅仅测试使用
   


if __name__ == '__main__':
    # 读取配置信息处理
    curr_register_init_config()
    # 测试打印读取的配置信息
    test_read_config()
    # 生产的log文件存在路径,并写入测试日志文件信息
    curr_creat_customize_log_loguru()
    # 传入配置读取信息对象,通过读取配置文件里面的信息来初始化注册我们的缓存
    register_cache(config)
    # 创建应用的实例
    app = create_default_app_application()
    # 运行App实例
    run(app=app, debug=True, host="127.0.0.1", port=8889, reloader=True)

然后我们的再接口里测试:

from core.cache.reids_pool_helper import redisCache as cache

@route('/user/cache',method=['POST', 'GET', 'OPTIONS'])
def index():
   if not cache.get('Testkey'):
        print(cache.set('Testkey', "Simple Test"))
        return  CustomizeApiResponse(msg='我是从没---缓存的地方返回来的')
   else:
    print(cache.get('Testkey'))
    return  CustomizeApiResponse(msg='我是有---缓存的地方返回来的')

测试我们的结果:

第一次请求:

第二次请求:

(本小节完)

10 、ORM数据库-peewee操作ostgresql

关于数据库的使用,通常我们对数据库操作的时候都是通过SQL进行操作,这样对的应用的性能来说总体肯定是会很高的,对于使用ORM还是有一定的损耗,但是有些时候还是可以在接受的范围之内,使用ORM可以让我们更加方便快捷对数据进行操作。

首先我们得需要安装一下这个ORM库,因为我使用得是postgresql所以我还需要安装一下链接postgresql得驱动库

> pip install peewee
> pip install psycopg2

关于PEEREE这个是一个简单小巧的Python ORM,和 sqlalchemy类似,其实按照官方文档操作一波,实践起来其实也是没什么难道。

我个人的话,通常的数据库设计流程是:

  • 1:设计数据库字典
  • 2:根据SQL生成数据库表
  • 3:根据数据库导出生成数据库映射的MODEL
  • 4:然后对相关的MODEL进行相关的CURD

当然你也可以先定义好MODEL再直接生产数据库。

简单的示例流程如下:

设计我们的RBAC字典:

然后我们的使用SQL生成工具来生成: 工具可以找大牛>大牛地址官方博文地址www.cnblogs.com/EmptyFS/p/7…查看具体的文章查看进行下载。

然后生产的SQL文件如下:

CREATE TABLE public.sys_user (
 id serial,
 user_id integer,
 username text DEFAULT '' NOT NULL,
 realname text DEFAULT '' NOT NULL,
 password text DEFAULT '' NOT NULL,
 email text DEFAULT '',
 mobile text DEFAULT '',
 status integer DEFAULT 1,
 birthday timestamp(0) WITHOUT TIME ZONE,
 sex integer DEFAULT 2,
 description text DEFAULT '',
 avatar text DEFAULT 'WhxKECPNujWoWEFNdnJE.png',
 del_flag integer DEFAULT 0,
 del_time timestamp(0) WITHOUT TIME ZONE,
 org_code text DEFAULT '',
 work_no text DEFAULT '',
 postion_name text DEFAULT '',
 postion_code text DEFAULT '',
 create_by text DEFAULT '',
 update_by text DEFAULT '',
 create_time timestamp(0) WITHOUT TIME ZONE DEFAULT now(),
 update_time timestamp(0) WITHOUT TIME ZONE,
  CONSTRAINT sys_user_pkey PRIMARY KEY(id)
)
WITH(oids = false);

COMMENT ON TABLE public.sys_user IS '用户信息表'; 
COMMENT ON COLUMN public.sys_user.id IS '主键ID'; 

COMMENT ON COLUMN public.sys_user.user_id IS '用户Id'; 

COMMENT ON COLUMN public.sys_user.username IS '登录账号'; 

COMMENT ON COLUMN public.sys_user.realname IS '真实姓名'; 

COMMENT ON COLUMN public.sys_user.password IS '密码'; 

COMMENT ON COLUMN public.sys_user.email IS '邮箱'; 

COMMENT ON COLUMN public.sys_user.mobile IS '电话'; 

COMMENT ON COLUMN public.sys_user.status IS '用户状态(1-正常,2-冻结)'; 

COMMENT ON COLUMN public.sys_user.birthday IS '生日'; 

COMMENT ON COLUMN public.sys_user.sex IS '性别 0男 1女 2保密'; 

COMMENT ON COLUMN public.sys_user.description IS '描述'; 

COMMENT ON COLUMN public.sys_user.avatar IS '头像'; 

COMMENT ON COLUMN public.sys_user.del_flag IS '删除状态(0-正常,1-已删除)'; 

COMMENT ON COLUMN public.sys_user.del_time IS '删除时间'; 

COMMENT ON COLUMN public.sys_user.org_code IS '机构编码'; 

COMMENT ON COLUMN public.sys_user.work_no IS '工号,唯一键'; 

COMMENT ON COLUMN public.sys_user.postion_name IS '职务,关联职务表'; 

COMMENT ON COLUMN public.sys_user.postion_code IS '职务编码,关联职务表'; 

COMMENT ON COLUMN public.sys_user.create_by IS '创建人'; 

COMMENT ON COLUMN public.sys_user.update_by IS '更新人'; 

COMMENT ON COLUMN public.sys_user.create_time IS '创建时间'; 

COMMENT ON COLUMN public.sys_user.update_time IS '修改时间'; 


CREATE INDEX sys_user_user_id_idx ON public.sys_user USING btree(user_id);
CREATE INDEX sys_user_username_idx ON public.sys_user USING btree(username);
CREATE INDEX sys_user_status_idx ON public.sys_user USING btree(status);
CREATE INDEX sys_user_del_flag_idx ON public.sys_user USING btree(del_flag);
create unique index on public.sys_user using btree(work_no);


生成结果:

有了数据库的存在之后,那我们接下来就可以根据数据库生成我们的MODEL了

python -m pwiz -e postgresql -H localhost -p 5432 -u postgres -P 123456 xcms_system_admin > xcms_system_admin_model.py

最终我们的得到的Model如下,那接下来我们的就可以在我们的接口上进行相关的数据CURD了:

from peewee import *

database = PostgresqlDatabase('xcms_system_admin', **{'host': 'localhost', 'port': 5432, 'user': 'postgres', 'password': '123456'})

class UnknownField(object):
    def __init__(self, *_, **__): pass

class BaseModel(Model):
    class Meta:
        database = database

class SysDepart(BaseModel):
    address = TextField(null=True)
    create_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    create_time = DateTimeField(constraints=[SQL("DEFAULT now()")], null=True)
    del_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    del_flag = IntegerField(constraints=[SQL("DEFAULT 0")], index=True, null=True)
    del_time = DateTimeField(null=True)
    depart_name = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    description = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    fax = TextField(null=True)
    mobile = TextField(null=True)
    org_category = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    org_code = TextField(constraints=[SQL("DEFAULT ''::text")], index=True, null=True)
    org_type = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    parent_id = IntegerField(index=True, null=True)
    remark = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    sort_no = IntegerField(constraints=[SQL("DEFAULT 0")], null=True)
    status = IntegerField(constraints=[SQL("DEFAULT 1")], index=True, null=True)
    update_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    update_time = DateTimeField(null=True)

    class Meta:
        table_name = 'sys_depart'

class SysPermission(BaseModel):
    always_show = IntegerField(constraints=[SQL("DEFAULT 1")], null=True)
    component = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    component_name = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    create_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    create_time = DateTimeField(constraints=[SQL("DEFAULT now()")], null=True)
    del_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    del_flag = IntegerField(constraints=[SQL("DEFAULT 0")], index=True, null=True)
    del_time = DateTimeField(null=True)
    description = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    icon = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    internal_or_external = IntegerField(constraints=[SQL("DEFAULT 0")], null=True)
    is_hidden = IntegerField(constraints=[SQL("DEFAULT 0")], null=True)
    is_keepalive = IntegerField(null=True)
    is_leaf = IntegerField(null=True)
    is_route = IntegerField(constraints=[SQL("DEFAULT 1")], null=True)
    menu_type = IntegerField(null=True)
    name = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    parent_id = IntegerField(index=True, null=True)
    perms = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    perms_type = TextField(constraints=[SQL("DEFAULT '0'::text")], null=True)
    redirect = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    rule_flag = IntegerField(constraints=[SQL("DEFAULT 0")], null=True)
    sort_no = IntegerField(null=True)
    status = IntegerField(constraints=[SQL("DEFAULT 1")], null=True)
    update_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    update_time = DateTimeField(null=True)
    url = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)

    class Meta:
        table_name = 'sys_permission'

class SysPosition(BaseModel):
    company_id = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    create_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    create_time = DateTimeField(constraints=[SQL("DEFAULT now()")], null=True)
    del_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    del_flag = IntegerField(constraints=[SQL("DEFAULT 0")], index=True, null=True)
    del_time = DateTimeField(null=True)
    position_code = TextField(constraints=[SQL("DEFAULT ''::text")], index=True, null=True)
    position_name = TextField(constraints=[SQL("DEFAULT ''::text")], index=True, null=True)
    post_rank = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    remark = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    sys_org_code = TextField(constraints=[SQL("DEFAULT ''::text")], index=True, null=True)
    update_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    update_time = DateTimeField(null=True)

    class Meta:
        table_name = 'sys_position'

class SysRole(BaseModel):
    create_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    create_time = DateTimeField(constraints=[SQL("DEFAULT now()")], null=True)
    del_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    del_flag = IntegerField(constraints=[SQL("DEFAULT 0")], index=True, null=True)
    del_time = DateTimeField(null=True)
    description = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    role_code = TextField(constraints=[SQL("DEFAULT ''::text")], index=True, null=True)
    role_name = TextField(constraints=[SQL("DEFAULT ''::text")], index=True, null=True)
    update_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    update_time = DateTimeField(null=True)

    class Meta:
        table_name = 'sys_role'

class SysRolePermission(BaseModel):
    permission_id = IntegerField(index=True, null=True)
    role_id = IntegerField(index=True, null=True)

    class Meta:
        table_name = 'sys_role_permission'

class SysUser(BaseModel):
    avatar = TextField(constraints=[SQL("DEFAULT 'WhxKECPNujWoWEFNdnJE.png'::text")], null=True)
    birthday = DateTimeField(null=True)
    create_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    create_time = DateTimeField(constraints=[SQL("DEFAULT now()")], null=True)
    del_flag = IntegerField(constraints=[SQL("DEFAULT 0")], index=True, null=True)
    del_time = DateTimeField(null=True)
    description = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    email = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    mobile = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    org_code = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    password = TextField(constraints=[SQL("DEFAULT ''::text")])
    postion_code = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    postion_name = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    realname = TextField(constraints=[SQL("DEFAULT ''::text")])
    sex = IntegerField(constraints=[SQL("DEFAULT 2")], null=True)
    status = IntegerField(constraints=[SQL("DEFAULT 1")], index=True, null=True)
    update_by = TextField(constraints=[SQL("DEFAULT ''::text")], null=True)
    update_time = DateTimeField(null=True)
    username = TextField(constraints=[SQL("DEFAULT ''::text")], index=True)

    class Meta:
        table_name = 'sys_user'

class SysUserDepart(BaseModel):
    dep_id = IntegerField(index=True, null=True)
    user_id = IntegerField(index=True, null=True)

    class Meta:
        table_name = 'sys_user_depart'

class SysUserRole(BaseModel):
    role_id = IntegerField(index=True, null=True)
    user_id = IntegerField(index=True, null=True)

    class Meta:
        table_name = 'sys_user_role'

另外,更好的使用我们的ORM,我们还需要简单做一个封装,如下:

新增一个service,用户专门对数据库的逻辑操作管理

如我们的用户管理逻辑:

from app_demo.models.xcms_system_admin import SysUser,session_scope
from core.helper.json_helper import class_to_dict

class LogicSysUser():
    @classmethod
    def get_user_model_by_username(cls, username=None):

        with session_scope():
            _result = SysUser.get_or_none(SysUser.username == username)
            if not _result:
                # 把对应的结果保存到缓存中---缓存穿透:处理
                return False, '暂无数据'  # 查询没有结果的时候
            result = class_to_dict(_result)
            # 把对应的结果保存到缓存中-时间超时 60*60=一个小时 --10天后过期
            return True, result

然后我们再我们的接口处完整的调一我们的数据库查询接口 新建一个测试数据库记录

然后我们修改之前我们的登入接口:

from app_demo.service.logic_sys_user import LogicSysUser
@route('/user/login',method=['POST', 'GET', 'OPTIONS'])
@validate_back_form(LoginForm)
def index(form):
    # 用户登入
    # 核查数据库信息是否存在此用户信息
    isok, result_user_info = LogicSysUser.get_user_model_by_username(form.username.data)
    if isok:
        if form.username.data == result_user_info.get('username') :
            # 用户密码校验通过返回一个token给用户,让用户后期提交此token来验证

            if form.password.data == result_user_info.get('password'):
                data = {
                    'userid':100001,
                    'username': '小钟同学',
                }
                isok,back_result = jwt_helper.create_token_by_data(secret='woshimiyao',data=data,exp_time=60 * 60 * 24 * 5)
                return CustomizeApiResponse(data={'access_token': back_result.get('access_token')})
            return CustomizeApiResponse(msg='用户密码错误!')
    else:
        return CustomizeApiResponse(msg='该用户还没注册!')

最后我们使用POSTMAN提交接口测试下:

11 、接口自动化测试

测试,是我们的上线生产环境的必要手段,当然有些大公司有专门的测试工程师,对于我们自己来说,写好的接口,进行自动化测试报告也是必须的,毕竟如果不想被别人叨叨的话,我们还是先自己测试一下也是可以的。

关于接口的自动化测试的,我这里使用的是:webtest 和 unittest

所以我们需要在开发环境上还是需要安装这个相关的测试库。编写相关的测试脚本!

这里我们需要用到库有:

  • webtest --测试用例

  • xmlrunner--测试用来输出的时候需要用到的库

安装了上面两个库之后,我们就可以开始写我们的测试用例了

首先在我们的应用充新建一个测试接口文件夹,专门用于存放测试接口用例:

然后我们写我们的测试用例:


from webtest import TestApp
import unittest
from app_demo.app import create_app
import json

class HelloV1Test(unittest.TestCase):
    def setUp(self):
        self.webApp = create_app()
        self.testApp = TestApp(self.webApp)

    def test_api_health_throws_Exception(self):
        _expected_status_code = 200
        resp = self.testApp.get(url='http://127.0.0.1:8889/health', expect_errors=True)
        print(resp.body)
        self.assertEqual(resp.status_int, _expected_status_code,"返回的响应码应该是:".format(_expected_status_code))


然后我们需要编写我们的测试脚本,导入上面编写的,然后一次性生成结果:

test_start.py

if __name__ == '__main__':
    import unittest
    import xmlrunner

    loader = unittest.TestLoader()
    # 打开当前测试目录的相关的测试
    x_tests = loader.discover(start_dir='xtests')
    # 不要以wb形式写入
    with open('./test-results.xml', 'w') as output:
        runner = xmlrunner.XMLTestRunner(output)
        runner.failfast = False
        runner.buffer = False
        runner.verbosity = 2
        runner.run(test=x_tests)

运行test_start.py

查看测试结果test-results.xml内容为:


<?xml version="1.0" ?>
<testsuite errors="0" failures="0" name="test_api_routes.HelloV1Test-20200316110135" tests="1" time="0.011">
	<testcase classname="test_api_routes.HelloV1Test" name="test_api_health_throws_Exception" time="0.011"/>
	<system-out>
<![CDATA[b'OK'
]]>	</system-out>
	<system-err>
<![CDATA[]]>	</system-err>
</testsuite>


(本小节完)

12 、生成依赖文件信息

D:\code\python\local_python\ZBotte>pip freeze > requirements.tx

然后查看我们的依赖配置文件

aiocontextvars==0.2.2
Beaker==1.11.0
beautifulsoup4==4.8.2
blinker==1.4
bottle==0.12.18
certifi==2019.11.28
chardet==3.0.4
colorama==0.4.3
contextvars==2.4
idna==2.9
immutables==0.11
loguru==0.4.1
peewee==3.13.1
psycopg2==2.8.4
PyJWT==1.7.1
redis==2.10.6
requests==2.23.0
six==1.14.0
soupsieve==2.0
urllib3==1.25.8
waitress==1.4.3
WebOb==1.8.6
WebTest==2.0.34
win32-setctime==1.0.1
WTForms==2.2.1
xmlrunner==1.7.7

总结

文章写的太长了,好像提醒告警了!所以我不做太多的说明。后续有修补的地方再修补以下了!

END

好了各位,以上是关于bottle一些基本的封装,主要涵盖了最初序言的那些说明了!先这样呗!

小钟同学 | 文 【原创】【转载请联系本人】| QQ:308711822