...

Реализация REST API интернет-магазина с Python-фреймворком Flask в Google Colab

разработка веб-приложения Flask Python пример, проектирование и реализация REST API для аналитика пример, Flask REST API аутентификация пример, как спроектировать и реализовать REST API, архитектура информационных систем примеры курсы обучение, системный анализ на пальцах простой пример для начинающих, техники бизнес-анализа, обучение аналитиков, техники системного анализа для начинающих, UML примеры с кодом, обучение начинающих аналитиков, курсы бизнес-анализа, курсы системного анализа, UML-диаграмма классов для Python-кода пример, Школа прикладного бизнес-анализа Учебный центр Коммерсант

Простая реализация REST-приложения на примере интернет-магазина с помощью Python-фреймворка Flask, Google Colab, утилиты для тунеллирования ngrok и базы данных PostgreSQL в облачной платформе Neon.

Слой представления: создаем HTML-страницы

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

  • Слой представления выполнен в виде пользовательского интерфейса веб-сайта, т.е. совокупности связанных HTML-страниц;
  • Слой бизнес-логики реализуется Flask-приложением, запускаемом в Goggle Colab и туннелированном с помощью утилиты ngrok, чтобы получить доступ к локальному хосту этой виртуальной машины через внешний URL-адрес;
  • Слой доступа к данным – это база данных PostgreSQL, развернутая в облачной платформе Neon.

Сперва создадим клиентскую часть, т.е. HTML-страницы. Фреймворк Flask использует шаблоны HTML-страниц для визуализации форм ввода и вывода данных. Эти шаблоны страниц, наполняемые динамически, т.е. значениями переменных, например, строками из БД и пр., должны храниться в директории /templates. А статические данные, например, таблица стилей для HTML-файлов, чтобы они выглядели единообразно, и картинки хранятся в директории /static.

Flask Google Colab Python REST API
Создание шаблонов HTML-страниц и статических файлов для клиентской части

Для этого в Google Colab нужен следующий код. Сперва надо установить библиотеки и импортировать пакеты:

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

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

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

import tkinter as tk
import jwt
from datetime import date, datetime, timedelta

Затем создать директории для хранения статических файлов  и динамических шаблонов HTML-страниц:

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

Создадим шаблоны HTML-страниц в виде файлов с расширением .html. Например, для создания страницы аутентификации, т.е. входа в систему, используется следующий код:

#############################новая ячейка в Google Colab###########################################################
# создаем html-страницу входа в систему
%%writefile templates/login.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">
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.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='/login'>Войти в ЛК</a></td>
                </td>
                <td>
                     <a href='/product'>Каталог товаров</a>
                </td>
                <td>
                     <a href='/provider'>Список поставщиков</a>
                </td>
                <td>
                     <a href='/order'>Заказы</a>
                </td>
            </tr>
          </table>
         <form method="POST" action="/login">
            <div class="form-group">
               <label for="login">Имя:</label>
               <input type="email" class="form-control" id="login" placeholder="логин (емейл)" name="login" required>
            </div>
            <div class="form-group">
               <label for="password">Пароль:</label>
               <input type="password" class="form-control" id="password" placeholder="пароль" name="password" required>
            </div>
            <div class="form-group">
               <label for="role">Роль:</label>
               <select id="role" name="role">
               <option value="customer">Покупатель</option>
               <option value="manager">Менеджер интернет-магазина</option>
               <option value="operator">Оператор склада</option>
               </select>
            </div>
            <button type="submit" class="btn btn-primary">ВОЙТИ</button>
         </form>
      </div>
        <img src="{{ url_for('static', filename='backpic.png') }}" style="display: block; margin-top: 20px;">
   </body>
</html>
аутентификация в веб-приложениях HTML
Внешний вид страницы входа в систему

Для страницы просмотра всех товаров нужно выводить их полный список. Для этого следует встроить в код HTML-шаблона переменные, т.е. атрибуты класса Product, заключив их в двойные фигурные скобки:

#############################новая ячейка в Google Colab###########################################################
# создаем html-страницу просмотра каталога товаров
%%writefile templates/product.html
<!DOCTYPE html>
<html lang="en">
<head>
      <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">
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
    <title>Каталог товаров интернет-магазина</title>
</head>
<body>
    <h1>Каталог товаров интернет-магазина:</h1>
          <table>
            <tr>
                <td>
                     <a href='/login'>Войти в ЛК</a></td>
                </td>
                <td>
                     <a href='/product'>Каталог товаров</a>
                </td>
                <td>
                  <a href='/new_product'>Добавить товар</a>
                </td>
                 <td>
                     <a href='/provider'>Список поставщиков</a>
                </td>
                <td>
                     <a href='/order'>Заказы</a>
                </td>
            </tr>
          </table>
    <table>
        <thead>
            <tr>
                <th>Товар </th>
                <th>Поставщик</th>
                <th>Цена</th>
                <th>Количество</th>
            </tr>
        </thead>
        <tbody>
            {% for product in products %}
            <tr>
                <td>{{ product.name }}</td>
                <td>{{ product.provider }}</td>
                <td>{{ product.price }}</td>
                <td>{{ product.quantity }}</td>
                <td>
                <form method="POST" action="/product/{{ product.id }}">
                <button value="{{ product.id }}" type="submit">Изменить</button>
                </form>
                </td>
                </td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
