关注各种黑科技
B站UP主,编程教学,游戏玩家,挂B,金牛座

使用Flask和Connexion构建Python REST API接口与Swagger文档

这周单独会录一个课程,使用Flask和Connexion构建Python RESTFUl API接口与Swagger文档,边学边录。

首先要说的是,我也没用flask开发过正儿八经的项目,不过python web框架其实都一样,学会一个另一个自然就会用了,但是可能不能提供最好的项目实践方法。所以这个文章也是参考翻译过来的,然后结合自己对flask的了解,再分享一些心(fei)得(hua)。

我们这里会实现一个restful的接口,自带接口文档,并且有一个前端页面调用数据并展示。数据是一些人员信息列表,有个timestamp字段用来追踪记录的更新时间。现在流行前后端分离,不像我们之前在blog课程里,模板里也有一些展示逻辑需要后端程序员处理。

什么是REST?
感觉和MVC一样,都是某种组织结构。

其中最基本的一个概念是CRUD通过http动词来划分,而不是把动作写在URL里。
操作 HTTP 动词
Create POST
Read GET
Update PUT
Delete DELETE

另外一个URL代表一个唯一的对象

当然rest还不值这些,甚至有专门的书来介绍。不过老外定制的标准,国内没几个用的,你看看微信api等国内的大厂没一个套此标准的,我自己做的项目也是没用,所以rest现在还是停留在教科书阶段。标准是好的,但是过于繁琐。

我们的定义

Create POST /api/people Defines a unique URL to create a new person
Read GET /api/people Defines a unique URL to read a collection of people
Read GET /api/people/Farrell Defines a unique URL to read a particular person in the people collection
Update PUT /api/people/Farrell Defines a unique URL to update an existing order
Delete DELETE /api/orders/Farrell Defines a unique URL to delete an existing person

开始
server.py

from flask import (
    Flask,
    render_template
)

# Create the application instance
app = Flask(__name__, template_folder="templates")

# Create a URL route in our application for "/"
@app.route('/')
def home():
    """
    This function just responds to the browser ULR
    localhost:5000/

    :return:        the rendered template 'home.html'
    """
    return render_template('home.html')

# If we're running in stand alone mode, run the application
if __name__ == '__main__':
    app.run(debug=True)

home.html

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Application Home Page</title></head><body><h2>Hello World!</h2></body></html>

使用Connexion添加rest功能
pip install connexion
通过connexion来使用Swagger,同时提供了数据输入输出验证,方便定义URL结构和参数,和API浏览的UI界面。

from flask import render_template
import connexion

# Create the application instance
app = connexion.App(__name__, specification_dir='./')

# Read the swagger.yml file to configure the endpoints
app.add_api('swagger.yml')

# Create a URL route in our application for "/"
@app.route('/')
def home():
    """
    This function just responds to the browser ULR
    localhost:5000/
    :return:        the rendered template 'home.html'
    """
    return render_template('home.html')

# If we're running in stand alone mode, run the application
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

swagger.yml

swagger: "2.0"
info:
  description: This is the swagger file that goes with our server code
  version: "1.0.0"
  title: Swagger Rest Article
consumes:
  - application/json
produces:
  - application/json

basePath: "/api"

# Paths supported by the server application
paths:
  /people:
    get:
      operationId: "people.read"
      tags:
        - "People"
      summary: "The people data structure supported by the server application"
      description: "Read the list of people"
      responses:
        200:
          description: "Successful read people list operation"
          schema:
            type: "array"
            items:
              properties:
                fname:
                  type: "string"
                lname:
                  type: "string"
                timestamp:
                  type: "string"

people.read处理方法实现

from datetime import datetime

def get_timestamp():
    return datetime.now().strftime(("%Y-%m-%d %H:%M:%S"))

# Data to serve with our API
PEOPLE = {
    "Farrell": {
        "fname": "Doug",
        "lname": "Farrell",
        "timestamp": get_timestamp()
    },
    "Brockman": {
        "fname": "Kent",
        "lname": "Brockman",
        "timestamp": get_timestamp()
    },
    "Easter": {
        "fname": "Bunny",
        "lname": "Easter",
        "timestamp": get_timestamp()
    }
}

# Create a handler for our read (GET) people
def read():
    """
    This function responds to a request for /api/people
    with the complete lists of people

    :return:        sorted list of people
    """
    # Create the list of people from our data
    return [PEOPLE[key] for key in sorted(PEOPLE.keys())]

