实战:开发Python项目管理平台Sailboat 3

接上一节平台存储结构设计工作结束后,接下来搭建基础结构

Sailboat基础结构的搭建

根据Flask文档介绍,我们很快可以搭建一个简单的Flask项目并启动。在sailboat/common.py中写入以下代码:

from flask import Flask
app = Flask(__name__)

然后在sailboat/handler/router.py中写下:

from common import app

接着在sailboat/server.py中写下:

from handler.router import app
if __name__ == '__main__':
    app.run(debug=True, port=3031)

将连接MongoDB数据库的代码写在sailboat/connect.py中:

from pymongo import MongoClient

client = MongoClient("mongodb://localhost:27017/")
databases = client.sailb

创建连接后,就在MongoDB中定义了一个名为sailb的数据库。后续数据库的存取都使用databases。

保存后运行sailboat/server.py文件,此时控制台输出如下内容:
运行
输出内容中的Running on代表Flask服务已经启动,后面跟着的URL为项目首页地址。此时用浏览器访问该地址,会得到Not Found的提示,这是因为我们启动了服务但未编写与视图相关的代码,也未设置路由。那么我们就来写点什么吧!
将以下内容写入到sailboat/handler/index.py中:

from flask.views import MethodView


class IndexHandler(MethodView):
    def get(self):
        return {"message": "Welcome to Sailboat Index.", "code": 200, "data": {}}

然后在sailboat/handler/router.py中引入IndexHandler,并设置路由。对应的代码如下:

from .index import IndexHandler

# 为接口设定版本入口
version = "/api/v1"

# 绑定路由与类视图
app.add_url_rule(version + '/', view_func=IndexHandler.as_view(version + '/'))

由于启动Flask时将debug的值设为True,所以保存代码后Flask会自动重启。此时用浏览器访问http://127.0.0.1:3031/api/v1,页面内容如图
浏览器截图
当用户访问 http://127.0.0.1:3031/api/v1/ 指定的路由时,这个请求就会转发到绑定的视图类,并将视图类的返回值展现给用户。我们只需要在sailboat/handler中编写视图类代码,并将其与路由绑定即可。了解了Flask框架的基本使用方法后,接下来我们将编写用户注册和登录接口。

Sailboat用户注册和登录接口的编写

在sailboat/handler目录下新建名为users的Python文件,按照IndexHandler的编写方法定义RegisterHandler,对应的代码如下:
在这里插入图片描述
根据之前对用户信息结构的设计,我们知道用户注册需要提交用户名、密码、昵称和邮箱,这些数据以JSON格式发送到RegisterHandler的post() 方法中。代码逻辑大体如下:

  • 从客户端提交的JSON中取出用户信息。
  • 判断信息完整性。
  • 判断用户是否为首个用户,并根据判断结果设定用户角色和用户状态。
  • 提取密码字符串的信息摘要,并将信息摘要作为密码和其他信息一并存储到数据库中。

在Flask框架中,取出客户端提交的信息需要用到request对象,在这里导入request:

from flask import request

用户角色和用户状态是固定值,这些可以用枚举来定义。在sailboat/component中新建名为enums的Python文件,并定义用户角色和用户状态。对应的代码如下:
在这里插入图片描述
用户在与Sailboat交互的过程中可能会产生很多异常,这时候需要让用户知道具体的错误原因,而不仅仅是通过400、403等状态码表达,所以这里需要定义一些常见的错误提示,例如not found、no auth和path error等。对应的代码如下:
在这里插入图片描述
完成枚举对象的定义后,到sailboat/handler/user.py中将它们引入:

from components.enums import Role, Status, StatusCode

接下来编写用户注册的主体代码,即将上面整理的代码逻辑变成代码。

RegisterHandler的完整代码
在这里插入图片描述
这里我们还需要编写一个密码加密方法,计算password的加密信息后将计算结果作为用户密码存储到数据库中。在sailboat/component中新建名为utils的Python文件,并写入如下代码:
在这里插入图片描述
将md5_encode引入到sailboat/handler/users.py文件中
以用户密码明文字符串的信息加密作为用户密码是目前广泛使用的用户密码保护手段之一。

用户登录时仅需要输入用户名和密码即可,服务端对这两个值进行校验,并根据校验情况返回登录结果。用户登录的代码逻辑大体如下:

  • 从客户端提交的JSON中取出用户信息。
  • 判断信息完整性。
  • 从数据库中读取相应的数据,校验用户是否存在以及用户状态是否为已激活。
  • 如果用户信息通过校验则生成用户凭证,构造登录结果并返回给客户端。

有了编写注册接口的经验,那么登录接口的编写也就不难了。

