...

Пишем свой REST API на Python с Flask: подробный guide

проектирование и реализация REST API на Python с Flask в Google Colab, курсы по системному и бизнес-анализу для начинающих, основы архитектуры и интеграции ИС для аналитика, проектирование веб-API, веб-API для аналитика, HTTP-запросы и ответы, обучение системных и бизнес-аналитиков основам архитектуры и интеграции ИС, Школа прикладного бизнес-анализа учебный центр Коммерсант

Как значения из форм браузера попадают в базу данных, в чем разница между телом и параметрами запроса и каким образом таблица БД связана с классом ООП-скрипта: пишем REST API своего веб-приложения на Python с СУБД SQLLite, фреймворком Flask и тестированием в Postman.

Проектирование REST API

Чтобы на своем курсе по основы архитектуры и интеграции информационных систем показать, как работать с REST-приложениями, включая вызовы HTTP-методов через Postman и элементы веб-интерфейса, я написала свой API на Python, используя фреймворк быстрой разработки приложений Flask. В качестве примера возьмем сервис управления клиентскими заявками, который предоставляет следующие функциональные возможности:

  • отправить заявку на курс с указанием имени клиента, адреса его электронной почты и номера телефона;
  • просмотреть отдельно взятую заявку;
  • просмотреть весь список имеющихся заявок;
  • изменить заявку;
  • удалить заявку.

Это типичные CRUD-операции, которые отлично реализуются в архитектурном стиле REST. Клиентская заявка представлена следующей структурой данных:

Поле Смысл Nullable (может быть не задано пользователем) Тип данных
course Курс, на который подается заявка. Значение выбирается из заранее заданного набора курсов (OAIS, TTIS, BUML) нет Строка (str)
name Имя клиента Да Строка (str)
email Адрес электронной почты клиента, можно проверять по шаблону xxx@xxx.xxx Да Строка (str)
phone Телефон клиента Да Строка (str)
id Идентификатор клиентской заявки, присваивается автоматически, а не задается пользователем да Целое число (Int)

В виде JSON-документа клиентская заявка представляется так:

{
    "course": "BUML",
    "email": "Kisa@yandex.ru",
    "name": "Kisa",
    "phone": "11111"
}

Само веб-приложение имеет классическую клиент-серверную трехзвенную архитектуру, которую я описывала здесь:

  • клиент (слой представления, фронтэнд) в виде браузера обращается к серверной части (бэкенду) не напрямую, а через веб-сервер (в моем случае Uvicorn — реализация веб-сервера ASGI для Python);
  • веб-сервер принимает HTTP-запросы от клиентов, обогащает их, маршрутизирует к серверному веб-приложению и выдает клиентам полученные от бэкенда HTTP-ответы;
  • серверное веб-приложение обращается к базе данных, направляя соответствующий SQL-запрос по предварительно настроенному подключению;

Чтобы сэкономить время, в качестве базы данных я решила использовать встроенную в Python легковесную SQLite — библиотеку, которая предоставляет облегченную базу данных на диске, не требующую отдельного серверного процесса. Она позволяет обращаться к базе данных с через SQL-запросы и может использоваться Python-приложениями для внутреннего хранения данных. Разумеется, много данных в ней не сохранить, но для демонстрационного примера это и не нужно.

Поскольку цель этого сервиса – научить начинающих аналитиков работать с REST API через инструмент тестирования Postman, я решила реализовать представленные выше функциональные требования в нескольких интерфейсах: через GUI в клиентском браузере и Postman. Причем изменение заявки возможно через отправку данных как в теле PUT-запроса, так и в параметрах, т.е. в адресной строке URL после знака вопроса (?). Чтобы резюмировать проектируемый REST API, сопоставим функциональные требования (ФТ) к веб-сервису с конечными точками (endpoint) и HTTP-запросами:

