Как стать автором
Обновить

Мой путь во Flask. Часть первая

Время на прочтение10 мин
Количество просмотров9.8K

Всем доброго времени суток. В программировании два года назад я был профан, и начал с Python, потому что с этим языком была связана моя диссертация, но понял, что не тяну в изучении сам и пошел на курсы, где научили основам Python, как делать блог-соцсеть на Django, как работать с API, и немного DevOps. Но курсы закончились, и логичный вопрос, «что дальше?». По определенным причинам я не могу сменить работу, но свободного времени хватает, поэтому сначала прочитал Лутца, чтобы подтянуть «матчасть», затем «Чистый код» Роберта Мартина. Cделал парочку проектов на Django, один API, поучил JS. И снова возник вопрос, «что дальше?» Было два варианта FastAPI и Flask. Выбор пал на Flask.

Хотелось бы отметить, что данный текст представляет собой как бы бортовой журнал изучения новой библиотеки джуном, поэтому я допускаю, что местами мог написать некоторые неточности. Я полностью открыт для критики, моя цель – повысить качество письма последующих статей,  так как данная (если она конечно опубликуется :) ) будет дебютом.

Установка

С установкой все просто.

  1. Устанавливаете виртуальное окружение через venv

py –m venv venv
# или 
python –m venv venv
# или 
python3 –m venv venv
  1. Запускаете виртуальное окружение:

# Windows:
source venv/Scripts/activate
# или
.\venv\Scripts\activate
# MacOS/Ubuntu:
source venv/bin/activate
  1. Устанавливаем Flask (все нужные пакеты Flask подтянет сам):

pip install flask

Все готово! Довольно хорошо это расписано в документации .

Wake up, Neo…

Делаем самое простое приложение на свете.

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

Сначала импортируем класс Flask из модуля flask

from flask import Flask

Далее создадим экземпляр данного класса. Созданный экземпляр (app) будет нашим WSGI-приложением(подробнее в PEP 3333).

app = Flask(__name__)

В данном примере в объект передается переменная __name__, в ней хранится имя данного модуля более подробно расскажу позже, а пока как написано в документации: «__name__ — это удобное сокращение, которое подходит для большинства случаев. Это необходимо для того, чтобы Flask знал, где искать ресурсы, такие как шаблоны(templates) и статические файлы.»

Далее создадим простенькую функцию, которая будет возвращать строку:

def wake_up():
    return 'Wake up, Neo...'

Теперь наша задача состоит в том, чтобы сообщить Flask, при запросе какого URL, должна «стригериться» (выполниться) функция wake_up. Для этого у Flask есть замечательный декоратор route:

@app.route('/')
def wake_up():
    return 'Wake up, Neo...'

Таким образом, при переходе в корень вэб-приложения на экран будет выводиться строка «Wake up, Neo...».

Теперь можно тестировать приложение. Для теста необходимо запустить локальный сервер, это делается следующим образом (здесь и далее приводятся версии кода для Windows).

  1. Говорим терминалу, с каким приложением работать (в моем случае файл называется start.py, однако если файл называется app.py или wsgi.py, задавать указание на переменную окружения нет нужды и можно пропустить этот шаг, т.к. Flask проверяет эти файлы в первую очередь), путем задания переменной окружения FLASK_APP:

export FLASK_APP=start.py
  1. Включаем все фичи для разработчиков (в том числе и дебагер), путем задания переменной окружения FLASK_ENV:

export FLASK_ENV=development
  1. И наконец запускаем сервер:

flask run

После ввода последней команды Flask локальный сервер, адрес которого обычно http://127.0.0.1:5000. Переходим по этому адресу. В результате пустая вэб-страничка с надписью «Wake up, Neo…».

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

if __name__ == "__main__":
    app.run(debug=True)

Создание первой модели и связанные с этим затруднения

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

pip install -U Flask-SQLAlchemy

Для опробования функционала моделей во Flask, я обратился к документации Flask-SQLAlchemy.

В разделе Quickstart есть довольно простая модель для разбора основного функционала, давайте разберем его подробно, пример из документации выглядит следующим образом:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////database.db'
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    
    def __repr__(self):
        return '<User %r>' % self.username

Сначала импортируем необходимые переменные из библиотек flask и flask_sqlalchemy (первые две строчки), создаем приложение (строчка 4), в словарь с конфигурацией приложения добавляем путь к файлу базы данных (строчка 5), создаем объект класса SQLAlchemyиспользуется для управления интеграцией SQLAlchemy в одно или несколько приложений Flask») и передаем ему аргументом объект приложения Flask (строчка 6), создаем класс-модель пользователя (для этого наследуемся от объекта класса SQLAlchemy) (строчка 8), далее указываем основные поля таблицы (модели) c помощью класса Column, сначала указывается класс типа данных (в скобка строковых типов указано максимально допустимое количество символов), а далее дополнительные параметры, например, идентификатору присваивается значение первичного ключа записей в таблице пользователей (primary_key=True) или недопустимости отправки пустого поля email (nullable=False) (строчки 9-11), затем переопределение магического метода __repr__, отвечающего за формат вывода информации об объекте класса(строки 13-14).

