BYOB с Flask, Python и MongoEngine

Привет всем,

Спасибо, что присоединились к этой статье. Сегодня мы рассмотрим один из самых популярных языков программирования Python и минималистичный, но очень мощный веб-фреймворк Flask. Мы также будем использовать базу данных документов NoSql под названием MongoDB и интегрировать ее с ODM (Object Document Mapper) под названием MongoEngine.

Предварительные условия

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

Visual Studio Code
Git Bash
Postman
Pip
Если вам случайно не хватает одного из этих инструментов, обязательно скачайте и установите его, прежде чем продолжить.

Пролог

В своей повседневной работе я работаю в основном с Node.Js в среде микросервисов, но иногда у нас есть задачи или целые эпопеи, которые управляются данными, и нам нужно обрабатывать массивные вычисления на данных, с которыми мы работаем, node.js не является подходящим инструментом для этого из-за своей однопоточной неблокирующей природы io, поэтому нам нужно выбрать правильный инструмент для работы. В микросервисной среде у вас может быть много различных сервисов, которые выполняют одну задачу, и они могут быть реализованы с помощью языка программирования или фреймворка вне вашего основного технологического стека. В данном случае на помощь приходит Python & Flask. В этой статье мы рассмотрим, как можно быстро получить работающий api.

Настройка

Мы откроем visual studio code и настроим наше рабочее пространство. Мы будем использовать инструмент под названием Pipenv — это менеджер пакетов, очень похожий на NPM в экосистеме Node.Js, и это новый способ делать вещи в экосистеме python. Мы начнем с создания каталога нашего проекта api.

mkdir api
cd api
Вход в полноэкранный режим Выход из полноэкранного режима

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

echo PIPENV_VENV_IN_PROJECT=1 >> .env
Войти в полноэкранный режим Выйти из полноэкранного режима

Этот шаг также может быть выполнен только один раз глобально и зависит от операционной системы, которая у вас установлена. Вы можете посмотреть в документации, где обновить эту переменную окружения глобально.

Наш следующий шаг — это то, с чего все начинается. Мы создадим наше инкапсулированное окружение с помощью pipenv и внутри установим все зависимости нашего проекта.

pip3 install pipenv
Вход в полноэкранный режим Выход из полноэкранного режима

Мы используем pip версии 3 для глобальной установки этого пакета, чтобы он был доступен для других проектов python, и это единственный случай, когда нам понадобится использование pip.

Теперь давайте активируем инкапсулированную среду и начнем установку пакетов.

pipenv shell
Вход в полноэкранный режим Выйти из полноэкранного режима

Эта команда фактически создает и активирует наше виртуальное окружение, что хорошо известно и используется в экосистеме python. Причина, по которой нам нужно, чтобы наш файл .env присутствовал заранее, заключается в том, что по умолчанию pipenv создает папку .venv в другом месте файловой системы, в зависимости от того, используете ли вы Windows, Linux или Mac. Таким образом, мы убеждаемся, что все, что связано с проектом, присутствует в рабочем пространстве проекта.

Вы можете видеть, что выполнив эту команду, мы получили виртуальную среду, а также создали файл Pipfile, который содержит все настройки для проекта в плане зависимостей, скриптов зависимостей dev и версий, подобно package.json в Node.js.

Теперь мы установим все зависимости сразу, чтобы не пришлось постоянно переключаться между контекстами. Вы можете просто скопировать и вставить это в терминал, если не хотите набирать все это.

pipenv install flask flask-restful flask-marshmallow marshmallow mongoengine flask-mongoengine python-dateutil flask-cors 
Вход в полноэкранный режим Выход из полноэкранного режима

После завершения установки вы увидите, что был создан новый файл Pipfile.lock, в котором описаны все зависимости и подзависимости, которые мы установили, а также указаны точные версии каждой зависимости.

Теперь мы создаем наш входной файл app.py внутри папки src, чтобы все было понятно. Затем мы изменим каталог в этой папке, и это будет наш корень проекта. Все дополнительные папки и файлы, которые мы будем создавать, будут потомками src.