ФТ Endpoint HTTP-метод Параметры запроса Пример тела запроса Пример тела ответа
отправить заявку /add_app POST Без параметров {

«course»: «BUML»,

«email»: «Kisa@yandex.ru»,

«name»: «Kisa»,

«phone»: «11111»

}

{

«course»: «BUML»,

«email»: «Kisa@yandex.ru»,

«id»: 5,

«name»: «Kisa»,

«phone»: «11111»

}

просмотреть заявку /apps/<app id> GET Без параметров нет тела
просмотреть список заявок /apps GET Без параметров нет тела HTML-страница со списком заявок
изменить заявку /apps/<app id> PUT Course

Name

Phone

Email

{

«course»: «BUML»,

«email»: «987654321@yandex.ru»,

«name»: «Петя»,

«phone»: «987654321»

}

{

«course»: «BUML»,

«email»: «987654321@yandex.ru»,

«id»: 3,

«name»: «Петя»,

«phone»: «987654321»

}

удалить заявку /apps/<app id> DELETE Без параметров нет тела Заявка с id <app id> удалена из базы данных

 

Поскольку я все-таки не разработчик и не часто пишу код, разворачивать полноценную IDE на своем компьютере пока кажется нецелесообразным. Под мои задачи проверки гипотез и быстрого прототипирования вполне хватает возможностей интерактивной среды Google Colab. В этот раз я буду пользоваться ей снова, чтобы запустить скрипт своего Python-приложения, код которого рассмотрим далее.

Разработка ТЗ на информационную систему по ГОСТ и SRS

Код курса
TTIS
Ближайшая дата курса
2 декабря, 2024
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

Реализация на Python в Google Colab

Возвращаясь к моему Pуthon-приложению, запущенному в Google Colab, чтобы получить доступ к нему, нужно использовать веб-сервер, домен, хостинг и пр. Напомню, веб-сервер позволяет обращаться к серверному веб-приложению по протоколам HTTP и HTTPS, поддерживая HTTP-стандарты. Чтобы не тратить время и силы на настройку веб-сервера, я снова воспользуюсь утилитой ngrok, которая позволяет поделиться локальным сервером разработки localhost, создав безопасный туннель с внешнего URL-адреса на локальный компьютер.

Утилита ngrok запускает на локальном компьютере небольшой клиентский процесс, который создает частный туннель подключения к облачной службе ngrok. Локальный сервер разработки localhost сопоставляется с поддоменом ngrok.io, к которому может получить доступ удаленный пользователь. При этом не нужно открывать порты, настраивать пересылки и выполнять прочие действия системного администратора, сложные для большинства аналитиков.

Чтобы воспользоваться возможностями Flask и uvicorn – удаленного веб-сервера для Python, в блокноте Google Colab, сперва следует установить их. Впрочем, для работы мне понадобились не только эти библиотеки, но и другие пакеты, например, Pydantic — библиотека на основе классов данных Python, добавляющая дополнительные функции для валидации и анализа данных. Именно Pydantic предоставляет специальный класс BaseModel, который я использую для определения модели данных клиентской заявки и правил проверки некоторых полей этого класса, таких как email. Чтобы сохранять заявки во встроенную базу данных SQLlite, нужно импортировать библиотеку sqlite3, а также модули многопоточной работы с базой данных и трассировки ошибок. Полный перечень необходимых зависимостей, т.е. устанавливаемых библиотек и импортируемых пакетов показан в следующем участке кода, который надо запускать в отдельной ячейке Google Colab:

#############################ячейка №1 в Google Colab###########################################################
# Установка необходимых библиотек
!pip install flask
!pip install flask-ngrok
!pip install fastapi nest-asyncio pyngrok uvicorn
!pip install pydantic[email]

# Импорт модулей и библиотек
import traceback
import threading

from flask_ngrok import run_with_ngrok
from flask import Flask, jsonify, request, render_template, Response
from pydantic import BaseModel, EmailStr, ValidationError
from enum import Enum
from typing import Union, Optional, List
import os