В принципе ничего сложного, однако после «копи-паста» у меня не запустилось данное приложение. После запуска интерактивного сеанса, я, согласно документации, импортировал переменную модуля db (я назвал модуль model.py):

>>> from model import db

Затем попытался создать базу:

>>> db.create_all()

Вот здесь получаю ошибку… Я получил трассировку вызова, с ошибкой следующего вида:

sqlite3.OperationalError: unable to open database file

Собственно понятно, что или ошибка в файле или в пути к нему, вероятнее всего второе. Поплясав немного с бубнами, попытался пофиксисить, посмотрел документацию, но тщетно.

Хотелось бы отметить, что на MacOS проблема решилась после изменения пути к файлу базы, а именно после того, как я убрал один слэш из пути (т.е. я изменил 'sqlite:////database.db' на 'sqlite:///database.db')

Что ж, самое время для StackOverflow. Первая же ссылка проливает свет (уже не в первый раз с начала моего пути в программировании)) на то, что кодить в Windows не очень удобно (сугубо субъективное мнение), потому что в пути к файлу бэкслеши (допускаю, что мог ошибиться с выводом!) В результате переписываю код согласно треду и все работает, ниже представляю конечный вариант кода:

import os

from flask import Flask
from flask_sqlalchemy import SQLAlchemy


file_path = os.path.abspath(os.getcwd()) + "\database.db" 
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+file_path
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    
    def __repr__(self):
        return '<User %r>' % self.username

Согласно документации, тестируем приложение:

>>> from model import db 
>>> db.create_all()
>>> from model import User # импорт класса-модели пользователя
>>> admin = User(username='admin', email='admin@example.com') # создание экземпляров пользователей
>>> guest = User(username='guest', email='guest@example.com')
>>> db.session.add(admin) # добавление пользователей в сессию
>>> db.session.add(guest) 
>>> db.session.commit() # Коммит данных сессии в базу данных
>>>User.query.all() # делаем запрос всех записей объектов пользователей из базы
[<User 'admin'>, <User 'guest'>]

Примечание: из-за указанной уникальности полей (unique=True), может вылететь ошибка при попытке закомитить в базу записи с идентичными полями. Чтобы «откатить» данные в сессии стоит прописать:

>>> db.session.rollback()

Попробуем отфильтровать запрос по полю, допустим по e-mail, для этого есть метод filter_by:

>>> User.query.filter_by(email='guest@example.com')

Однако нам возвращается объект типа BaseQuery, а точнее указание на его расположение в памяти:

<flask_sqlalchemy.BaseQuery object at 0x0000000004F80F48>

При этом, можно вывести и сам объект пользователя с помощью указания на первый объект в запросе (функция first()):

>>> User.query.filter_by(email='guest@example.com').first()
<User 'guest'>

Также можно получить конкретную запись таблицы базы данных, по идентификатору с помощью метода get_or_404():

>>> User.query.get_or_404(ident=1)
<User 'admin'>
>>> User.query.get_or_404(ident=2)
<User 'guest'>

Если запросить несуществующую запись, вернется 404-ая ошибка (ниже буду указывать только саму ошибку, без полной трассировки вызова):

>>> User.query.get_or_404(ident=3)
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was 
not found on the server. If you entered the URL manually please check 
your spelling and try again.

Также для фильтрации используется метод filter, отличие хорошо видно на следующем примере:

>>> User.query.filter(User.email=='guest@example.com').first()
<User 'guest'>

Т.е. надо указывать критерий (выражение) сравнения (=='guest@example.com'), с указанием таблицы и поля фильтрации (User.email). Как видно, для простых запросов, все-таки, лучше использовать метод filter_by.

Готово! Теперь мы имеем базу с двумя пользователями.

Дегустация связей между моделями

Продолжим исследование моделей Flask, попробовав сделать связи между ними используя тип поля db.relationship. Создадим еще два-класса модели, поста и групп (для группировки постов):

from datetime import datetime


class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    author = db.relationship('User', backref=db.backref('posts', lazy=True))
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    title = db.Column(db.String(80), nullable=False)
    text = db.Column(db.Text, nullable=False)
    pub_date = db.Column(db.DateTime, nullable=False,
        default=datetime.utcnow)
    group_id = db.Column(db.Integer, db.ForeignKey('group.id'), nullable=False)
    group = db.relationship('Group', backref=db.backref('posts', lazy=True))
    
    def __repr__(self):
        return '<Post %r>' % self.title