在前后端分离的项目中,后端返回的用户凭证通常是Token。Token是包含用户身份信息的加密字符串,加密算法通常是对称加密中的SHA256,也可以选择其他加密算法。
用户身份信息通过服务端的校验后,服务端为用户生成具有时效性的Token,并将Token返回给客户端。往后客户端的每次请求都需要携带Token,而服务端则会将客户端提交的Token解密,并从中拿到用户身份信息。
在这里插入图片描述
如果拿到的用户角色所拥有的权限与当前设定的权限不符,则返回403状态码,并提示无权访问。
Sailboat也采用Token来鉴别用户身份,这里借助第三方库PyJWT实现Token的生成和解密。对应的安装命令如下:

pip install pyjwt

生成和解密的示例代码如下:
在这里插入图片描述
其中用到几个参数:·主体信息。·密钥。·加密算法。

在Sailboat中,主体信息就是用户的身份信息,加密算法选用PyJWT示例中使用的HS256,密钥可以自行定义。注意到密钥在加密和解密时都会用到,所以我们将密钥内容赋值给常量,并放置在sailboat/settings.py文件中:
在这里插入图片描述
密钥的数据类型为字符串,具体内容不受限制,建议采用4字符与短横线连接的组合。

LoginHandler的完整代码。
在这里插入图片描述
在这里插入图片描述
以上就是Sailboat用户注册和登录接口的具体实现,接下来我们将学习权限验证逻辑和对应的代码实现。

Sailboat权限验证装饰器的编写

上一节实现了Token生成的功能,那么问题来了:

  • ·客户端请求时如何携带Token呢?
  • ·在哪里对Token进行校验呢?
  • ·校验哪些信息呢?
  • ·如何判断Token的时效性呢?

请求时携带自定义请求参数的方式有很多,例如将其拼接到URL中、在表单中夹带或者放在请求头中,前端开发工程师和后端开发工程师双方约定好即可。常用的携带方式是放在请求头中,头域字段为Authorization,Token作为值,后端工程师从请求头中取出Authorization头域对应的值即可。带有Authorization的HTTP请求头看上去是这样的:
在这里插入图片描述
Python的装饰器赋予了Python语言极大的灵活性,我们可以为Token的校验工作编写一个装饰器,然后将装饰器应用到需要鉴权的方法上即可。鉴权装饰器的基础结构如下:
在这里插入图片描述
我们只需要将与用户凭证校验相关的代码写在wrapper()方法中即可。之前编写过Token生成的代码,参考PyJWT的文档不难写出Token解密的代码:
在这里插入图片描述
解密结果就是加密前的Token 的主体信息,使用 get()方法便可取出指定键对应的值。这里需要对用户身份进行校验,确保是已注册且状态为已激活的用户,所以需要查询数据库:
在这里插入图片描述
如果用户不存在则返回相应提示。Token 的时效性问题,用当前时间减去Token中的时间,运算结果为负数则代表Token已过期:
在这里插入图片描述
如果Token过期则返回相应提示。
要解决的最后一个问题是鉴权,我们从请求头中取到了Token,但如何判断用户是有权限还是无权限呢?这很简单,在需要鉴权的视图类中设定权限级别,并将鉴权装饰器作用在某个方法上即可。我们将鉴权装饰器写在sailboat/handler/auth.py文件中,然后在需要用到它的文件中引入它。例如,将IndexHandler视图类的权限级别设为Other,并为get()方法加上鉴权装饰器,对应的代码改动如下:
在这里插入图片描述
在鉴权装饰器中,我们可以从args对象中取出视图类设定的permission:
在这里插入图片描述
接着从Token中取出用户角色,通过比较运算判断权限级别,对于权限级别不够的请求返回相应提示:
在这里插入图片描述
至此,我们完成了Sailboat权限验证装饰器的代码编写。
提示:在开发过程中为了减少不必要的权限干扰,可以不使用鉴权功能,待开发完毕后为需要鉴权的方法逐一加上鉴权装饰器即可。

Sailboat项目部署接口和文件操作对象的编写

项目部署实际上是用户将本机上的EGG文件上传到运行着Sailboat的服务器上的过程。其中涉及文件上传和文件存储相关的知识。我们了解了Scrapyd在项目部署过程中对EGG文件的处理流程,看过eggstorage对象源码的读者可知,处理EGG文件的对象中包含几个功能:

  • 文件存储。
  • 文件拷贝。
  • 文件删除。
  • 检查文件是否存在。