</body>
</html>
рендеринг HTML-страницы с переменными
Список товаров магазина

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

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

Чтобы изменить конкретный товар методом PUT или удалить его методом DELETE, как запланировано в спецификации OpenAPI, приходится переопределять HTTP-метод, поскольку согласно стандарту HTML, к кнопкам с надписью «Изменить» может быть привязан только метод GET или POST. Эту замену приходится делать на странице изменения или удаления конкретного товара, и даже добавлять JavaScript для вызова соответствующих методов HTTP:

#############################новая ячейка в Google Colab###########################################################
# создаем html-страницу изменения товара
%%writefile templates/update_product.html
<!DOCTYPE html>
<html lang="en">
<head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
    <title>Изменение поставщика</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Expires" content="0">
</head>
<body>
    <h1>Изменение товара</h1>
    <table>
    <tr>
    <td>
    <form id="form" method="POST" action="/product/{{ product.id }}" onSubmit="myFunctionName(); return false">
      <input type="hidden" name="_method" value="PUT">
        <label for="name">Название:</label>
        <input type="text" class="form-control" id="name" placeholder="название товара" name="name" value="{{ product.name }}"><br><br>
        <label for="provider">Поставщик:</label>
        <input type="number" class="form-control" id="provider" placeholder="поставщик товара" name="provider" value="{{ product.provider }}"><br><br>
        <label for="price">Цена:</label>
        <input type="number" step="0.01" class="form-control" id="price" placeholder="цена товара" name="price" value="{{ product.price }}"><br><br>
        <label for="quantity">Количество:</label>
        <input type="number" class="form-control" id="quantity" placeholder="количество товара" name="quantity" value="{{ product.quantity }}"><br><br>
        <button type='button' id='change-button'>Изменить</button>
    </form>
    </td>
    <td>
    <button id='delete-button'>Удалить</button>
    </td>
    </tr>
    </table>
    <img src="{{ url_for('static', filename='backpic.png') }}" style="display: block; margin-top: 20px;">
  <script>
   $(document).ready(function(){

    $("#delete-button").on("click", () => {
     $.ajax({
      url: "/product/{{ product.id }}",
      method: "DELETE",
      headers: {
      "X-my": "aaaaaaaaaaaaaaaaaaaaaaaa"
    },
    success : function () {
      window.location.href = "/product";
    }
    });
    });

    $("#change-button").on("click", () => {
  $.ajax({

    url: "/product/{{ product.id }}",
    method: "PUT",
  data:$("#form").serialize(),
    headers: {
      "X-my": "aaaaaaaaaaaaaaaaaaaaaaaa"
    },
    success : function () {
      window.location.href = "/product";
    },
error: function (jqXHR, exception) {
  if (jqXHR.status === 0) {
    alert('Not connect. Verify Network.');
  } else if (jqXHR.status == 404) {
    alert('Requested page not found (404).');
  } else if (jqXHR.status == 500) {
    alert('Internal Server Error (500).');
  } else if (exception === 'parsererror') {
    alert('Requested JSON parse failed.');
  } else if (exception === 'timeout') {
    alert('Time out error.');
  } else if (exception === 'abort') {
    alert('Ajax request aborted.');
  } else {
    alert('Uncaught Error. ' + jqXHR.responseText);
  }}
});
});
    });
  </script>

</body>
</html>

Пример страницы, отрисованной по этому шаблону для ресурса, доступного по маршруту /product/83, где 83 – это идентификатор конкретного товара.

переопределение методов HTML-кнопок с Javascript
Пример HTML-страницы с Java-скриптом для переопределения методов HTML-кнопок

Все HTML-страницы полностью представлены в моем Github-репозитории. Закончив с представлениями, перейдем к модели, т.е. классам.

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

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

Классы модели данных и функции работы с БД

Пока в этом приложении классы модели данных я использую только для вывода списка товаров, поставщиков и заказов в соответствующих HTML-страницах. Чтобы отобразить массив таких объектов, считанных из базы данных, следует определить их классы, а затем агрегировать. Сперва построим модель в UML-диаграммы классов.

UML-диаграмма классов
UML-диаграмма классов

Эта диаграмма создана следующим PlantUML-скриптом:

@startuml
class Provider {
  - id: int
  - name: Optional[str]
  - phone: Optional[str]
  - email: Optional[EmailStr]
  - address: Optional[str]
}

class Product {
  - id: int
  - name: Optional[str]
  - provider: Optional[Provider]
  - price: Optional[float]
  - quantity: Optional[int]
}

class Order {
  - id: int
  - customer: str
  - state: str
  - delivery: Optional[str]
  - sum: Optional[float]
  - date: Optional[date]
}

class Welcome {
  - products: List[Product]
  - providers: List[Provider]
  - orders: List[Order]
  + init(apps: List[Product], providers: List[Provider], orders: List[Order]) -> None
}

Provider --> Product
Welcome o--> Product
Welcome o--> Provider
Welcome o--> Order
@enduml

Python-код по этой диаграмме:

#############################новая ячейка в Google Colab###########################################################
# Описание моделей данных
class Provider(BaseModel):
    id: int
    name: Optional[str] = None
    phone: Optional[str] = None
    email: Optional[EmailStr] = None
    address: Optional[str] = None

class Product(BaseModel):
    id: int
    name: Optional[str] = None
    provider:  Optional[Provider] = None
    price: Optional[float] = None
    quantity: Optional[int] = None

class Order(BaseModel):
    id: int
    customer: str
    state:  str
    delivery: Optional[str] = None
    sum: Optional[float]
    date: Optional[date]

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

    providers: List[Provider]
    def init(self, apps: List[Provider]) -> None:
        self.providers = providers

    orders: List[Order]
    def init(self, apps: List[Order]) -> None:
        self.orders = orders

DDD, ООП и UML для аналитика

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

На этом работа с данными не закончена: необходимо определить функции работы с базой данных, которые непосредственно выполняют SQL-запросы. За это отвечает следующая ячейка Colab с Python-кодом:

#############################новая ячейка в Google Colab###########################################################
# Строка подключения к БД
global connection_string
connection_string='postgres://my_database' #сюда вставлять СВОЮ строку подключения к СВОЕЙ БД
conn = psycopg2.connect(connection_string)

# Функция получения всех записей из таблицы товаров для отображения каталога
def read_product_db(conn):
    cursor = conn.cursor()
    try:
        cursor.execute("SELECT product.id, product.name, provider.name, product.price, product.quantity FROM product JOIN provider on provider.id=product.provider ORDER BY product.id DESC")
        results = cursor.fetchall()
        products = []
        for row in results:
            Product = {"id":row[0], "name": row[1], "provider": row[2], "price": row[3], "quantity": row[4]}
            products.append(Product)
        return products
    except:
        traceback.print_exc()
        return 'Ошибка при получении товаров из базы данных'
    finally:
        cursor.close()

# Функция получения всех записей из таблицы заказов для отображения списка
def read_order_db(conn):
    cursor = conn.cursor()
    try:
        cursor.execute("SELECT orders.id, customer.name, order_states.name, delivery.address, orders.sum, orders.date FROM orders JOIN customer on customer.id=orders.customer JOIN order_states on order_states.id=orders.state JOIN delivery on delivery.id=orders.delivery ORDER BY orders.id DESC")
        results = cursor.fetchall()
        orders = []
        for row in results:
            Order = {"id":row[0], "customer": row[1], "state": row[2], "delivery": row[3], "sum": row[4], "date": row[5]}
            orders.append(Order)
        return orders
    except:
        traceback.print_exc()
        return 'Ошибка при получении заказов из базы данных'
    finally:
        cursor.close()

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

# Функция поиска пользователя в базе данных
def find_user_in_db(conn, login, password, role):
    cursor = conn.cursor()
    try:
      cursor.execute("SELECT * FROM users WHERE login = %s AND password = %s AND role=%s", (login, password, role))
      result = cursor.fetchone()
      if result:
        user = {"id": result[0], "login": result[1], "password": result[2], "role": result[3]}
        return user
      else:
        return None
    except:
        traceback.print_exc()
        return 'Ошибка при получении пользователя из базы данных'
    finally:
        cursor.close()

# Функция регистрации пользователя (добавления записи в таблицу users)
def add_user_to_db(conn, user_data):
    cursor = conn.cursor()
    try:
        cursor.execute("SELECT MAX(id) FROM users")
        id = cursor.fetchone()[0] + 1
        cursor.execute("INSERT INTO users (id, login, password, role) VALUES (%s, %s, %s, %s)", (id, user_data['login'], user_data['password'], user_data['role']))
        conn.commit()
        user = {"id": id, "login": user_data['login'], "password": user_data['password'], "role": user_data['role']}
        return user
    except Exception as e:
        traceback.print_exc()
        conn.rollback()
        return None
    finally:
        cursor.close()

# Функция генерации JWT-токена
def generate_jwt_token(user_id, role, expiration_minutes):
  if role=='manager':
    # Задаем время жизни токена
    expiration_time = datetime.utcnow() + timedelta(minutes=expiration_minutes)

    # Создаем словарь с данными пользователя, которые будут включены в токен
    payload = {
        "user_id": user_id,
         "role": role,
        "exp": expiration_time
    }

    # Генерируем JWT токен с помощью секретного ключа
    token = jwt.encode(payload, secret_key, algorithm="HS256")
    return token

# Функция поиска токена в базе данных
def find_token_in_db(conn, sysuser):
  cursor = conn.cursor()
  try:
    cursor.execute("SELECT token FROM jwts WHERE sysuser = %s::integer", (sysuser,))
    token = cursor.fetchone()
    print('find_token_in_db ', token)
    return token
  except:
        traceback.print_exc()
        return 'Ошибка при получении токена из базы данных'
  finally:
        cursor.close()

# Функция извлечения данных пользователя из токена
def extract_user_data_from_token(token):
    try:
        # Декодируем токен для получения данных пользователя
        decoded_token = jwt.decode(token, secret_key, algorithms=["HS256"])
        user_id = decoded_token["user_id"]
        sysuser = user_id["id"]
        if find_token_in_db(conn, sysuser):
          return decoded_token
    except jwt.exceptions.ExpiredSignatureError:
        print("Время жизни токена истекло, надо снова войти в систему")
        response = make_response(render_template('login.html'), 401)
        return response
    except jwt.exceptions.DecodeError:
        return None