mkdir src
cd src
touch app.py
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы создадим 2 папки, которые будут представлять нашу бизнес-логику для проекта, и со временем, когда этот проект будет масштабироваться, вы сможете следовать той же схеме.

В нашем примере речь пойдет о пост-модели crud api. Модель будет построена с вложенной схемой, просто небольшое дополнение от меня, чтобы увидеть, как мы работаем с вложенными документами в MongoDB.

# database related tree

mkdir database controllers
mkdir database/models

touch database/db.py
touch database/models/{metadata,post}.py
Вход в полноэкранный режим Выход из полноэкранного режима
# controller related tree

mkdir controllers
mkdir controllers/posts
mkdir controllers/posts/dto

touch controllers/posts/dto/post.dto.py
touch controllers/routes.py
touch controllers/posts/{posts,post}Api.py
Войти в полноэкранный режим Выход из полноэкранного режима

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

- api #(project folder)
| - src
|    | - app.py
|    | - controllers
|         | - dto
|              | - post.dto.py
|         | - posts
|              | - postApi.py
|              | - postsApi.py
|         | - routes.py
|    | - database
|         | - db.py
|         | - models
|              | - post.py
|              | - metadata.py
Вход в полноэкранный режим Выход из полноэкранного режима

Настройка базы данных

В этом шаге мы настроим нашу базу данных в проекте. Мы будем использовать MongoDB. Существуют различные способы использования этого сервиса. Самый популярный — через облачное предложение под названием MongoDB Atlas, оно бесплатно, и все, что вам нужно сделать, — это зарегистрироваться на сервисе. Я, например, не хочу добавлять уровень задержки при локальной разработке, поэтому вместо этого я выберу Docker. Этот шаг необязателен, и вы не обязаны делать его так же, как я, но я подробно расскажу, как запустить локальный контейнер Docker для MongoDB в целях разработки. Это не учебник по Docker, и я не хочу менять фокус, поэтому если вы не знакомы с ним, используйте Atlas.


# docker-compose.yml file

version: "3"

services:
  db:
    restart: always
    container_name: mongodb
    image: mongo
    ports:
      - "27017:27017"
      - "27018:27018"
      - "27019:27019"
    volumes:
      - "./mongo_vol:/data/db"
Вход в полноэкранный режим Выход из полноэкранного режима
# spin up a mongodb container by running this command in your terminal

docker-compose up -d
Войти в полноэкранный режим Выход из полноэкранного режима

После того как наша база данных запущена и работает, локально или в облаке, мы можем перейти к ее настройке в нашем проекте. Давайте откроем файл db.py в папке с базой данных и напишем немного кода.

Нам нужно импортировать в этот файл утилиту для работы с MongoDB, и мы установили пакет под названием flask-mongoengine, который будет обрабатывать подключение к базе данных и операции с ней.

from flask_mongoengine import MongoEngine

db = MongoEngine()

def init_db(app):

    # replace the host URI with your atlas URI if you are not working locally
    # this settings must be as close as posiible to the init app call!
    app.config["MONGODB_SETTINGS"] = { "host" : "mongodb://localhost:27017/products" }

    db.init_app(app)
    pass
Вход в полноэкранный режим Выход из полноэкранного режима

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

Настройка модели базы данных — пост

Наша модель для примера — это пост. Мы хотим описать, как выглядит пост с точки зрения полей свойств и разделить некоторые из них на вложенную схему. Наш пост будет состоять из следующих свойств: заголовок, текст, дата создания, email автора, url. Мы решим, что родительская схема будет содержать поля title и text, а остальные будут вложены в дочернюю схему под названием metadata, как показано ниже:

    # Metadata schema

    import datetime
    from ..db import db

    class Metadata(db.EmbeddedDocument):
        url = db.URLField()
        email = EmailField()
        date = db.DateTimeField(default=datime.datime.utcnow)
        pass
Войти в полноэкранный режим Выход из полноэкранного режима

Обратите внимание на поле даты, где мы инициализируем его с датой по умолчанию & время только при создании. Мы не будем предоставлять это значение через api. Модель сама создаст и установит его.

    # Post schema

    from ..db import db
    from .metadata import Metadata

     class Post(db.Document):
         title = db.StringField()
         text = db.StringField()
         metadata = db.EmbeddedDocumentField(Metadata, default=Metadata)

         meta = {
             "collection" : "passages",
             "auto_create_index": True,
             "index_background": True,
             "inheritance": True
         }
         pass
Вход в полноэкранный режим Выход из полноэкранного режима

Также обратите внимание, что все поля являются необязательными, кроме поля метаданных. Модель Post ожидает получить объект метаданных с его полями, поскольку мы установили флаг по умолчанию. Ваша задача — изменить их на обязательные поля с помощью выражения required=True внутри круглых скобок.

Как видите, у нас есть две модели, каждая из которых содержит частичные данные о нашем посте, а вместе они представляют собой единое целое. Теперь, используя класс Post, мы обратимся к базе данных через наш api. Нам не нужно ничего делать с классом Metadata. Он существует для удобства.

Настройка API

Flask поддерживает различные способы создания веб-приложений и API, начиная с шаблонов для статических страниц, через чертежи и представления и заканчивая rest. Мы выберем restful подход, для чего уже установили соответствующий пакет flask-restful.

Способ построения rest api с помощью пакета restful заключается в использовании классов, которые наследуются от базового класса Resource. Мы создадим 2 класса для нашего api. Первый класс будет обрабатывать все общие запросы, а второй класс будет обрабатывать все специфические запросы, которые мы хотим отфильтровать, например, по id поста. Очевидно, что вы можете выбрать лучший подход для ваших нужд.


# PostsApi.py file

from database.models.post import Post
from flask import request, Response
from flask_restful import Resource
import json

class PostsApi(Resource):

    # get all post from database
    def get(self):
        posts = Post.objects().to_json()
        return Response(
            posts,
            mimetype="application/json",
            status=200
        )


    # create new post
    def post(self):
        body = request.get_json()
        post = Post(**body)
        post.save()
        return Response(
            json.dumps({
                "message" : str(post.id),
                "status": "success"
            }),
            mimetype="application/json",
            status=201
        )

Вход в полноэкранный режим Выход из полноэкранного режима

Здесь видно, что класс имеет 2 метода, названные в честь HTTP-глаголов, за обработку которых они отвечают. Вызов GET HTTP к этой конечной точке /api/posts/ вернет список постов в базе данных, а вызов POST HTTP к той же конечной точке вызовет тот же класс API для создания нового документа в нашей базе данных. Обратите также внимание на то, как мы взаимодействуем с базой данных MongoDB, а именно с классом Post, который мы создали ранее. Это post api, и нам нужна сущность, связанная с ним. Теперь давайте посмотрим, как создать специфический запрос api.


# PostApi.py file

from database.models.post import Post
from flask import request, Response
from flask_restful import Resource
import json

class PostsApi(Resource):

    # get one post by id from database
    def get(self, id):
        post = Post.objects.get_or_404(id=id).to_json()
        return Response(
            post,
            mimetype="application/json",
            status=201
        )


    ### delete one passages
    def delete(self, id):
        post = Post.objects(id=id).delete()
        return Response(
            json.dumps({
                "id" : str(id),
                "success" : post
            }),
            mimetype="application/json", 
            status=200)


    # updates existing post by id
    def post(self, id):
        body = request.get_json()
        post = Post.objects(id=id).update_one(**body)
        return Response(
            json.dumps({
                "message" : post,
                "status": "success"
            }),
            mimetype="application/json",
            status=200
        )

Вход в полноэкранный режим Выход из полноэкранного режима

Как вы можете видеть, наибольшая разница заключается в аргументах, которые методы класса получают в своей сигнатуре. Параметр id является динамическим и берется из URL. Класс Flask Resource занимается его получением и предоставлением нам внутри написанной нами логики. Обратите внимание, что это не тот способ, который используется в производственной среде, мы вернемся к этому позже, когда закончим со всем, чтобы протестировать пару вещей.

Теперь нам осталось настроить маршруты для api, и мы закончили этот шаг.

    # routes.py file

    from flask_restful import Api
    from .postApi import postApi
    from .postsApi import postsApi

    def init_routes(app):
        api = Api(app)
        api.add_resource(postsApi, "/api/posts")
        api.add_resource(postApi, "/api/posts/<id>")
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что динамические параметры URL во Flask обернуты в ромбовидные скобки, в отличие от node.js, где они имеют префикс : и знак двоеточия.

Настройка приложения

Так что мы работали в обратном направлении и наконец-то оказались здесь. Место, где все складывается как по волшебству. Нам нужно взять все различные части, которые мы создали с нуля голыми руками, и заставить их соединиться. Это волшебство произойдет внутри файла приложения.

from flask import Flask
from resources.routes import init_routes
from database.db import init_db
from flask_cors import CORS


# Init flask app with mongodb using mongoengine, flask-mongoengine 
app = Flask(__name__)
init_routes(app)
init_db(app)
CORS(app)


### run app watch mode
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=True)
    pass
Вход в полноэкранный режим Выход из полноэкранного режима

Что мы делаем здесь: импортируем базу данных util function init_db, маршруты util function init_routes, утилиты из Flask, чтобы связать все и заставить дождь (🌧️ 🌧️ 🌧️ 🌧️) ❗ (😀 😀 😀 😀) заставить его работать❗❗❗ Обратите внимание, что в production вы хотите отключить флаг отладки.

Теперь мы возвращаемся в терминал и хотим, чтобы это приложение Flask было запущено. Помните, что мы установили pipenv❓ Мы хотим добавить скрипт для запуска приложения. Давайте откроем Pipfile и добавим раздел скриптов в самом низу.


[scripts]
dev = "python src/app.py runserver"

Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь в терминале, где мы инициализировали нашу виртуальную среду, выполните следующую команду


pipenv run dev

Войти в полноэкранный режим Выйти из полноэкранного режима

Поздравляем! Это ваш первый Flask Rest API, и он оказался совсем не плохим. Теперь вы можете открыть postman и провести несколько тестов API. Теперь вы знаете, что это корпоративный способ использования Flask для API, но есть несколько вещей, которые мы пропустили при написании различных классов api, это обработка ошибок и валидация ввода. В python, как и во многих других языках программирования, вы будете и должны сталкиваться с ошибками, чтобы исправить их до того, как вы отправитесь в производство. Для этого в Python используются блоки try…except, и самое интересное, что мы можем складывать несколько блоков except для обработки различных ошибок в нашем коде. Например, у вас может быть один метод, который может выбросить ошибку сервера (500 & up) или ошибку ввода пользователя (400 <= 499).

Я создам один пример, а ваше домашнее задание — добавить его во все методы класса api. Я выберу HTTP-метод DELETE и реализую общее исключение.


### delete one post
def delete(self, id):
    try:
        post = Post.objects(id=id).delete()
        return Response(
                json.dumps({
                    "id" : str(id),
                    "success" : post
                }),
                mimetype="application/json", 
                status=200)

    except Exception as err:
        return Response(
                json.dumps({
                    "message" : str(err)
                }), 
                mimetype="application/json", 
                status=500)

Вход в полноэкранный режим Выход из полноэкранного режима

Вот и все, друзья! Это была хорошая поездка сегодня. Надеюсь, вы получили удовольствие и чему-то научились. Обязательно выполните домашнее задание и, конечно же, прочитайте документацию по Python, Flask и MongoEngine.

И последнее, немного пищи для размышлений — обязательно пишите код, который проверяет, что база данных работает… независимо от того, запускаете ли вы установку mongodb, контейнер docker или Atlas.

В следующий раз, надеюсь, скоро, я хочу немного поговорить об инъекции зависимостей в Python-приложениях, сфокусировавшись на Flask.

Следите за новостями
Ставьте лайк, подписывайтесь, комментируйте и все такое…
Спасибо и до свидания

Оцените статью
devanswers.ru
Добавить комментарий