Python flask фреймворк - правильная структура приложения

Многие фреймворки диктуют свою структуру приложения при разработке. С одной стороны это хорошо: в больших проектах человеку знакомому с фреймворком будет легче и проще вникнуть в проект. С другой стороны - это накладывает определенные ограничения, и иногда вынуждает использовать методы и паттерны без которых вполне можно было обойтись. Во всех таких фреймворках есть определенные инструменты позволяющие разделять приложение на модули. Во flask так же есть подобный инструмент, и называются он blueprints.

Вопреки частому заблуждению, flask прекрасно подходит для больших проектов. В нескольких таких крупных проектах мне даже довелось учавствовать. Поэтому, если вас убеждают, что flask никто не использует не верьте им. Ещё как используют. Flask прекрасный, небольшой фреймворк подходящий для очень широкого круга задач.

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

Для тех, кто не знаком с Flask - это небольшой фреймворк написанный на языке python c весьма большим сообществом, и множеством модулей на все случаи жизни. В отличии от, скажем, Django, Flask не навязывает определенное решение той или иной задачи. Вместо этого, он предлагает использовать различные сторонние или собственные решения по вашему усмотрению. Я ни в коем случае не хочу сказать, что Django плохой фреймворк. Просто в некоторых случаях Django вынуждает писать много лишнего кода попутно залезая все глубже и глубже во внутренности самого фреймворка. Так что flask - это лишь ещё один инструмент, подходящий под определенные задачи.

Disclaimer: в этой статье нет ничего принципиально нового и неизвестного. Все это общеизвестные практики, только собранные в одном месте. Эта статья ориентирована в основном на тех, кто не знаком с flask и новичков.

Introduction

Чаще всего я (да и большинство остальных разработчиков) использую flask совместно со следующими python-библиотеками:

  • Sqlalchemy (используя расширение Flask-Sqlalchemy) - для работы с базой данных;
  • Wtforms - (используя расширение flask-wtf) для обработки html-форм;
  • psycopg2 - прослойка для Sqlalchemy. Необходима для работы postgresql;
  • flask-script - для удобного запуска приложения и различных скриптов;

Конечная структура будет такой:

testproject
├── app
│   ├── __init__.py
│   ├── database.py
│   ├── firstmodule
│   │   ├── controllers.py
│   │   ├── forms.py
│   │   ├── models.py
│   ├── static
│   │   ├── css
│   │   ├── img
│   │   └── js
│   └── templates
│       ├── base.html
│       ├── index.html
│       └── _list.html
│       ├── firstmodule
│       │   ├── create.html
│       │   ├── delete.html
│       │   ├── list.html
│       │   ├── update.html
│       │   └── view.html
├── config.py
├── env
├── manage.py
└── requipments
    ├── base.txt
    ├── development.txt
    └── production.txt

Перед началом, убедимся что все нужные пакеты у нас в системе установлены. Нам нужен собственно сам python и pip:

sudo apt-get install python3-pip python3-dev

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

Основной "скелет" приложения

Сначала создадим наше виртуальное окружение. Создадим директорию, где будет располагаться наш проект. Здесь, и дальше во всей статье я буду предполагать что это директория testproject:

mkdir testproject
cd testproject

Создадим виртуальное окружение c помощью virtualenv:

virtualenv -p python3 env

Создадим директорию для кода нашего будущего проекта:

mkdir app

И заодно директорию для шаблонов, и статических файлов:

# Директория для шаблонов
mkdir app/templates
# Директория для статических файлов (стили, javascript, и т.д.)
mkdir -p app/static/{css,js,img}

Активируем виртуальное окружение:

source env/bin/activate

Важно: здесь и далее считаем, что все команды выполняются внутри виртуального окружения!

Установим нужные нам пакеты с помощью pip:

pip install flask flask-sqlalchemy flask-wtf flask-script psycopg2

Основной "скелет" приложения готов.

Git

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

В корневой директории нашего проекта (напомню, это testproject) создадим файл .gitignore.

touch .gitignore

Он необходим для того, чтобы лишние файлы, такие как кэши не попали в git репозиторий. Добавим в него следующее:

.idea/
.idea/*

lib/
lib/*

*.py[cod]
*.pyc

bin/
bin/*

share/
share/*

local/
local/*

include/
include/*

env/

Инициализируем репозиторий:

git init

И сделаем первый коммит:

git add -A
git commit -m 'Initial commit'

Теперь необходимо указать url git репозитория. Для этого его необходимо сначала создать. Сейчас очень много различных компаний предлагают бесплатный хостинг git репозиториев. Из них можно выделить всем известные github и bitbucket. Лично я для своих проектов предпочитаю последний, т.к. он позволяет создавать скрытые репозитории бесплатно. После создания репозитория на любом из этих сервисов, будет показана подсказка, в которой будет описано как добавить url репозитория в существующий проект. Сейчас я на этом подробно останавливаться не буду.

Зависимости

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

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

  • development - зависимости, необходимые для разработки;
  • production - зависимости, необходимые только в production окружении;

При желании вы легко сможете добавить сколько угодно необходимых. Сами же файлы с версиями будем хранить в отдельной директории - requipments. Создадим её:

mkdir requipments

Теперь создадим базовый файл, в котором будут содержаться зависимости общие для всех окружений:

pip freeze > requipments/base.txt

Этой командой мы добавим в файл base.txt список всех установленных в данный момент библиотек с их зависимостями. В данный момент мы уже установили в наше виртуальное окружение собственно сам flask, Flask-Sqlalchemy, flask-wtf и psycopg2. Содержимое файла будет таким:

click==6.6
Flask==0.11
Flask-SQLAlchemy==2.1
Flask-Script==2.0.5
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
psycopg2==2.6.1
SQLAlchemy==1.0.13
Werkzeug==0.11.10
WTForms==2.1

Для того, чтобы переиспользовать содержимое base.txt в файлах других окружений, в начало каждого из них необходимо добавить:

-r base.txt

А затем остальные зависимости построчно.

Например, во время разработки в development окружении, возможно вы захотите использовать flask-debugtoolbar. В этом случае файл requipments/development.txt может быть таким:

-r base.txt
flask-debugtoolbar

В production окружении будет хорошей идеей использовать gunicorn для запуска самого приложения, и содержимое файла requipments/production.txt может быть таким:

-r base.txt
gunicorn

Так же не лишнем будет указать версии используемых библиотек аналогично тому, как они указаны в файле requipments/base.txt.

Файл настроек

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

Настройки будем хранить в файле config.py. В нем аналогично тому, как мы описывали зависимости, опишем базовую конфигурацию для всех окружений и переопределим некоторые настройки под определенные окружения.

import os

class Config(object):
    # Определяет, включен ли режим отладки
    # В случае если включен, flask будет показывать
    # подробную отладочную информацию. Если выключен -
    # - 500 ошибку без какой либо дополнительной информации.
    DEBUG = False
    # Включение защиты против "Cross-site Request Forgery (CSRF)"
    CSRF_ENABLED = True
    # Случайный ключ, которые будет исползоваться для подписи
    # данных, например cookies.
    SECRET_KEY = 'YOUR_RANDOM_SECRET_KEY'
    # URI используемая для подключения к базе данных
    SQLALCHEMY_DATABASE_URI = os.environ['DATABASE_URL']
    SQLALCHEMY_TRACK_MODIFICATIONS = False


class ProductionConfig(Config):
    DEBUG = False

class DevelopmentConfig(Config):
    DEVELOPMENT = True
    DEBUG = True

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

Файл запуска приложения

Для запуска приложения мы будем использовать удобное расширение - flask-scripts. Весь необходимый код поместим в файл manage.py в корень проекта. Название не случайно. Кроме того, что оно "говорящее" ещё и довольно привычное многим python-разработчикам.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env python
import os
from flask_script import Manager

from app import create_app

app = create_app()
app.config.from_object(os.environ['APP_SETTINGS'])
manager = Manager(app)

if __name__ == '__main__':
    manager.run()

Само flask приложение мы будем создавать используя "фабрику" - функцию, создающую экземпляр приложения и возвращающую его. Её мы опишем дальше в этой статье.

База данных

Как выше уже говорилось, для работы с базой данных используем sqlalchemy через дополнение Flask-sqlalchemy. Это, пожалуй, самая часто используемая связка. Переменную, в которой будет хранится сессия для работы с базой данных мы будем хранить в отдельном файле - app/database.py. В этом файле почти всегда будет несколько строк, но помещать эти строки в тот же файл, где мы будем инициализировать flask приложение не рекомендуется. Мы же не хотим получать странные сообщения об ошибках во время запуска потому, что случайно сделали круговой импорт модулей (circular import)? Инициализацию же расширения flask-sqlaclhemy мы будем проводить там же, где и инициализацию flask приложения.

Содержимое файла app/database.py:

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

Инициализация приложения

Как я уже выше писал, инициализация приложения будет происходить в функции-фабрике. Под инициализацией подразумевается создание экземпляра flask приложения со всеми необходимыми настройками. Использовать фабрику строго обязательно. Без этого, в какой-то момент "по запарке" можем выстрелить себе в ногу все теми же "круговыми импортами". Да и тесты иначе не получится нормально писать, а без тестов вообще нельзя. Совсем. Никогда. :)

Наша фабрика будет находится в файле app/__init__.py. Вот его содержимое:

import os
from flask import Flask
from .database import db

def create_app():
    app = Flask(__name__)
    app.config.from_object(os.environ['APP_SETTINGS'])

    db.init_app(app)
    with app.test_request_context():
        db.create_all()

    import app.firstmodule.controllers as firstmodule

    app.register_blueprint(firstmodule.module)

    return app

Обратите внимание, что инициализация расширения flask-sqlaclhemy происходит внутри тела функции, а не просто в файле. Тоже самое касается и импортов blueprint модулей.

Модули (blueprints)

Теперь займемся непосредственно модулями нашего приложения.

Создадим директорию для шаблонов модуля:

mkdir -p app/templates/firstmodule

И директорию для самого модуля:

mkdir -p app/firstmodule

Да, шаблоны используемые модулем лучше хранить в отдельной под-директории для лучшей его (модуля) переносимости.

Вот так выглядит структура модуля:

╰─➤  tree app/firstmodule/
.
├── controllers.py
├── forms.py
├── models.py
  • controllers.py - предназначен для контроллеров;
  • models.py - предназначен для sqlalchemy моделей;
  • forms.py - предназначен для форм;

Теперь более подробно примерное содержимое каждого из файлов.

app/firstmodule/models.py:

from sqlalchemy import event

from app.database import db

class Entity(db.Model):
    __tablename__ = 'entity'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(1000), nullable=False, unique=True)
    slug = db.Column(db.String(1000))
    content = db.Column(db.String(5000))

    comments = db.relationship('Comment', backref='entity')

    def __str__(self):
        return self.name


@event.listens_for(Entity, 'after_delete')
def event_after_delete(mapper, connection, target):
    # Здесь будет очень важная бизнес логика
    # Или нет. На самом деле, старайтесь использовать сигналы только
    # тогда, когда других, более правильных вариантов не осталось.
    pass

app/firstmodule/controllers.py:

# ВНИМАНИЕ: код для примера! Не нужно его бездумно копировать!
from flask import (
    Blueprint,
    render_template,
    request,
    flash,
    abort,
    redirect,
    url_for,
    current_app,
)
from sqlalchemy.exc import SQLAlchemyError

from .models import Entity, db
from .forms import EntityCreateForm
from app.comment.models import Comment

module = Blueprint('entity', __name__, url_prefix ='/entity')


def log_error(*args, **kwargs):
    current_app.logger.error(*args, **kwargs)


@module.route('/', methods=['GET'])
def index():
    entities = None
    try:
        entities = Entity.query.join(Comment).order_by(Entity.id).all()
        db.session.commit()
    except SQLAlchemyError as e:
        log_error('Error while querying database', exc_info=e)
        flash('Во время запроса произошла непредвиденная ошибка.', 'danger')
        abort(500)
    return render_template('entity/index.html', object_list=entities)


@module.route('/<int:id>/view/', methods=['GET'])
def view(id):
    entity = None
    try:
        entity = Entity.query.outerjoin(Comment).first_or_404(id)
        db.session.commit()
        if entity is None:
            flash('Нет entity с таким идентификатором', 'danger')
            abort(404)
    except SQLAlchemyError as e:
        log_error('Error while querying database', exc_info=e)
        flash('Во время запроса произошла непредвиденная ошибка', 'danger')
        abort(500)
    return render_template('entity/view.html', object=image)


@module.route('/create/', methods=['GET', 'POST'])
def create():
    form = EntityCreateForm(request.form)
    try:
        if request.method == 'POST' and form.validate():
            entity = Entity(**form.data)
            db_session.add(entity)
            db_session.flush()
            id = entity.id
            db_session.commit()
            flash('Запись была успешно добавлена!', 'success')
            return redirect(url_for('entity.view', id=id))
    except SQLAlchemyError as e:
        log_error('There was error while querying database', exc_info=e)
        db.session.rollback()
        flash('Произошла непредвиденная ошибка во время запроса к базе данных', 'danger')
    return render_template('entity/create.html', form=form)


# Наивное удаление. Чаще всего, будет сложная логика с правильной обработкой
# зависимых объектов.
@module.route('/<int:id>/remove/', methods=['GET', 'POST'])
def remove(id):
  entity = None
  try:
      entity = Entity.query.get(id)
      if entity is None:
          flash('Нет записи с таким идентификатором', 'danger')
  except SQLAlchemyError as e:
      log_error('Error while querying database', exc_info=e)
      flash('Произошла непредвиденная ошибка во время запроса к базе данных', 'danger')
  finally:
      db_session.commit()
      flash('Запись была успешна удалена!', 'success')
  return redirect(url_for('entity.index'))

app/firstmodule/forms.py:

from flask.ext.wtf import Form
from wtforms import (
    StringField,
    TextAreaField,
)
from wtforms.validators import DataRequired

class EntityCreateForm(Form):
    name = StringField(
        'Название',
        [
            DataRequired(message="Поле обязательно для заполнения")
        ],
        description="Название"
    )
    content = TextAreaField(
        'Содержимое',
        [],
        description="Содержимое записи",
    )

Шаблоны

Как я уже выше писал, шаблоны приложения будут находится в app/templates/. В этой директории надо расположить базовый шаблон, шаблоны страниц с ошибками и какие либо служебные части. Шаблоны отдельных модулей надо помещать в директорию с именем соответствующим имени модуля.

Вот пример:

.
├── 403.html
├── 404.html
├── 500.html
├── base.html
├── entity
│   ├── create.html
│   ├── delete.html
│   ├── list.html
│   ├── update.html
│   └── view.html
├── form-errors.html
├── form-macros.html
├── index.html
├── messages.html
├── pagination.html

Примеры самих шаблонов я приводить не буду, они как правило слишком объёмные.

Определяем переменные окружения

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

В терминале пишем:

export APP_SETTINGS="config.DevelopmentConfig"
export DATABASE_URL='postgresql://USERNAME:PASSWORD@localhost/DBNAME'

Это хорошая практика не хранить нигде логин/пароль в файлах конфигурации. В случае взлома приложения, взломщику будет намного тяжелее получить пароль базы данных. На production сервере в переменной окружения APP_SETTINGS конечно же будет необходимо указать config.ProductionConfig.

Запуск приложения

В примерах кода выше было очень много допущений, поэтому если вы просто копировали их, то ничего не заработает. :) Сделано это специально, т.к. в статье я рассказывал не о том как писать приложения на flask, а как правильно организовывать их структуру. Полностью рабочий пример можете найти в репозитории на github.

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

# Делаем файл manage.py исполняемым
chmod +x manage.py
╰─➤  ./manage.py runserver
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger pin code: 147-804-358

Главное не забудьте про переменные окружения, иначе ничего тем более не заработает. :)

Полезные ссылки:


Понравилась статья? Поделись с друзьями!




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