import sqlite3
import tkinter as tk

За визуальную часть моего приложения отвечают 3 HTML-страницы, которые будут использоваться в качестве шаблонов, лежащих в директории templates в пространстве пользователя Colab в течение длительности сеанса:

REST API FLask HTML Python Google Colab
Создание фротенда для веб-приложения

В отдельной ячейке Colab пишем код для создания директории для хранения HTML-страниц:

#############################ячейка №2 в Google Colab###########################################################
# создаем директорию с шаблонами
!mkdir templates

В следующих 3-х ячейках пишем фронтэнд, т.е. создаем сами HTML-страницы для GUI:

#############################ячейка №3 в Google Colab###########################################################
# создаем html-страницу добавления заявки
%%writefile templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Сервис управления заявками</title>
</head>
<body>
    <h1>Сервис управления заявками</h1>
    <table>
            <tr>
                <td>
                     <a href='/add_app'>Отправить новую заявку</a></td>
                </td>
            </tr>
            <tr>
                <td><a href='/apps'>Смотреть все заявки</a></td>
            </tr>
    </table>
</body>
</html>

#############################ячейка №4 в Google Colab###########################################################
# создаем html-страницу добавления заявки
               #<input type="text" class="form-control" id="course" placeholder="Введите курс" name="course" required>
%%writefile templates/add_app.html
<!DOCTYPE html>
<html>
   <head>
      <title>Отправить заявку</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
   </head>
   <body>
      <div class="container mt-3">
         <h2>Отправить заявку</h2>
         <table>
         <tr>
         <td><a href='/'>ГЛАВНАЯ</a></td>
    <td><a href='/apps'>Все заявки</a></td>
    <td><a href='/add_app'>Добавить заявку</a></td>
    </tr>
    </table>
         <form method="POST" action="/add_app">
            <div class="form-group">
               <label for="course">Курс:</label>
  <select id="course" name="course">
    <option value="OAIS">OAIS - Основы Архитектуры и Интеграции </option>
    <option value="TTIS">TTIS - Разработка ТЗ на ИС</option>
    <option value="BUML">BUML - UML Для бизнес-аналитика</option>
  </select>
            </div>
            <div class="form-group">
               <label for="name">Имя:</label>
               <input type="text" class="form-control" id="name" placeholder="Введите имя" name="name" required>
            </div>
            <div class="form-group">
               <label for="email">Email:</label>
               <input type="email" class="form-control" id="email" placeholder="Введите email" name="email">
            </div>
            <div class="form-group">
               <label for="phone">Телефон:</label>
               <input type="text" class="form-control" id="phone" placeholder="Введите телефон" name="phone">
            </div>
            <button type="submit" class="btn btn-primary">Отправить</button>
         </form>
      </div>
   </body>
</html>