# Функция записи JWT-токена в таблицу jwts
def add_JWT_to_db(conn, JWTtime, token, sysuser):
    cursor = conn.cursor()
    try:
        cursor.execute("SELECT MAX(id) FROM jwts")
        id = cursor.fetchone()[0] + 1
        cursor.execute("INSERT INTO jwts (id, published, token, sysuser) VALUES (%s, %s, %s, %s)", (id, JWTtime, token, sysuser))
        conn.commit()
        return token
    except Exception as e:
        traceback.print_exc()
        conn.rollback()
        return None
    finally:
        cursor.close()

# Функция добавления товара в таблицу product
def add_product_to_db(conn, name, provider, price, quantity):
    cursor = conn.cursor()
    try:
        cursor.execute("SELECT MAX(id) FROM product")
        id = cursor.fetchone()[0] + 1
        cursor.execute("INSERT INTO product (id, name, provider, price, quantity) VALUES (%s::integer, %s, %s::integer, %s::double precision, %s::integer)", (id, name, provider, price, quantity))
        conn.commit()
        product = {"id":id, "name": name, "provider": provider, "price": price, "quantity": quantity}
        return product
    except Exception as e:
        traceback.print_exc()
        conn.rollback()
        return None
    finally:
        cursor.close()

# Функция добавления поставщика в таблицу provider
def add_provider_to_db(conn, name, phone, email, address):
    cursor = conn.cursor()
    try:
        cursor.execute("SELECT MAX(id) FROM provider")
        id = cursor.fetchone()[0] + 1
        conn.commit()
        cursor.execute("INSERT INTO provider (id, name, phone, email, address) VALUES (%s::integer, %s, %s, %s, %s)", (id, name, phone, email, address))
        conn.commit()
        provider = {"id":id, "name": name, "phone": phone, "email": email, "address": address}
        return provider
    except Exception as e:
        traceback.print_exc()
        conn.rollback()
        return None
    finally:
        cursor.close()

# Функция поиска поставщика в базе данных
def get_provider_from_db(conn, provider_id):
    cursor = conn.cursor()
    try:
        cursor.execute("SELECT * FROM provider WHERE id = (%s::integer)", (provider_id,))
        provider_data = cursor.fetchone()
        if provider_data:
            provider = {
                "id": provider_data[0],
                "name": provider_data[1],
                "phone": provider_data[2],
                "email": provider_data[3],
                "address": provider_data[4]
            }
            return provider
        else:
            return None
    except Exception as e:
        traceback.print_exc()
        return None
    finally:
        cursor.close()

# Функция обновления поставщика в базе данных
def update_provider_in_db(conn, id, name, phone, email, address):
    cursor = conn.cursor()
    try:
        cursor.execute("UPDATE provider SET name = %s, phone = %s, email = %s, address = %s WHERE id = %s::integer", (name, phone, email, address, id))
        conn.commit()
        if cursor.rowcount > 0:
            provider = {
                "id": id,
                "name": name,
                "phone": phone,
                "email": email,
                "address": address
            }
            return provider
        else:
            return None
    except Exception as e:
        traceback.print_exc()
        conn.rollback()
        return None
    finally:
        cursor.close()

# Функция удаления поставщика в базе данных
def delete_provider_from_db(conn, provider_id):
    cursor = conn.cursor()
    try:
        cursor.execute("DELETE FROM provider WHERE id = (%s::integer)", (provider_id,))
        conn.commit()
        if cursor.rowcount > 0:
            return True
        else:
            return False
    except Exception as e:
        traceback.print_exc()
        conn.rollback()
        return False
    finally:
        cursor.close()

# Функция поиска товара в базе данных
def get_product_from_db(conn, product_id):
    cursor = conn.cursor()
    try:
        cursor.execute("SELECT * FROM product WHERE id = (%s::integer)", (product_id,))
        product_data = cursor.fetchone()
        if product_data:
            product = {
                "id": product_data[0],
                "name": product_data[1],
                "provider": product_data[2],
                "price": product_data[3],
                "quantity": product_data[4]
            }
            print('get_product_from_db', product)
            return product
        else:
            return None
    except Exception as e:
        traceback.print_exc()
        return None
    finally:
        cursor.close()

# Функция обновления товара в базе данных
def update_product_in_db(conn, id, name, provider, price, quantity):
    cursor = conn.cursor()
    try:
        cursor.execute("UPDATE product SET name = %s, provider = %s::integer, price = %s::double precision, quantity = %s::integer WHERE id = %s::integer", (name, provider, price, quantity, id))
        conn.commit()
        if cursor.rowcount > 0:
            product = {
                "id": id,
                "name": name,
                "provider": provider,
                "price": price,
                "quantity": quantity
            }
            print("update_product_in_db", product)
            return product
        else:
            return None
    except Exception as e:
        traceback.print_exc()
        conn.rollback()
        return None
    finally:
        cursor.close()