文件拷贝功能在项目解包运行时使用,此时会将目标项目的EGG文件拷贝到临时区,解包运行完毕后再删除临时区中对应的EGG文件,这样可以保护项目原EGG文件的完整性,避免多个进程同时读取和解包可能造成的异常。
下图描述了EGG文件副本EGG2的生命周期。
在这里插入图片描述
需要注意的是,文件存取或者删除时通常会遇到因路径不存在引发的异常,所以拷贝、读取或删除前都需要检查文件是否存在。

Sailboat中文件存储对象FileStorages的完成代码。

import os
import logging
import shutil

from settings import FILEPATH, TEMPATH


class FileStorages:

    @staticmethod
    def put(project, version, content):
        """文件存储
        :param project:
        :param version:
        :param content:
        :return:
        """
        # 根据项目名称生成路径
        room = os.path.join(FILEPATH, project)
        if not os.path.exists(room):
            # 如果目录不存在则创建
            os.makedirs(room)
        # 拼接文件完整路径,以时间戳作为文件名
        filenames = os.path.join(room, "%s.egg" % str(version))
        try:
            with open(filenames, 'wb') as f:
                # 写入文件
                f.write(content)
        except Exception as e:
            # 异常处理,打印异常信息
            logging.warning(e)
            return False
        return True

    def get(self):
        pass

    @staticmethod
    def delete(project, version):
        """文件删除状态
        A - 文件或目录存在且成功删除
        B - 文件或目录不存在,无需删除
        :param project:
        :param version:
        :return:
        """
        sign = 'B'
        room = os.path.join(FILEPATH, project)
        if project and version:
            # 删除指定文件
            filename = os.path.join(room, "%s.egg" % str(version))
            if os.path.exists(filename):
                sign = 'A'
                os.remove(filename)
        if project and not version:
            # 删除指定目录
            if os.path.exists(room):
                sign = 'A'
                shutil.rmtree(room)
        return sign

    @staticmethod
    def copy_to_temporary(project, version):
        """根据参数将指定文件拷贝都指定目录
        :param project:
        :param version:
        :return:
        """
        before = os.path.join(FILEPATH, project, "%s.egg" % version)
        after = os.path.join(TEMPATH, "%s.egg" % version)
        if not os.path.exists(before):
            logging.warning("File %s Not Exists" % before)
            return None
        if not os.path.exists(TEMPATH):
            os.makedirs(TEMPATH)
        # 文件拷贝
        shutil.copyfile(before, after)
        return after

    @staticmethod
    def exists(project, version):
        """检查指定项目名称和版本号的文件是否存在
        :param project:
        :param version:
        :return:
        """
        file = os.path.join(FILEPATH, project, "%s.egg" % version)
        if not os.path.exists(file):
            return False
        return True

在sailboat/component中新建名为storage的Python文件,并将FileStorages对象的完整代码写入文件中。从sailboat/settings.py文件中引入的FILEPATH和TEMPATH是Sailboat项目存储EGG文件和执行时用到的临时区目录,它们的具体定义如下:
在这里插入图片描述
用户上传的EGG文件将存储在sailboat/files目录下,而执行时用到的临时区目录的路径则是sailboat/temproary。

编写好文件操作对象后,项目部署的代码就完成一半了。在sailboat/handler中新建名为deploy的Python文件,并编写部署视图类的代码:

import time

from datetime import datetime
from flask.views import MethodView
from flask import request

from components.enums import StatusCode
from components.storage import FileStorages
from components.auth import get_user_info
from connect import databases

storages = FileStorages()


class DeployHandler(MethodView):
    def post(self):
        """项目部署接口"""
        project = request.form.get('project')
        remark = request.form.get('remark')
        file = request.files.get('file')
        if not project or not file:
            # 确保参数和值存在
            return {"message": StatusCode.MissingParameter.value[0], "data": {},
                    "code": StatusCode.MissingParameter.value[1]}, 400
        filename = file.filename
        if not filename.endswith('.egg'):
            # 确保文件类型正确
            return {"message": StatusCode.NotFound.value[0], "data": {},
                    "code": StatusCode.NotFound.value[1]}, 400
        version = int(time.time())
        content = file.stream.read()
        # 将文件存储到服务器
        result = storages.put(project, version, content)
        if not result:
            # 存储失败返回相关提示
            return {"message": StatusCode.OperationError.value[0], "data": {},
                    "code": StatusCode.OperationError.value[1]}, 400

        token = request.headers.get("Authorization")
        idn, username, role = get_user_info(token)
        message = {
            "project": project,
            "version": str(version),
            "remark": remark or "Nothing",
            "idn": idn,
            "username": username,
            "create": datetime.now()
        }
        var = databases.deploy.insert_one(message).inserted_id
        message["_id"] = str(message.pop("_id"))
        return {"message": "success", "data": message, "code": 201}, 201