访问获得数据
localhost:5000/api/people

Swagger UI界面
localhost:5000/api/ui

SPA完善

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Application Home Page</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.0/normalize.min.css">
    <link rel="stylesheet" href="static/css/home.css">
    <script
      src="http://code.jquery.com/jquery-3.3.1.min.js"
      integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
      crossorigin="anonymous">
    </script>
</head>
<body>
    <div class="container">
        <h1 class="banner">People Demo Application</h1>
        <div class="section editor">
            <label for="fname">First Name
                <input id="fname" type="text" />
            </label>
            <br />
            <label for="lname">Last Name
                <input id="lname" type="text" />
            </label>
            <br />
            <button id="create">Create</button>
            <button id="update">Update</button>
            <button id="delete">Delete</button>
            <button id="reset">Reset</button>
        </div>
        <div class="people">
            <table>
                <caption>People</caption>
                <thead>
                    <tr>
                        <th>First Name</th>
                        <th>Last Name</th>
                        <th>Update Time</th>
                    </tr>
                </thead>
                <tbody>
                </tbody>
            </table>
        </div>
        <div class="error">
        </div>
    </div>
</body>
<script src="static/js/home.js"></script>
</html>

css

/*
 * This is the CSS stylesheet for the demo people application
 */

@import url(http://fonts.googleapis.com/css?family=Roboto:400,300,500,700);

body, .ui-btn {
    font-family: Roboto;
}

.container {
    padding: 10px;
}

.banner {
    text-align: center;
}

.editor {
    width: 50%;
    margin-left: auto;
    margin-right: auto;
    padding: 5px;
    border: 1px solid lightgrey;
    border-radius: 3px;
    margin-bottom: 5px;
}

label {
    display: inline-block;
    margin-bottom: 5px;
}

button {
    padding: 5px;
    margin-right: 5px;
    border-radius: 3px;
    background-color: #eee;
}

.people {
    width: 50%;
    margin-left: auto;
    margin-right: auto;
    margin-bottom: 5px;
}

table {
    width: 100%;
    border-collapse: collapse;
}

table, caption, th, td {
    border: 1px solid lightgrey;
}

table caption {
    height: 33px;
    font-weight: bold;
    padding-top: 5px;
    border-bottom: none;
}

tr {
    height: 33px;
}

tr:nth-child(even) {
    background-color: #f0f0f0
}

td {
    text-align: center;
}

.error {
    width: 50%;
    margin-left: auto;
    margin-right: auto;
    padding: 5px;
    border: 1px solid lightgrey;
    border-radius: 3px;
    background-color: #fbb;
    visibility: hidden;
}

js

/*
 * JavaScript file for the application to demonstrate
 * using the API
 */

// Create the namespace instance
let ns = {};

// Create the model instance
ns.model = (function() {
    'use strict';

    let $event_pump = $('body');

    // Return the API
    return {
        'read': function() {
            let ajax_options = {
                type: 'GET',
                url: 'api/people',
                accepts: 'application/json',
                dataType: 'json'
            };
            $.ajax(ajax_options)
            .done(function(data) {
                $event_pump.trigger('model_read_success', [data]);
            })
            .fail(function(xhr, textStatus, errorThrown) {
                $event_pump.trigger('model_error', [xhr, textStatus, errorThrown]);
            })
        },
        create: function(fname, lname) {
            let ajax_options = {
                type: 'POST',
                url: 'api/people',
                accepts: 'application/json',
                contentType: 'application/json',
                dataType: 'json',
                data: JSON.stringify({
                    'fname': fname,
                    'lname': lname
                })
            };
            $.ajax(ajax_options)
            .done(function(data) {
                $event_pump.trigger('model_create_success', [data]);
            })
            .fail(function(xhr, textStatus, errorThrown) {
                $event_pump.trigger('model_error', [xhr, textStatus, errorThrown]);
            })
        },
        update: function(fname, lname) {
            let ajax_options = {
                type: 'PUT',
                url: 'api/people/' + lname,
                accepts: 'application/json',
                contentType: 'application/json',
                dataType: 'json',
                data: JSON.stringify({
                    'fname': fname,
                    'lname': lname
                })
            };
            $.ajax(ajax_options)
            .done(function(data) {
                $event_pump.trigger('model_update_success', [data]);
            })
            .fail(function(xhr, textStatus, errorThrown) {
                $event_pump.trigger('model_error', [xhr, textStatus, errorThrown]);
            })
        },
        'delete': function(lname) {
            let ajax_options = {
                type: 'DELETE',
                url: 'api/people/' + lname,
                accepts: 'application/json',
                contentType: 'plain/text'
            };
            $.ajax(ajax_options)
            .done(function(data) {
                $event_pump.trigger('model_delete_success', [data]);
            })
            .fail(function(xhr, textStatus, errorThrown) {
                $event_pump.trigger('model_error', [xhr, textStatus, errorThrown]);
            })
        }
    };
}());

// Create the view instance
ns.view = (function() {
    'use strict';

    let $fname = $('#fname'),
        $lname = $('#lname');

    // return the API
    return {
        reset: function() {
            $lname.val('');
            $fname.val('').focus();
        },
        update_editor: function(fname, lname) {
            $lname.val(lname);
            $fname.val(fname).focus();
        },
        build_table: function(people) {
            let rows = ''

            // clear the table
            $('.people table > tbody').empty();

            // did we get a people array?
            if (people) {
                for (let i=0, l=people.length; i < l; i++) {
                    rows += `<tr><td class="fname">${people[i].fname}</td><td class="lname">${people[i].lname}</td><td>${people[i].timestamp}</td></tr>`;
                }
                $('table > tbody').append(rows);
            }
        },
        error: function(error_msg) {
            $('.error')
                .text(error_msg)
                .css('visibility', 'visible');
            setTimeout(function() {
                $('.error').css('visibility', 'hidden');
            }, 3000)
        }
    };
}());

// Create the controller
ns.controller = (function(m, v) {
    'use strict';

    let model = m,
        view = v,
        $event_pump = $('body'),
        $fname = $('#fname'),
        $lname = $('#lname');

    // Get the data from the model after the controller is done initializing
    setTimeout(function() {
        model.read();
    }, 100)

    // Validate input
    function validate(fname, lname) {
        return fname !== "" && lname !== "";
    }

    // Create our event handlers
    $('#create').click(function(e) {
        let fname = $fname.val(),
            lname = $lname.val();

        e.preventDefault();

        if (validate(fname, lname)) {
            model.create(fname, lname)
        } else {
            alert('Problem with first or last name input');
        }
    });

    $('#update').click(function(e) {
        let fname = $fname.val(),
            lname = $lname.val();

        e.preventDefault();

        if (validate(fname, lname)) {
            model.update(fname, lname)
        } else {
            alert('Problem with first or last name input');
        }
        e.preventDefault();
    });

    $('#delete').click(function(e) {
        let lname = $lname.val();

        e.preventDefault();

        if (validate('placeholder', lname)) {
            model.delete(lname)
        } else {
            alert('Problem with first or last name input');
        }
        e.preventDefault();
    });

    $('#reset').click(function() {
        view.reset();
    })

    $('table > tbody').on('dblclick', 'tr', function(e) {
        let $target = $(e.target),
            fname,
            lname;

        fname = $target
            .parent()
            .find('td.fname')
            .text();

        lname = $target
            .parent()
            .find('td.lname')
            .text();

        view.update_editor(fname, lname);
    });

    // Handle the model events
    $event_pump.on('model_read_success', function(e, data) {
        view.build_table(data);
        view.reset();
    });

    $event_pump.on('model_create_success', function(e, data) {
        model.read();
    });

    $event_pump.on('model_update_success', function(e, data) {
        model.read();
    });

    $event_pump.on('model_delete_success', function(e, data) {
        model.read();
    });

    $event_pump.on('model_error', function(e, xhr, textStatus, errorThrown) {
        let error_msg = textStatus + ': ' + errorThrown + ' - ' + xhr.responseJSON.detail;
        view.error(error_msg);
        console.log(error_msg);
    })
}(ns.model, ns.view));

参考

https://realpython.com/flask-connexion-rest-api/
http://flask.pocoo.org/
https://github.com/zalando/connexion
https://swagger.io/
https://editor.swagger.io swagger配置语法验证
https://github.com/realpython/materials/tree/master/flask-connexion-rest/version_4

转载请注明出处
分享到: 更多 (0)

评论 0