#############################ячейка №5 в Google Colab###########################################################
# создаем html-страницу просмотра списка заявок
%%writefile templates/apps.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Все заявки</title>
</head>
<body>
    <h1>Все заявки:</h1>
         <table>
         <tr>
         <td><a href='/'>ГЛАВНАЯ</a></td>
    <td><a href='/apps'>Все заявки</a></td>
    <td><a href='/add_app'>Добавить заявку</a></td>
    </tr>
    </table>
    <table>
        <thead>
            <tr>
                <th>№ (ID)</th>
                <th>Курс (Course)</th>
                <th>Клиент (Name)</th>
                <th>Почта (Email)</th>
                <th>Телефон (Phone)</th>
            </tr>
        </thead>
        <tbody>
            {% for app in apps %}
            <tr>
                <td>{{ app.id }}</td>
                <td>{{ app.course }}</td>
                <td>{{ app.name }}</td>
                <td>{{ app.email }}</td>
                <td>{{ app.phone }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</body>
</html>

Наконец, пишем сам код Python-приложения:

#############################ячейка №6 в Google Colab###########################################################
# Создание экземпляра Flask-приложения

app = Flask(__name__)

# Установка токена для авторизации в ngrok #@param {type:"string"}
auth_token = "………..персональный токен разработчика ngrok…………" 
os.system(f"ngrok authtoken {auth_token}")

# Запуск приложения с использованием ngrok
run_with_ngrok(app)

# Создание таблицы в базе данных SQLite
conn = sqlite3.connect('example.db')
cursor = conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS apps
                (id INTEGER PRIMARY KEY AUTOINCREMENT, course TEXT, name TEXT, email TEXT, phone TEXT)''')
conn.commit()

# Описание моделей данных
class Courses(str, Enum):
    OAIS = 'OAIS'
    TTIS = 'TTIS'
    BUML = 'BUML'

class App(BaseModel):
    course: Courses
    name: Optional[str] = None
    email: Optional[EmailStr] = None
    phone: Optional[str] = None
    id: Optional[int] = None

class Welcome:
    apps: List[App]
    def init(self, apps: List[App]) -> None:
        self.apps = apps

# Функция добавления записи в базу данных
def add_app_to_db(conn, app):
    cursor = conn.cursor()
    try:
        cursor.execute("INSERT INTO apps (course, name, email, phone) VALUES (?, ?, ?, ?)", (app.course, app.name, app.email, app.phone))
        conn.commit()
        # получаем id после вставки записи
        app.id = cursor.lastrowid 
        return app
    except:
        traceback.print_exc()
        conn.rollback()
    finally:
        cursor.close()

# Функция изменения записи в базе данных
def update_app_in_db(conn, app_id, app_data):
    cursor = conn.cursor()
    try:
        cursor.execute("UPDATE apps SET course=?, name=?, email=?, phone=? WHERE id=?", (app_data.get('course'), app_data.get('name'), app_data.get('email'), app_data.get('phone'), app_id))
        conn.commit()
        return app_data
    except:
        traceback.print_exc()
        conn.rollback()
    finally:
        cursor.close()

# Функция получения всех записей из базы данных
def get_all_apps_from_db(conn):
    cursor = conn.cursor()
    try:
        cursor.execute("SELECT * FROM apps")
        results = cursor.fetchall()
        apps = []
        for row in results:
            app = {"id": row[0], "course": row[1], "name": row[2], "email": row[3], "phone": row[4]}
            apps.append(app)
        return apps
    except:
        traceback.print_exc()
        return 'Ошибка при получении списка заявок из базы данных'
    finally:
        cursor.close()

# Функция получения записи из базы данных по id
def get_app_from_db(conn, app_id):
    cursor = conn.cursor()
    cursor.execute(f"SELECT * FROM apps WHERE id ='{app_id}'")
    result = cursor.fetchone()
    if result:
        app = {"id": result[0], "course": result[1], "name": result[2], "email": result[3], "phone": result[4]}
        return app
    return 'Пользователь не найден'

# Функция удаления записи из базы данных
def delete_app_from_db(conn, app_id):
    cursor = conn.cursor()
    try:
        cursor.execute("DELETE FROM apps WHERE id=?", (app_id,))
        conn.commit()
    except:
        traceback.print_exc()
        conn.rollback()
    finally:
        cursor.close()

# Обработчик запросов по корневому адресу
@app.route('/')
def hello():
    return render_template('index.html')

# Обработчик запросов на получение списка заявок:
@app.route('/apps', methods=['GET'])
def get_apps():
    conn = sqlite3.connect('example.db')
    try:
        apps = get_all_apps_from_db(conn)
        return render_template('apps.html', apps=apps)
    except Exception as e:
        traceback.print_exc()
        return f'Ошибка при получении списка заявок из базы данных: {e}'
    finally:
        conn.close()

# Обработчик добавления новой заявки:
@app.route('/add_app', methods=['POST', 'GET'])
def apps():
    conn = sqlite3.connect('example.db')
    apps = None
    try:
        if request.method == 'POST':
            if request.content_type == 'application/json':
                app_data = request.json
            elif request.form:
                app_data = request.form
            else:
                app_data = request.args
            course = app_data.get('course')
            name = app_data.get('name')
            email = app_data.get('email')
            phone = app_data.get('phone')
            new_app = App(course=course or None, name=name or None, email=email or None, phone=phone or None)
            add_app_to_db(conn, new_app)
            conn.commit()
            apps = get_all_apps_from_db(conn)
            return jsonify(new_app.dict())
        else:
            apps = get_all_apps_from_db(conn)
            return render_template('add_app.html', apps=apps)
    except Exception as e:
        traceback.print_exc()
        if request.method == 'POST':
            return f'Ошибка при добавлении пользователя в базу данных: {e}'
        else:
            return f'Ошибка при получении списка заявок из базы данных: {e}'
    finally:
        conn.close()

# Обработчик изменения заявки по id:
@app.route('/apps/<app_id>', methods=['GET', 'PUT', 'DELETE'])
def apps_op(app_id):
    if request.method == 'GET':
        # Обработчик запросов на получение записи из базы данных по id
        conn = sqlite3.connect('example.db')
        try:
            app = get_app_from_db(conn, app_id)
            if app != 'Пользователь не найден':
                return jsonify(app)
            return 'Пользователь не найден'
        except:
            traceback.print_exc()
            return 'Ошибка при получении заявки из базы данных'
        finally:
            conn.close()
    elif request.method == 'PUT':
        # Обработчик запросов на изменение записи в базе данных
        conn = sqlite3.connect('example.db')
        if request.content_type == 'application/json':
            app_data = request.json
        else:
            app_data = {k: v for k, v in request.args.items() if k in ['course', 'name', 'email', 'phone']}
        try:
            app = get_app_from_db(conn, app_id)
            if app == 'Пользователь не найден':
                return 'Пользователь не найден'
            # Проверяем, были ли введены новые значения полей app
            # Если значение не введено, оставляем прежнее значение из базы данных
            for field in ['course', 'name', 'email', 'phone']:
                if field not in app_data:
                    app_data[field] = app[field]
            update_app_in_db(conn, app_id, app_data)
            updated_app = get_app_from_db(conn, app_id)
            return jsonify(updated_app)
        except:
            traceback.print_exc()
            return 'Ошибка при изменении заявки в базе данных'
        finally:
            conn.close()
    elif request.method == 'DELETE':
        # Обработчик запросов на удаление записи в базе данных
        conn = sqlite3.connect('example.db')
        try:
            delete_app_from_db(conn, app_id)
            return f'Заявка с id {app_id} удалена из базы данных'
        except:
            traceback.print_exc()
            return 'Ошибка при удалении заявки из базы данных'
        finally:
            conn.close()

#Запуск приложения
if __name__ == '__main__':
    app.run()

В этом коде пробрасывается туннель для доступа к локальному серверу разработки из внешнего URL, сгенерированного утилитой ngrok. В качестве аутентификации используется персональный токен разработчика на сайте этой платформы (https://dashboard.ngrok.com/signup), который следует получить заблаговременно, чтобы вставить его в код.

В вышеприведенном коде для сохранения заявок в базу данных SQLlite реализована работа с ней через курсор, исполняющий SQL-запросы. Определены функции записи данных в базу, изменения и удаления, а также чтения данных с фильтрацией по значению поля id.

Flask Python Google Colab REST API
Код Flask-приложения: фрагмент подключения к SQLLite и объявление Python-класса

Связь функций обращения к базе данных с REST API реализована через декорированный метод @route() фреймворка Flask, в параметрах которого указаны конечные точки и возможные HTTP-методы. Чтобы было возможным отправлять данные как через GUI, так и через Postman, используются методы request.form и request.json соответственно. А для изменения данных заявки через параметры PUT-запроса используется метод request.args.

Flask REST API Python
Обработчик POST-запроса в REST API

Последовательно запустив все вышеприведенные ячейки в Google Colab, в области вывода после последнего запуска появится ссылка на внешний URL-адрес, созданный с помощью утилиты ngrok.

ngrok google colab python
Туннелирование с локального хоста на на внешний URL

Этот URL-адрес на поддомене ngrok-free следует скопировать и вставить в адресную строку браузера. При переходе по этому адресу браузер может выдать предупреждение о переходе на подозрительный сайт.

ngrok Python Colab
Предупреждение о переходе на потенциально опасный внешний ресурс

Мой антивирус не пускает пользователей на подозрительные адреса, поэтому я внесла ngrok-домены в список исключений.

AVG антивирус исключения URL
Добавление URL-адресов в список исключений в настройках антивируса

После всех этих манипуляций, наконец, можно перейти к работе с созданным веб-сервисом в GUI и через Postman, что мы и рассмотрим далее.

Основы архитектуры и интеграции информационных систем

Код курса
OAIS
Ближайшая дата курса
20 января, 2025
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

Тестирование созданного веб-сервиса в браузере и через Postman

Поскольку этот демонстрационный пример не предназначен для практического использования, GUI фронтенда выглядит чрезмерно лаконично. Однако, чтобы упростить навигацию по конечным точкам созданного сервера, я добавила гиперссылки на все его 3 HTML-страницы (главная, отправка новой заявки и просмотр списка заявок). А для реализации функции отправки новой заявки в GUI с помощью метода рендеринга HTML-страниц фрейморка Flask создана форма для ввода значений полей.

После отправки заявки методом POST в браузере показывается тело ответа сервера с успешным или неуспешным результатом выполнения. Говоря в терминах REST API, при успешном создании нового ресурса все значения его полей отображаются в браузере в виде JSON-документа. Также можно просмотреть список всех имеющихся заявок, обратившись к конечной точке /apps, куда отправляется GET-запрос.

GUI REST API
GUI (фронтенд) созданного веб-приложения REST API

Поскольку одной из задач моего курса по основы архитектуры и интеграции информационных систем является познакомить начинающих аналитиков с популярным инструментом тестирования REST API под названием Postman, рассмотрим отправку запросов к созданному веб-сервису с помощью этого средства. Сперва получим список всех заявок, отправив GET-запрос к конечной точке /apps.

GET Postman REST API
GET-запрос в Postman

Добавим новую заявку, отправив POST-запрос через Postman.

POST Postman REST API
POST-запрос к REST API через Postman

Изменим значение поля курс в заявке с id=3, отправив PUT-запрос к конечной точке /apps/3 с параметрами запроса.

PUT Postman REST API
PUT-запрос к REST API через Postman

Наконец, выполним удаление заявки с id=2, отправив DELETE-запрос к конечной точке /apps/2.

DELETE Postman REST API
DELETE -запрос к REST API через Postman

Надеюсь, что этот небольшой демонстрационный пример поможет начинающим аналитикам понять ключевые принципы работы типовых веб-приложений с REST API. Для закрепления знаний вы можете создать свой аналогичный веб-сервис, используя в качестве заготовок мои скрипты в репозитории на Github: https://github.com/AnnaVichugova/PythonApps/blob/main/Flask-app.

Основы архитектуры и интеграции информационных систем

Код курса
OAIS
Ближайшая дата курса
20 января, 2025
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.

А подробнее узнать про другие важные аспекты архитектуры и интеграции информационных систем, затронутые в этом материале, вам помогут курсы Школы прикладного бизнес-анализа в нашем лицензированном учебном центре обучения и повышения квалификации системных и бизнес-аналитиков в Москве:

 

Я даю свое согласие на обработку персональных данных и соглашаюсь с политикой конфиденциальности.

Добавить комментарий