# Функция удаления товара в базе данных
def delete_product_from_db(conn, product_id):
    cursor = conn.cursor()
    try:
        cursor.execute("DELETE FROM product WHERE id = (%s::integer)", (product_id,))
        conn.commit()
        if cursor.rowcount > 0:
            return True
        else:
            return False
    except Exception as e:
        traceback.print_exc()
        conn.rollback()
        return False
    finally:
        cursor.close()

# Функция поиска заказа в базе данных
def get_order_from_db(conn,order_id):
    cursor = conn.cursor()
    try:
        cursor.execute("SELECT orders.id, customer.name, order_states.name, delivery.address, orders.sum, orders.date FROM orders JOIN customer on customer.id=orders.customer JOIN order_states on order_states.id=orders.state JOIN delivery on delivery.id=orders.delivery WHERE orders.id = (%s::integer)", (order_id,))
        order_data = cursor.fetchone()
        if order_data:
            order = {
                "id": order_data[0],
                "customer": order_data[1],
                "state": order_data[2],
                "delivery":order_data[3],
                "sum": order_data[4],
                "date": order_data[5]
            }
            return order
        else:
            return None
    except Exception as e:
        traceback.print_exc()
        return None
    finally:
        cursor.close()

# Функция обновления заказа в базе данных
def update_order_in_db(conn, id, state):
    cursor = conn.cursor()
    try:
        cursor.execute("UPDATE orders SET state = %s::integer WHERE id = (%s::integer)", (state, id))
        conn.commit()
        cursor.execute("SELECT * FROM orders WHERE id = (%s::integer)", (id,))
        order_data = cursor.fetchone()
        if order_data:
            order = {
                "id": order_data[0],
                "customer": order_data[1],
                "state": order_data[2],
                "delivery":order_data[3],
                "sum": order_data[4],
                "date": order_data[5]
            }
            print("update_order_in_db:", order)
            return order
        else:
            return None
    except Exception as e:
        traceback.print_exc()
        conn.rollback()
        return None
    finally:
        cursor.close()

Далее перейдем к самому интересному, т.е. определению маршрутов веб-прилоения и HTTP-методов работы с ними.

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

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

Создание Flask-приложения

Конечная точка REST-приложения во фреймворке Flask помечается декорированным методом @route, в параметрах которого указывается URL-адрес маршрута и методы доступа к нему. Например, следующий код показывает, что к маршруту /product можно обратиться методами GET и POST, при этом далее определяется функция get_product().

# Обработчик запросов на получение списка товаров:
@app.route('/product', methods=['GET', 'POST'])
def get_product():
    # Подключение к PostgreSQL
    conn = psycopg2.connect(connection_string)

    try:
        if request.method == 'GET':
            products = read_product_db(conn)
            return render_template('product.html', products=products)
        else:
            if request.content_type == 'application/json':
               new_product = request.json
            elif request.form:
              new_product = request.form

            name = new_product.get('name')
            provider = new_product.get('provider')
            price = new_product.get('price')
            quantity = new_product.get('quantity')

            # Добавление нового товара в базу данных
            product = add_product_to_db(conn, name, provider, price, quantity)
            return redirect('/product')
    except Exception as e:
        traceback.print_exc()
        return f'Ошибка при получении товаров из базы данных: {e}'
    finally:
        conn.close()

В блоке try выполняются действия в зависимости от типа запроса. Если это GET-запрос, то выполняется функция read_product_db для получения списка товаров из базы данных, а затем отображается шаблон ‘product.html’ с передачей списка товаров в него.

Метод GET используется для получения списка товаров из базы данных, а метод POST — для добавления нового товара, что доступно только аутентифицированному пользователю с ролью Менеджер. Как это реализовано с помощью JWT-токенов, зашитых в куки-файлы клиентских запросов, я писала здесь. При выполнении POST-запроса создается новый объект new_product с информацией о новом товаре из формы HTML-страницы или содержания JSON-документа, который может быть отправлен через Postman. Из объекта new_product извлекаются данные о товаре (название, поставщик, цена и количество), и используются в качестве параметров вызова функции add_product_to_db(). Эта функция непосредственно работает с БД и была описана ранее. После этого происходит перенаправление на страницу со списком товаров.

Функции обработки запроса заключены в блок try-except для отлова ошибок (исключений) без сбоя программы. При возникновении исключения выводится сообщение об ошибке, а затем закрывается соединение с базой данных в блоке finally.

Весь код Flask-приложения выглядит следующим образом:

#############################новая ячейка в Google Colab###########################################################
# Установка токена для авторизации в ngrok #@param {type:"string"}
auth_token = "my-token-for-ngrok" #сюда вставить СВОЙ ngrok-токен, полученный здесь https://dashboard.ngrok.com/
os.system(f"ngrok authtoken {auth_token}")

########################################################################################
# Задаем секретный ключ для подписи JWT
secret_key = "SecretKey4JWT" #здесь задать СВОЙ секретный ключ для формирования JWT-токена
# Задаем время жизни JWT-токена в минутах
exp_time=15

# Создание экземпляра Flask-приложения
app = Flask(__name__)

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

# set the secret key для кодирования сессии:
app.secret_key = secret_key

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

