Как значения из форм браузера попадают в базу данных, в чем разница между телом и параметрами запроса и каким образом таблица БД связана с классом ООП-скрипта: пишем REST API своего веб-приложения на Python с СУБД SQLLite, фреймворком Flask и тестированием в Postman.
Проектирование REST API
Чтобы на своем курсе по основы архитектуры и интеграции информационных систем показать, как работать с REST-приложениями, включая вызовы HTTP-методов через Postman и элементы веб-интерфейса, я написала свой API на Python, используя фреймворк быстрой разработки приложений Flask. В качестве примера возьмем сервис управления клиентскими заявками, который предоставляет следующие функциональные возможности:
- отправить заявку на курс с указанием имени клиента, адреса его электронной почты и номера телефона;
- просмотреть отдельно взятую заявку;
- просмотреть весь список имеющихся заявок;
- изменить заявку;
- удалить заявку.
Это типичные CRUD-операции, которые отлично реализуются в архитектурном стиле REST. Клиентская заявка представлена следующей структурой данных:
Поле | Смысл | Nullable (может быть не задано пользователем) | Тип данных |
course | Курс, на который подается заявка. Значение выбирается из заранее заданного набора курсов (OAIS, TTIS, BUML) | нет | Строка (str) |
name | Имя клиента | Да | Строка (str) |
Адрес электронной почты клиента, можно проверять по шаблону 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 | { «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 в течение длительности сеанса:
В отдельной ячейке 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.
Связь функций обращения к базе данных с REST API реализована через декорированный метод @route() фреймворка Flask, в параметрах которого указаны конечные точки и возможные HTTP-методы. Чтобы было возможным отправлять данные как через GUI, так и через Postman, используются методы request.form и request.json соответственно. А для изменения данных заявки через параметры PUT-запроса используется метод request.args.
Последовательно запустив все вышеприведенные ячейки в Google Colab, в области вывода после последнего запуска появится ссылка на внешний URL-адрес, созданный с помощью утилиты ngrok.
Этот URL-адрес на поддомене ngrok-free следует скопировать и вставить в адресную строку браузера. При переходе по этому адресу браузер может выдать предупреждение о переходе на подозрительный сайт.
Мой антивирус не пускает пользователей на подозрительные адреса, поэтому я внесла ngrok-домены в список исключений.
После всех этих манипуляций, наконец, можно перейти к работе с созданным веб-сервисом в GUI и через Postman, что мы и рассмотрим далее.
Основы архитектуры и интеграции информационных систем
Код курса
OAIS
Ближайшая дата курса
20 января, 2025
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.
Тестирование созданного веб-сервиса в браузере и через Postman
Поскольку этот демонстрационный пример не предназначен для практического использования, GUI фронтенда выглядит чрезмерно лаконично. Однако, чтобы упростить навигацию по конечным точкам созданного сервера, я добавила гиперссылки на все его 3 HTML-страницы (главная, отправка новой заявки и просмотр списка заявок). А для реализации функции отправки новой заявки в GUI с помощью метода рендеринга HTML-страниц фрейморка Flask создана форма для ввода значений полей.
После отправки заявки методом POST в браузере показывается тело ответа сервера с успешным или неуспешным результатом выполнения. Говоря в терминах REST API, при успешном создании нового ресурса все значения его полей отображаются в браузере в виде JSON-документа. Также можно просмотреть список всех имеющихся заявок, обратившись к конечной точке /apps, куда отправляется GET-запрос.
Поскольку одной из задач моего курса по основы архитектуры и интеграции информационных систем является познакомить начинающих аналитиков с популярным инструментом тестирования REST API под названием Postman, рассмотрим отправку запросов к созданному веб-сервису с помощью этого средства. Сперва получим список всех заявок, отправив GET-запрос к конечной точке /apps.
Добавим новую заявку, отправив POST-запрос через Postman.
Изменим значение поля курс в заявке с id=3, отправив PUT-запрос к конечной точке /apps/3 с параметрами запроса.
Наконец, выполним удаление заявки с id=2, отправив DELETE-запрос к конечной точке /apps/2.
Надеюсь, что этот небольшой демонстрационный пример поможет начинающим аналитикам понять ключевые принципы работы типовых веб-приложений с REST API. Для закрепления знаний вы можете создать свой аналогичный веб-сервис, используя в качестве заготовок мои скрипты в репозитории на Github: https://github.com/AnnaVichugova/PythonApps/blob/main/Flask-app.
Основы архитектуры и интеграции информационных систем
Код курса
OAIS
Ближайшая дата курса
20 января, 2025
Продолжительность
16 ак.часов
Стоимость обучения
36 000 руб.
А подробнее узнать про другие важные аспекты архитектуры и интеграции информационных систем, затронутые в этом материале, вам помогут курсы Школы прикладного бизнес-анализа в нашем лицензированном учебном центре обучения и повышения квалификации системных и бизнес-аналитиков в Москве:
- Разработка ТЗ на информационную систему по ГОСТ и SRS
- Основы архитектуры и интеграции информационных систем
- UML для бизнес-аналитика