class Group(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False)
    description = db.Column(db.Text, nullable=True)
    
    def __repr__(self):
        return '<Group %r>' % self.name

Рассмотрим подробнее поля модели Post.

Поле author, ссылается на модель User (первый аргумент), далее указывается аргумент backref (сокращение от backreference (обратная связь)), таким образом мы создаем поле в модели User, c названием posts (первый аргумент функции db.backref, lazy=True, обозначает, что «SQLAlchemy будет загружать данные по мере необходимости за один раз, используя стандартный оператор select», более подробно здесь. Те же действия происходят с полем group, но в место модели User будет использоваться модель Group, и в ней будет создано поле c обратной связью posts.

Также обратим внимание на поле group_id, тип данных integer, данные будут подтягиваться напрямую из модели (таблицы базы) Group. Связь таблиц осуществляется через функцию db.ForeignKey().

Примечание: если же, например, вы укажете имя поля, этой же модели и оно не будет совпадать с именем (например, поле author в модели поста), тогда SQLAlchemy выдаст ошибку вида:

sqlalchemy.exc.NoReferencedTableError: Foreign key associated with column 
'post.author_id' could not find table 'author' with which to generate a 
foreign key to target column 'id' 

То есть нужны ссылки на модель, через вот такое вот суб-поле (с идентификатором записи в другой таблице базы), чтобы организовать полноценные связи между таблицами базы. Если попытаться сделать связи без этих суб-полей (group_id, author_id), будет выведена ошибка следующего вида:

sqlalchemy.exc.NoForeignKeysError: Could not determine join condition between 
parent/child tables on relationship Post.author - there are no foreign keys 
linking these tables.  Ensure that referencing columns are associated with a 
ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression. 

Протестируем созданные связи.

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

Для начала создадим пару пользователей для новой базы:

>>> from models import db
>>> db.create_all()
>>> from models import User
>>> trinity = User(username='trinity', email='trinity@example.com')
>>> morpheus = User(username='morpheus', email='morpheus@example.com')
>>> db.session.add(trinity)
>>> db.session.add(morpheus)
>>> db.session.commit()
>>> User.query.all()
[<User 'trinity'>, <User 'morpheus'>]

Далее перейдем к созданию группы и наполним ее постами. Обратите внимание, что посты добавляются в группу разными способами. Как через модель Post, так и через прокидывание постов в поле posts группы matrix:

>>> from models import Post, Group
>>> matrix = Group(name='Matrix')
>>> Post(title='Wake up', text='Wake up, Neo...', author=trinity, group=matrix) # создание напрямую через модель
<Post 'Wake up'>
>>> p1 = Post(title='Truth', text='You are a slave, Neo', author=morpheus)
>>> p2 = Post(title='Secret', text='Open your mind', author=morpheus)
>>> matrix.posts.append(p1) # прокидывание поста сразу в posts
>>> matrix.posts.append(p2)
>>> db.session.add(matrix)
>>> matrix.posts
[<Post 'Wake up'>, <Post 'Truth'>, <Post 'Secret'>]

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

Согласно документации Flask-SQLAlchemy, так как в отношениях выбрана так называемая «ленивая» загрузка данных по связям, то это может стать проблемой (в плане скорости загрузки) при подгрузке большого количества записей, однако этого можно избежать. SQLAlchemy позволяет переопределить стратегию загрузки на уровне запроса. При необходимости можно произвести загрузку всех групп и их постов за один запрос, как показано на примере ниже:

>>> from sqlalchemy.orm import joinedload
>>> query = Group.query.options(joinedload('posts'))
>>> for group in query:
...     print(group, group.posts)
<Group 'Matrix'> [<Post 'Wake up'>, <Post 'Truth'>, <Post 'Secret'>]

Если же нужно получить объект типа query для данной связи, можно воспользоваться функцией with_parent():

>>> Post.query.with_parent(matrix).filter(Post.title != 'Truth').all()
[<Post 'Wake up'>, <Post 'Secret'>]

Заключение

На этом я пока прерву свой рассказ, если это опубликуется, я планирую дальше постараться описывать свой путь исследования Flask. На данный момент, как видно из описанного выше, все довольно просто в усвоении, но как говориться: «Дьявол кроется в деталях», буду надеется, Flask окажется исключением.

«Follow the white rabbit.»

Теги:
Хабы:
Всего голосов 8: ↑4 и ↓4+1
Комментарии10

Публикации

Истории

Работа

Data Scientist
77 вакансий
Python разработчик
117 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань