{{ post.title }}
Автор: {{ post.author }} Дата: {{ post.created_on }}
Привет, Хабр! Сегодня поговорим об одном интересном микро-фреймворке для Python — Flask. Мы создадим свое собственное веб-приложение и изучим расширения flask, а после задеплоим его на сервер, чтобы иметь доступ из внешнего мира.
Flask всегда мне нравился, ибо он был минималистичный, быстрый, лёгкий для изучения, и в то же время легко расширялся до полноценного проекта.
Мы затронем все моменты, я объясняю каждую строчку кода. Мы будем создавать не просто какой то статичный сайт —, а открытую публичную стену, с регистрацией и авторизацией. Каждый может туда зайти, авторизоваться и оставлять посты на общедоступной стене.
А самое главное — безболезненный, быстрый и легкий деплой будущего приложения.
Наш проект я назвал Open Wall. Оригинально, но вы можете назвать как хотите, главное название поменяйте.
Чтобы понимать, о чем мы будем разговаривать, вам требуется знать сам язык python, язык разметки HTML и CSS. Без этих базовых знаний вы мало что поймете.
По моему концепту, Open Wall будет состоять из следующего функционала:
Open Wall будет иметь базовый функционал. Если проект станет популярным — может быть, я улучшу его, и сделаю новый туториал на тему улучшения. Например, добавление flask-admin.
Если вы хотите сразу опробовать его — перейдите на сам сайт или в его репозиторий.
За дизайн сайта не ругайте, вы вольны его изменить как хотите, я быстро сделал легкий, адаптивный сайт.
Итак, допустим вы хотите сделать проект с самого начала. Тогда следуйте дальнейшим шагам для создания базового виртуального окружения. Виртуальное окружение позволит изолировать ваш проект от остальных. Все команды будут указаны для Linux.
python3 -m venv venv && source venv/bin/activate && pip3 install flask gunicorn flask_login flask_sqlalchemy && pip3 freeze > requirements.txt
Этой командой мы: а) создали виртуальное окружение и активировали его; б) установили зависимости — сам flask и gunicorn для запуска его на сервере; в) «заморозили» зависимости в файле, чтобы после можно было их установить на сервер.
После создаем базовую архитектуру:
Итак, давайте напишем само приложение:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
Давайте разберем каждую строчку:
В Flask используется шаблонизатор Jinja. Это быстрая программа, которая позволяет создавать HTML-шаблоны. Вот как выглядит базовый шаблон base.html:
Document
{% block content %}
{% endblock %}
Здесь вы можете увидеть фигурные скобки с процентами — они используются если нужны какие-то функции, например цикл for:
{% for item in items %}
{{ item }}
{% endfor %}
Также есть просто двойные фигурные скобки — они нужны, например, для переменных.
Flask — это микрофреймворк, поэтому база данных может быть любая, даже просто встроенный модуль sqlite3. Но мы будем использовать специальное расширение — Flask SQLAlchemy. Это ORM модуль, с возможностью подключения различных БД — от sqlite до MySQL. Мы будем использовать sqlite.
Вот пример модели пользователя для БД:
from datetime import datetime
from flask import Flask, render_template, url_for, request, redirect, flash, get_flashed_messages
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, current_user, login_user, logout_user, login_required
from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy import desc
app = Flask(__name__)
login = LoginManager(app)
login.login_view = 'login'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///wall.db'
app.config['SECRET_KEY'] = 'fc186fd3-3ace-46c8-8031-a819ec7d9a0e'
db = SQLAlchemy(app)
@login.user_loader
def load_user(id):
return db.session.get(User, int(id))
@app.route('/logout')
def logout():
logout_user()
return redirect(url_for('index'))
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer(), primary_key=True)
title = db.Column(db.String(255), nullable=False)
content = db.Column(db.Text(), nullable=False)
created_on = db.Column(db.DateTime(), default=datetime.utcnow)
author = db.Column(db.Integer(), nullable=False)
def __repr__(self):
return "<{}:{}>".format(self.id, self.title[:10])
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(63), index=True, unique=True)
password_hash = db.Column(db.String(127))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return '<{}:{}>'.format(self.id, self.username[:10])
Давайте разберем код подробнее:
После идет создание экземпляров классов и настройка:
Теперь давайте создадим некоторые нужные нам функции — это загрузка пользователя и выход из аккаунта:
Настала очередь создать две модели для базы данных — это модель поста:
И модель пользователя:
Вот и первая часть кода позади. Мы создали уже основу, скелет приложения, но не хватает главного — самого функционала сайта, взаимодействия с БД.
Итак, начнем c функции index. Главная функция, отображает корневой каталог сайта.
@app.route('/')
@app.route('/index')
def index():
posts = Post.query.order_by(desc(Post.created_on)).all()
return render_template('index.html', posts=posts)
В функции index мы задаем два пути отображения — / и /index. После мы создаем список постов, обращаемся к модели Post, и требуем выдать все посты, отсортированные по дате создания. После мы делаем рендер шаблона, и передаем список постов аргументов, чтобы потом использовать его.
Давайте рассмотрим сам шаблон index.html и base.html. Сами шаблоны хранятся в директории templates. base.html — это базовый шаблон, от него наследуются уже все остальные. В нем мы задаем форму и создаем блоки. В общем, вот как выглядит он у меня:
Open Wall
Open Wall - открытая публичная стена
Репозиторий
Статья
{% if not current_user.is_authenticated %}
Войти
Регистрация
{% else %}
Создать пост
{% endif %}
{% block content %}
{% endblock %}
Думаю, вы знаете HTML, но некоторые моменты надо прояснить. Как мы уже говорили, фигурные скобки с процентами — для функций шаблонизатора Jinja, а просто двойные фигурные скобки — для переменных.
Итак, в head-блоке, в элементе link, где мы задаем CSS стили, можно увидеть строку {{ url_for ('static', filename='css/style.css') }}. Чтобы нам вручную не вводить путь, мы задаем путь через url_for (функция из Flask). Здесь мы задаем путь до стилей, лежат они в директории static. Вторым аргументом мы задаем путь до файла — css/style.css.
Если вы хотите увидеть стили (я не стал сюда их вставлять, ибо это заняло бы слишком много места) — то перейдите по ссылке.
Ниже мы видим конструкцию типа if-else, то есть условный оператор. Первой строкой мы узнаем, текущий пользователь авторизован или нет. Если нет, то мы выводим ссылки на вход и регистрацию, а иначе — одну ссылку на создание поста. В последней строке мы даем шаблонизатору понять, что конструкция закончилась.
И последнее — это блок контента. Чтобы мы могли наследовать и задавать контент, мы создадим блок, и в других шаблонах мы можем туда вписывать что нам нужно.
Теперь рассмотрим файл index.html:
{% extends 'base.html' %}
{% block content %}
{% for post in posts %}
{% endfor %}
{% endblock %}
Теперь займемся функцией регистрации:
@app.route('/reg', methods=['GET', 'POST'])
def reg():
if current_user.is_authenticated:
return redirect(url_for('index'))
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
password2 = request.form['password2']
if password != password2:
flash('Пароли не совпадают')
return render_template('reg.html')
if len(username) > 63:
flash('Длина имени пользователя не должна быть больше 63 символов')
return render_template('reg.html')
try:
user = User(username=username)
user.set_password(password)
db.session.add(user)
db.session.commit()
except Exception as e:
flash(f'Ошибка при создании пользователя: {e}')
return render_template('reg.html')
else:
login_user(user)
return redirect('index')
return render_template('reg.html')
Мы задаем путь отображения — это /reg. Также мы добавляем в декораторе параметр METHODS, который определяет принимаемые запросы — GET и POST.
После мы проверяем, авторизован ли пользователь. Если да, то отправляем его на главную страницу.
Следующий проверяет метод запроса — если это POST, то есть отправка данных, то мы начинаем регистрацию. Мы получаем username, пароль и повторение пароля из формы, проверяем пароли, проверяем длину имени пользователя и после мы создаем нового пользователя, автоматически его авторизуя в системе. При успешной регистрации перенаправляем на главную страницу, а при провале или при GET-запросе — рендерим шаблон reg.html:
{% extends 'base.html' %}
{% block content %}
Регистрация
{% endblock %}
В это шаблоне мы также используем функцию get_flashed_messages для отображения flash-сообщений.
Следующим шагом мы создадим функцию логина пользователя:
@app.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('index'))
if request.method == 'POST':
user = db.session.query(User).filter(User.username == request.form['username']).first()
if user is not None:
if not user.check_password(request.form['password']):
flash('Пароль неверный')
return render_template('login.html')
login_user(user)
return redirect(url_for('index'))
else:
flash('Пользователя не существует. Проверьте имя пользователя или пароль.')
return render_template('login.html')
Также мы задаем типы методов, и также проверяем авторизован ли уже пользователь.
После мы проверяем, существует ли пользователь с таким именем в системе. Если он существует, то мы проверяем хеш паролей (если пароль неверный, выводим об этом сообщение). Если хеши совпали, то можно авторизовать пользователя и перевести его на главную страницу.
Шаблон страницы логина:
{% extends 'base.html' %}
{% block content %}
Вход в аккаунт
{% endblock %}
Все практически также, как и при регистрации. Разве что без повторного ввода пароля.
Теперь давайте немного отдохнем, напишем несложную функцию отображения профиля:
@app.route('/profile/')
def profile(username: str):
user = db.session.query(User).filter(User.username == username.first()
if user is None:
return redirect('index')
posts = db.session.query(Post).filter(Post.author == username).order_by(desc(Post.created_on)).all()
return render_template('profile.html', username=username, posts=posts)
В пути вы могли увидеть после второго слэша слово username. Это специальный аргумент, который будет означать произвольное слово после /profile/. В данном случае — пользователя. Этот аргумент также есть в функции.
Также, как и при логине мы проверяем, существует ли пользователь. Если нет, то делаем редирект на главную страницу. Если пользователь существует, мы собираем все посты данного пользователя и сортируем по дате, и после передаем username и posts в функцию render_template. А вот сам шаблон профиля:
{% extends 'base.html' %}
{% block content %}
Пользователь: {{ username }}
{% if current_user.username == username %}
Выйти из аккаунта
{% endif %}
Посты
{% for post in posts %}
{% if post.author == username %}
{{ post.title }}
{{ post.content }}
Дата: {{ post.created_on }}
{% endif %}
{% endfor %}
{% endblock %}
Здесь я добавил небольшую фичу — если пользователь перешел на страницу своего же аккаунта, то мы добавляем ссылку для выхода из профиля.
И наконец-то последняя функция — функция создания нового поста.
@login_required
@app.route('/new', methods=['GET', 'POST'])
def new_post():
if not current_user.is_authenticated:
return redirect('login')
if request.method == 'POST':
title = request.form['title']
content = request.form['content']
if len(title) > 0 and len(title) < 256 and len(content) > 0:
post = Post(title=title, content=content, author=current_user.username)
try:
db.session.add(post)
db.session.commit()
except Exception as e:
flash(f'Возникла ошибка при записи в базу данных: {e}')
else:
return redirect('index')
else:
flash('Ошибка, длина заголовка поста не соответствует стандартам. Максимальное количество символов заголовка - 255.')
return render_template('newpost.html')
return render_template('newpost.html')
Здесь как раз мы и используем декоратор login_required. И также мы, на случай обхода этого декоратора, создаем условие на проверку, авторизован ли пользователь. Если нет — то посылаем его на страницу логина.
Потом мы также, как и при регистрации, если метод POST получаем заголовок и контент поста, проверяем его на размер, создаем новую модель и отправляем его в БД.
Вот шаблон newpost.html:
{% extends 'base.html' %}
{% block content %}
Создание статьи
{% endblock %}
В файл app.py (главный файл приложения), в самый конец, прописываем специальный условный оператор:
if __name__ == '__main__':
app.run(debug=True, port=5000)
Если программа запускается напрямую (не импортируется), то мы запускаем приложение с включенным дебагом и портом на 5000.
После мы создаем файл create_db.py, который будет создавать файл базы данных:
from app import app, db
with app.app_context():
db.create_all()
Мы импортируем app и db из файла приложения (app) и при помощи контекстного менеджера with вызываем метод create_all из db.
И давайте создадим последний файл — main.py, он и будет запускать наш сервер, именно через него gunicorn (специальный модуль для запуска фласк-приложения на сервере) будет запускать нашу стену:
from app import app
if __name__ == '__main__':
app.run(debug=False)
И наконец то создадим небольшой bash-скрипт deploy.sh, для деплоя на сервер без лишних телодвижений:
#!/bin/bash
pip3 install -r requirements.txt
python3 create_db.py
echo "END"
Здесь мы устанавливаем зависимости и создаем БД.
Если вы хотите запустить сайт — можете ввести две команды (на выбор):
И вот, все готово. Мы огромные молодцы. Можно скинуть свой сайт другу… Стоп, так мы же на локалхосте?
Если вы хотите опубликовать наше приложение на сервер, то есть два варианта:
Оба варианта нам не подойдут. Первый слишком муторный, а 2, к сожалению, из РФ недоступен.
Но есть еще один вариант — Cloud Apps от Timeweb Cloud. Это сервис для быстрого деплоя приложения, чтобы можно было его быстро опубликовать и забыть.
Думаю у многих возникала усталость от бесконечного конфигурирования серверов или постоянной монотонной рутины — подключился на сервер, клонировал репозиторий, установил, отключился. И так по кругу.
Или просто появлялась банальная лень — хотелось бы просто указать репозиторий, указать команды для сборки, и чтобы все сделали за тебя… Желательно чтобы и недорого, и с логами, и с поддержкой нескольких языков программирования и фреймворков. И Docker-контейнеры, и бекенд, и фронтенд.
Есть Netlify, Vercel —, но их использование, из-за геополитического конфликта, в России ограничено.
Давайте опубликуем наше веб-приложение!
Приложение делится на три типа — frontend, backend и docker. Cloud Apps поддерживает большинство популярных языков программирования и фреймворков — «большая тройка» JS-библиотек (vue, react, angular) и другие популярные библиотеки или даже просто ванильный nodeJS, бекенд — от PHP и NodeJS до Java, Python, Go, Elixir и .NET, а также просто через Docker-контейнер. Мы будем использовать backend, python (Flask).
Плюсы Cloud Apps:
Минусы, это и так понятно, что для больших высоконагруженных проектов, где надо постоянно заходить на сервер, это будет не удобно.
Как я уже говорил, Apps — это облачный сервис для автоматической выгрузки кода и автодеплоя ваших приложений на серверах Timeweb.
Работает он так:
После запуска сервиса вы можете работать с кодом, как обычно: вносить правки и дополнения и делать коммиты в репозиторий. Сервис Apps автоматически отследит наличие изменений и, если у вас включен автодеплой, выкатит обновления в продакшен-среду.
Если что-то пошло не так и нужно откатиться на прошлую версию — запустите новый деплой с коммитом, по которому был последний успешный деплой.
К приложению будет привязан бесплатный технический домен с SSL Let’s Encrypt, который можно использовать для тестирования и запросов к вашему приложению.
Основная функция сервиса приложений — автоматический деплой. Apps автоматически выгружает на сервер код вашего сайта, API-сервиса, приложения и т.п.
Для бекенда также автоматически запускается сервер на nginx, а также приложение хранится в Docker-контейнере.
Работа сервиса с frontend-приложениями имеет одно важное отличие от backend-приложений — после сборки не создается Docker-контейнер, приложение хранится в директории на сервере. Такое приложение — это статические файлы, которые отдаются клиентам с сервера.
Однако, в отличие от обычного размещения приложения на сервере, где вам нужно самостоятельно настраивать окружение, сервис Apps, как и в случае с бэкенд-приложениями, сделает всё за вас:
А в дальнейшем будет автоматически деплоить изменения — если вы оставите включенной опцию автодеплоя.
Время попробовать задеплоить нашу открытую стену. Но перед созданием Cloud App нам требуется сам репозиторий — и этот репозиторий можеть быть на вашем GitHub (но также поддерживается GitLab и BitBucket) аккаунте, либо можно даже просто склонировать по URL. Мы советуем первый способ, т.к. так можно сделать всю настройку, и запушить все в приватный репозиторий, ведь при первом способе можно импортировать даже их, в отличии от второго.
Если у вас остались вопросы, то советуем перейти на документацию по подключению репозиториев. Там все рассказано подробно, если у вас возникнут ошибки или проблемы.
Для начала вам потребуется зайти на Timeweb Cloud и зарегистрироваться или войти в аккаунт.
После этого вы попадете в ваш личный кабинет:
После перейдите на вкладку «Apps»:
Займемся переносом приложения на Cloud Apps. Укажите URL вашего GIT-репозитория, в этом примере — наш сайт:
После этого в команде сборки указываем bash deploy.sh, а в команде запуска указываем gunicorn main: app --timeout 60:
Готово! Можете перейти на сайт и протестировать нашу небольшую публичную стену!
Приложения могут быть невероятно полезны. Быстро опубликовать сайт-визитку, протестировать что-то или сделать временный сайт — без проблем. Но когда дело касается серьезных проектов — лучше использовать обычные сервера и потратить время на ручную настройку.
Я надеюсь, вам понравилась эта статья. Мы смогли написать довольно хорошее приложение на Flask и быстро опубликовать его. Буквально за день вы получаете +1 проект в ваше портфолио.
Если у вас есть мнение по коду — то прошу их оставить в комментарии, я обязательно отвечу.
Дуров, верни стену!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