# Обработчик запросов на получение списка товаров:
@app.route('/product', methods=['GET', 'POST'])
def get_product():
    # Подключение к PostgreSQL
    conn = psycopg2.connect(connection_string)

    try:
        if request.method == 'GET':
            products = read_product_db(conn)
            return render_template('product.html', products=products)
        else:
            if request.content_type == 'application/json':
               new_product = request.json
            elif request.form:
              new_product = request.form

            name = new_product.get('name')
            provider = new_product.get('provider')
            price = new_product.get('price')
            quantity = new_product.get('quantity')

            # Добавление нового товара в базу данных
            product = add_product_to_db(conn, name, provider, price, quantity)
            return redirect('/product')
    except Exception as e:
        traceback.print_exc()
        return f'Ошибка при получении товаров из базы данных: {e}'
    finally:
        conn.close()

# Обработчик запросов на добавление нового товара:
@app.route('/new_product', methods=['POST', 'GET'])
def add_product():
    # Получаем токен из заголовка HTTP-запроса
    token = request.cookies.get('token')

    # Подключение к PostgreSQL
    conn = psycopg2.connect(connection_string)
    try:
        if token:
            # Проверяем валидность токена и получаем данные пользователя
            user_data = extract_user_data_from_token(token)
            if user_data:
                # Проверяем, что пользователь имеет роль "manager"
                if user_data['role'] == 'manager':
                    if request.method == 'POST':
                      response = make_response(render_template('product.html'), 200)
                      return response
                    else:
                      return render_template('new_product.html')
                else:
                    print("У вас нет доступа к этой странице")
                    response = make_response(render_template('login.html'), 403)
                    return response
            else:
                print("Неверный токен")
                response = make_response(render_template('login.html'), 403)
                return response
        else:
            print("Требуется авторизация")
            response = make_response(render_template('login.html'), 401)
            return response
            #return redirect(url_for('login'))
    except Exception as e:
        traceback.print_exc()
        return f'Ошибка при обработке запроса: {e}'
    finally:
       conn.close()

# Обработчик запросов на получение списка поставщиков:
@app.route('/provider', methods=['GET', 'POST'])
def get_provider():
    # Подключение к PostgreSQL
    conn = psycopg2.connect(connection_string)
    try:
        if request.method == 'GET':
            providers = read_provider_db(conn)
            return render_template('provider.html', providers=providers)
        else:
            if request.content_type == 'application/json':
             new_provider = request.json
            elif request.form:
             new_provider = request.form

            name = new_provider.get('name')
            phone = new_provider.get('phone')
            email = new_provider.get('email')
            address = new_provider.get('address')

            # Добавление нового товара в базу данных
            provider = add_provider_to_db(conn, name, phone, email, address)
            return redirect('/provider')
    except Exception as e:
        traceback.print_exc()
        return f'Ошибка при получении поставщиков из базы данных: {e}'
    finally:
       conn.close()

# Обработчик запросов на добавление поставщика:
@app.route('/new_provider', methods=['POST', 'GET'])
def add_provider():
    # Получаем токен из заголовка HTTP-запроса
    token = request.cookies.get('token')
    print('token ', token)

    # Подключение к PostgreSQL
    conn = psycopg2.connect(connection_string)
    try:
        if token:
            # Проверяем валидность токена и получаем данные пользователя
            user_data = extract_user_data_from_token(token)
            if user_data:
                # Проверяем, что пользователь имеет роль "manager"
                if user_data['role'] == 'manager':
                    if request.method == 'POST':
                      response = make_response(render_template('provider.html'), 200)
                      return response
                    else:
                        return render_template('new_provider.html')
                else:
                    print("У вас нет доступа к этой странице")
                    response = make_response(render_template('login.html'), 403)
                    return response
            else:
                print("Неверный токен")
                response = make_response(render_template('login.html'), 403)
                return response
        else:
            print("Требуется авторизация")
            response = make_response(render_template('login.html'), 401)
            return response
    except Exception as e:
        traceback.print_exc()
        return f'Ошибка при обработке запроса: {e}'
    finally:
        conn.close()

# Обработчик входа в систему существующего пользователя:
@app.route('/login', methods=['POST', 'GET'])
def login():

    # Подключение к PostgreSQL
    conn = psycopg2.connect(connection_string)
    try:
        if request.method == 'POST':
            if request.content_type == 'application/json':
                user_data = request.json
            elif request.form:
                user_data = request.form
            login = user_data.get('login')
            password = user_data.get('password')
            role = user_data.get('role')

            # Поиск пользователя в базе данных
            user = find_user_in_db(conn, login, password, role)
            if user is not None:
                # Генерация JWT-токена
              if role =='manager':
                token = generate_jwt_token(user, role, exp_time)
                sysuser=user['id']
                print('token', token)
                # Запись JWT-токена в таблицу jwts
                add_JWT_to_db(conn, datetime.utcnow(), token, sysuser)
                # Добавление токена в заголовок запроса
                response = make_response(render_template('index.html'), 200)
                response.set_cookie('token', token)
                return response
              else:
               return render_template('index.html')
            else:
                response = make_response(render_template('registration.html'), 401)
                return response
 #               return redirect(url_for('registration'))
  #              return redirect('/registration')
        else:
           response = make_response(render_template('login.html'), 200)
           return response
           #return redirect('/login')
    except Exception as e:
        traceback.print_exc()
        if request.method == 'POST':
            return jsonify({"error": f'Ошибка аутентификации пользователя в базу данных: {e}'})
        else:
            return jsonify({"error": f'Сервис аутентификации недоступен, попробуйте позже: {e}'})
    finally:
      conn.close()