从用户提交的参数中取出project、file和remark,然后对它们进行一些基础校验,例如完整性和文件类型等。爬虫工程师有时候会运行不同版本的项目,Sailboat设计时也将其考虑在内,我们用时间戳作为EGG文件的版本号。参数校验通过后调用FileStorages对象的put() 方法将EGG文件存储到sailboat/files目录中。接着构造项目信息,并将信息存储到数据库中。最后将项目信息作为部署结果返回给用户。
sailboat/files目录的结构如下:
在这里插入图片描述
用户上传的EGG文件以项目名称作为区分条件,每个项目都有对应的同名目录,而目录中存储的则是该项目不同版本的EGG文件。
项目部署时并没有存储与用户相关的信息,我们需要在构造项目信息时加入用户ID和用户名。这两个信息可以从Token中提取,考虑到这种情形也会出现在其他视图类中,所以将从Token中提取用户信息的代码封装成一个方法,放在sailboat/component/auth.py文件中。对应的代码如下:
在这里插入图片描述
然后将其引入到DeployHandler所在的文件中:

from components.auth import get_user_info

项目部署成功之后,肯定是需要进行调度的,调度执行期间产生的日志和异常信息将被记录下来,Sailboat监控到异常的产生时会对其进行整理,并将整理后的汇总信息通过社交应用软件或邮件发送给平台相关人员。
下图描述了Sailboat项目调度到通知的整个流程。
在这里插入图片描述
Sailboat将通过集成APScheduler实现项目定时调度的功能;项目的执行则通过子进程和上下文管理器实现;异常监控与子进程有关联;消息通知功能将与社交应用软件钉钉结合。整个流程较长,涉及的模块较多,接下来我们将逐一学习具体功能的设计和实现。

Sailboat项目调度接口的编写

项目调度接口与用户注册接口类似,从用户提交的请求中取出参数并进行验证,调用APScheduler中的add_job() 方法实现定时任务,最后将调度信息存储到数据库中。

流程很清晰,根据之前设计的调度信息数据结构和DeployHandler的编写经验,在sailboat/handler中新建名为timer的Python文件,并编写调度视图类的代码:

from uuid import uuid4
from datetime import datetime
from flask.views import MethodView
from flask import request

from executor.actuator import performer
from components.enums import StatusCode
from components.storage import FileStorages
from components.auth import get_user_info
from connect import databases
from common import scheduler

storages = FileStorages()


class TimerHandler(MethodView):
    def post(self):
        project = request.json.get('project')
        version = request.json.get('version')
        mode = request.json.get('mode')
        rule = request.json.get('rule')
        if not project or not rule or not version:
            return {"message": StatusCode.ParameterError.value[0], "data": {},
                    "code": StatusCode.ParameterError.value[1]}, 400
        if not storages.exists(project, version):
            return {"message": StatusCode.NotFound.value[0], "data": {},
                    "code": StatusCode.NotFound.value[1]}, 400
        token = request.headers.get("Authorization")
        idn, username, role = get_user_info(token)
        # 生成唯一值作为任务标识
        jid = str(uuid4())
        # 添加任务,这里用双星号传入时间参数
        try:
            scheduler.add_job(performer)
        except Exception as e:
            return {"message": StatusCode.ParameterError.value[0], "data": {},
                    "code": StatusCode.ParameterError.value[1]}, 400
        message = {
            "project": project,
            "version": version,
            "mode": mode,
            "rule": rule,
            "jid": jid,
            "idn": idn,
            "username": username,
            "create": datetime.now()
        }
        inserted = databases.timers.insert_one(message).inserted_id
        return {"message": "success", "data": {
            "project": project,
            "version": version,
            "jid": jid,
            "inserted": str(inserted)
        },
                "code": 201}, 201

在sailboat/common.py文件中加入以下代码:
在这里插入图片描述
配置好后还需要启动APScheduler服务。
将启动服务的代码放到sailboat/server.py文件中,对应的代码改动如下:
在这里插入图片描述
然后在TimerHandler所在的文件中引入APScheduler实例:

from common import scheduler

保存代码后为TimerHandler设置路由,然后使用Postman工具对调度接口进行测试。
在这里插入图片描述
需要注意的是,TimerHandler中接收的是JSON数据,所以用Postman发送请求时需要设置请求头Content-Type为application/json,参数需要填写在Body面板中的raw框中。点击Send按钮后,服务端返回的信息如下:
在这里插入图片描述
出现parameter error提示是因为我们并未为APScheduler的add_job() 方法指定调用方法名。在Sailboat中,这个被调用的方法就是执行Python项目解包运行的执行器方法名称。

下一节执行器编写和日记生成

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值