# Обработчик регистрации нового пользователя:
@app.route('/registration', methods=['POST', 'GET'])
def registration():

    # Подключение к PostgreSQL
    conn = psycopg2.connect(connection_string)
    try:
        if request.method == 'POST':
            if request.content_type == 'application/json':
                user_data = request.json
            elif request.form:
                user_data = request.form
            login = user_data.get('login')
            password = user_data.get('password')
            role = user_data.get('role')

            # Регистрация пользователя в базе данных
            added_user = add_user_to_db(conn, user_data)
            response = make_response(render_template('login.html'), 201)
            return response
#            return redirect('/login')
        else:
            response = make_response(render_template('registration.html'), 200)
            return response
            #return redirect('/registration')
    except Exception as e:
        traceback.print_exc()
        if request.method == 'POST':
            return jsonify({"error": f'Ошибка при добавлении пользователя в базу данных: {e}'})
        else:
            return jsonify({"error": f'Сервис регистрации недоступен, попробуйте позже: {e}'})
    finally:
      conn.close()

# Обработчик изменения поставщика по id:
@app.route('/provider/<int:id>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def provider_ops(id):
    # Получаем токен из заголовка HTTP-запроса
    token = request.cookies.get('token')
    print('token ', token)

    # Подключение к PostgreSQL
    conn = psycopg2.connect(connection_string)
    try:
      if token:
        # Проверяем валидность токена и получаем данные пользователя
        user_data = extract_user_data_from_token(token)
        if user_data:
          # Проверяем, что пользователь имеет роль "manager"
          if user_data['role'] == 'manager':
            provider = get_provider_from_db(conn, id)
            if not provider:
              return f'Поставщик с ID {id} не найден'
            else:
              if request.method == 'PUT':
                if request.content_type == 'application/json':
                    updated_provider = request.json
                elif request.form:
                    updated_provider = request.form
                else:
                    updated_provider = request.args

                if  updated_provider.get('name'):
                  name = updated_provider.get('name')
                else:
                  name = provider['name']
                if  updated_provider.get('phone'):
                  phone = updated_provider.get('phone')
                else:
                  phone = provider['phone']
                if  updated_provider.get('email'):
                  email = updated_provider.get('email')
                else:
                  email = provider['email']
                if  updated_provider.get('address'):
                  address = updated_provider.get('address')
                else:
                  address = provider['address']

                #Обновление данных поставщика в базе данных
                if update_provider_in_db(conn, id, name, phone, email, address):
                  return ('', 200)
              elif request.method == 'DELETE':
                try:
                    delete_provider_from_db(conn, id)
                    response = make_response(render_template('provider.html'), 204)
                    return response
                except Exception as e:
                    traceback.print_exc()
                    return f'Ошибка при удалении поставщика: {e}'
                finally:
                   conn.close()
              else:
                provider = get_provider_from_db(conn, id)
                if provider:
                    return render_template('update_provider.html', provider=provider)
                else:
                    return f'Поставщик с ID {id} не найден'
          else:
            print("У вас нет доступа к этой странице")
            response = make_response(render_template('login.html'), 403)
            return response
        else:
          print("Неверный токен")
          response = make_response(render_template('login.html'), 403)
          return response
      else:
        print("Требуется авторизация")
        response = make_response(render_template('login.html'), 401)
        return response
    except Exception as e:
        traceback.print_exc()
        return f'Ошибка при изменении поставщика: {e}'
    finally:
        conn.close()

# Обработчик изменения товара по id:
@app.route('/product/<int:id>', methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
def product_ops(id):
    # Получаем токен из заголовка HTTP-запроса
    token = request.cookies.get('token')
    print('token ', token)

    # Подключение к PostgreSQL
    conn = psycopg2.connect(connection_string)
    try:
      if token:
        # Проверяем валидность токена и получаем данные пользователя
        user_data = extract_user_data_from_token(token)
        if user_data:
          # Проверяем, что пользователь имеет роль "manager"
          if user_data['role'] == 'manager':
            product = get_product_from_db(conn, id)
            if not product:
              return f'Товар с ID {id} не найден'
            else:
              if request.method == 'PUT' or request.method == 'PATCH':
                if request.content_type == 'application/json':
                    updated_product = request.json
                elif request.form:
                    updated_product = request.form
                else:
                    updated_product = request.args

                if  updated_product.get('name'):
                  name = updated_product.get('name')
                else:
                  name = product['name']
                if  updated_product.get('provider'):
                  provider = updated_product.get('provider')
                else:
                  provider = product['provider']
                if  updated_product.get('price'):
                  price = updated_product.get('price')
                else:
                  price = product['price']
                if  updated_product.get('quantity'):
                  quantity = updated_product.get('quantity')
                else:
                  quantity = product['quantity']

                #Обновление данных поставщика в базе данных
                update_product_in_db(conn, id, name, provider, price, quantity)
                return ('', 200)
              elif request.method == 'DELETE':
                try:
                    delete_product_from_db(conn, id)
                    response = make_response(render_template('product.html'), 204)
                    return response
                except Exception as e:
                    traceback.print_exc()
                    return f'Ошибка при удалении товара: {e}'
                finally:
                    conn.close()
              else:
                product = get_product_from_db(conn, id)
                if product:
                    return render_template('update_product.html', product=product)
                else:
                    return f'Товар с ID {id} не найден'
          else:
            print("У вас нет доступа к этой странице")
            response = make_response(render_template('login.html'), 403)
            return response
        else:
          print("Неверный токен")
          response = make_response(render_template('login.html'), 401)
          return response
      else:
        print("Требуется авторизация")
        response = make_response(render_template('login.html'), 401)
        return response
    except Exception as e:
        traceback.print_exc()
        return f'Ошибка при изменении поставщика: {e}'
    finally:
        conn.close()

# Обработчик запросов на получение списка заказов:
@app.route('/order', methods=['GET'])
def get_order():

    # Получаем токен из заголовка HTTP-запроса
    token = request.cookies.get('token')
    print('token ', token)

    # Подключение к PostgreSQL
    conn = psycopg2.connect(connection_string)
    try:
        if token:
            # Проверяем валидность токена и получаем данные пользователя
            user_data = extract_user_data_from_token(token)
            if user_data:
                # Проверяем, что пользователь имеет роль "manager"
                if user_data['role'] == 'manager':
                    orders = read_order_db(conn)
                    return render_template('order.html', orders=orders)
                else:
                    print("Вы не менеджер, просмотр запрещен")
                    response = make_response(render_template('login.html'), 401)
                    return response
            else:
                print("Надо войти как менеджер")
                response = make_response(render_template('login.html'), 403)
                return response
        else:
          print("Надо войти в систему")
          response = make_response(render_template('login.html'), 403)
          return response
    except Exception as e:
        traceback.print_exc()
        return f'Ошибка при получении заказов из базы данных: {e}'
    finally:
        conn.close()

# Обработчик изменения заказа по id:
@app.route('/order/<int:id>', methods=['GET', 'POST', 'PUT'])
def order_ops(id):
    # Получаем токен из заголовка HTTP-запроса
    token = request.cookies.get('token')
    print('token ', token)

    # Подключение к PostgreSQL
    conn = psycopg2.connect(connection_string)
    try:
      if token:
        # Проверяем валидность токена и получаем данные пользователя
        user_data = extract_user_data_from_token(token)
        if user_data:
          # Проверяем, что пользователь имеет роль "manager"
          if user_data['role'] == 'manager':
            order = get_order_from_db(conn, id)
            print(order)
            if not order:
              return f'Заказ с ID {id} не найден'
            else:
              if request.method == 'PUT':
                if request.content_type == 'application/json':
                    updated_order_state = request.json
                elif request.form:
                    updated_order_state = request.form
                else:
                    updated_order_state = request.args

                if  updated_order_state.get('state'):
                  state = updated_order_state.get('state')
                else:
                  state = order['state']

                #Обновление данных поставщика в базе данных
                if update_order_in_db(conn, id, state):
                  return ('', 200)
              else:
                order = get_order_from_db(conn, id)
                if order:
                    return render_template('update_order.html', order=order)
                else:
                    return f'Заказ с ID {id} не найден'
          else:
            print("У вас нет доступа к этой странице")
            response = make_response(render_template('login.html'), 403)
            return response
        else:
          print("Неверный токен")
          response = make_response(render_template('login.html'), 403)
          return response
      else:
        print("Требуется авторизация")
        response = make_response(render_template('login.html'), 401)
        return response
    except Exception as e:
        traceback.print_exc()
        return f'Ошибка при изменении поставщика: {e}'
    finally:
       conn.close()

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

Чтобы не исчерпать лимит тунелей, последней ячейкой в Colabe закроем открытые тунель, убив ngrok-процессы. Эту ячейку следует запускать только после остановки предыдущей, с кодом серверного Flask-приложения:

#############################новая ячейка в Google Colab###########################################################
# Kill tunnel
!pkill -f ngrok

После запуска серверного Flask-приложения и манипуляции с ним в GUI через элементы HTML-страниц в области вывода Colab можно посмотреть HTTP-запросы и отладочную информацию.

Запуск Flask-приложения в Google Colab
Запуск Flask-приложения в Google Colab

Благодаря тунелированию с ngrok можно отправлять клиентские запросы к этому серверному приложению, получив ссылку внешнего URL-адреса. В данном случае это http://71a5-34-141-243-159.ngrok-free.app, однако, этот адрес меняется после каждого запуска. При открытии этого адреса в браузере отобразится приветственная HTML-страница с навигацией по сайту.

пример Flask-приложения REST API Google Colab
Главная страница сайта интернет-магазина

Полный набор артефактов этого веб-приложения доступен здесь, в моем Github-репозитории.

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

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

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

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

 

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

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