Ранее вся программа, с которой мы работали с использованием фреймворка Flask, находилась в файле с названием main2.py. Работать с одним файлом нормально, если необходимо создать небольшое приложение. Тем не менее, если требуется более широкий функционал, управлять программой становится очень непросто. Если разбить большой файл на большее количество маленьких, можно добиться большей читабельности кода.
Flask дает широкий набор возможностей по структурированию программ. Тем не менее, есть ряд рекомендаций, которые желательно соблюдать для того, чтобы создать качественные модульные программы.
В целом, структура программы будет такой.
/app_dir /app __init__.py /static /templates views.py config.py runner.py
Приведем описание каждого файла и папки:
Файл | Описание |
app_dir | Корневая папка проекта |
app | Пакет Python с файлами представления, шаблонами и статическими файлами |
__init__.py | Этот файл сообщает Python, что папка app — пакет Python |
static | Папка со статичными файлами проекта |
templates | Папка с шаблонами |
views.py | Маршруты и функции представления |
config.py | Настройки приложения |
runner.py | Точка входа приложения |
Теперь рассмотрим методы, как добиться такой структуры проекта.
Настройки на основе классов
В целом, проект работает в трех средах сразу: разработка, тестирование и непосредственно работа с программой. По мере развития приложения необходимо для каждой среды задать специфические параметры. Но ряд из них все равно будут одинаковыми, независимо от того, на каком уровне осуществляется их использование.
Для начала необходимо определить, какие настройки будут по умолчанию. Это делается непосредственно в базовом классе. И лишь потом необходимо создавать классы для отдельных сред. Они будут наследовать параметры из базового или иметь свои.
Давайте создадим файл конфигурации config.py внтури папки flask_app, и туда вставим такой код:
import os app_dir = os.path.abspath(os.path.dirname(__file__)) class BaseConfig: SECRET_KEY = os.environ.get('SECRET_KEY') or 'A SECRET KEY' SQLALCHEMY_TRACK_MODIFICATIONS = False ##### настройка Flask-Mail ##### MAIL_SERVER = 'smtp.googlemail.com' MAIL_PORT = 587 MAIL_USE_TLS = True MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or 'YOU_MAIL@gmail.com' MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or 'password' MAIL_DEFAULT_SENDER = MAIL_USERNAME class DevelopementConfig(BaseConfig): DEBUG = True SQLALCHEMY_DATABASE_URI = os.environ.get('DEVELOPMENT_DATABASE_URI') or \ 'mysql+pymysql://root:pass@localhost/flask_app_db' class TestingConfig(BaseConfig): DEBUG = True SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URI') or \ 'mysql+pymysql://root:pass@localhost/flask_app_db' class ProductionConfig(BaseConfig): DEBUG = False SQLALCHEMY_DATABASE_URI = os.environ.get('PRODUCTION_DATABASE_URI') or \ 'mysql+pymysql://root:pass@localhost/flask_app_db'
Учтите, что здесь значения ряда настроек изначально заимствованы у переменных среды. Помимо этого, есть ряд значений по умолчанию. Чтобы понимать параметры, используется метод from_object().
app.config.from_object('config.Create')
Создание пакета предложения
В папку flask_app необходимо добавить новую папку, которая будет называться app, и почти все имеющиеся файлы и папки приложения переместить туда. Только не надо перемещать папки env и migrations.
Далее в этой папке создаем файл с таким кодом. Назовем его __init__.py
from flask import Flask from flask_migrate import Migrate, MigrateCommand from flask_mail import Mail, Message from flask_sqlalchemy import SQLAlchemy from flask_script import Manager, Command, Shell from flask_login import LoginManager import os, config # создание экземпляра приложения app = Flask(__name__) app.config.from_object(os.environ.get('FLASK_ENV') or 'config.DevelopementConfig') # инициализирует расширения db = SQLAlchemy(app) mail = Mail(app) migrate = Migrate(app, db) login_manager = LoginManager(app) login_manager.login_view = 'login' # import views from . import views # from . import forum_views # from . import admin_views
Затем изменяем название файла main2.py на views.py и вносим его такие изменения, чтобы в нем были маршруты и функции представления. Здесь должны содержаться не только функции представления, но и код моделей, классов форм, и так далее.
Также необходимо создать разные файлы, каждый из которых отвечает за свои конкретные функции и с кодом.
Приводим полный код обновленного views.py.
from app import app from flask import render_template, request, redirect, url_for, flash, make_response, session from flask_login import login_required, login_user,current_user, logout_user from .models import User, Post, Category, Feedback, db from .forms import ContactForm, LoginForm from .utils import send_mail @app.route('/') def index(): return render_template('index.html', name='Jerry') @app.route('/user/<int:user_id>/') def user_profile(user_id): return "Profile page of user #{}".format(user_id) @app.route('/books/<genre>/') def books(genre): return "All Books in {} category".format(genre) @app.route('/login/', methods=['post', 'get']) def login(): if current_user.is_authenticated: return redirect(url_for('admin')) form = LoginForm() if form.validate_on_submit(): user = db.session.query(User).filter(User.username == form.username.data).first() if user and user.check_password(form.password.data): login_user(user, remember=form.remember.data) return redirect(url_for('admin')) flash("Invalid username/password", 'error') return redirect(url_for('login')) return render_template('login.html', form=form) @app.route('/logout/') @login_required def logout(): logout_user() flash("You have been logged out.") return redirect(url_for('login')) @app.route('/contact/', methods=['get', 'post']) def contact(): form = ContactForm() if form.validate_on_submit(): name = form.name.data email = form.email.data message = form.message.data # здесь логика БД feedback = Feedback(name=name, email=email, message=message) db.session.add(feedback) db.session.commit() send_mail("New Feedback", app.config['MAIL_DEFAULT_SENDER'], 'mail/feedback.html', name=name, email=email) flash("Message Received", "success") return redirect(url_for('contact')) return render_template('contact.html', form=form) @app.route('/cookie/') def cookie(): if not request.cookies.get('foo'): res = make_response("Setting a cookie") res.set_cookie('foo', 'bar', max_age=60*60*24*365*2) else: res = make_response("Value of cookie foo is {}".format(request.cookies.get('foo'))) return res @app.route('/delete-cookie/') def delete_cookie(): res = make_response("Cookie Removed") res.set_cookie('foo', 'bar', max_age=0) return res @app.route('/article', methods=['POST', 'GET']) def article(): if request.method == 'POST': res = make_response("") res.set_cookie("font", request.form.get('font'), 60*60*24*15) res.headers['location'] = url_for('article') return res, 302 return render_template('article.html') @app.route('/visits-counter/') def visits(): if 'visits' in session: session['visits'] = session.get('visits') + 1 else: session['visits'] = 1 return "Total visits: {}".format(session.get('visits')) @app.route('/delete-visits/') def delete_visits(): session.pop('visits', None) # удаление посещений return 'Visits deleted' @app.route('/session/') def updating_session(): res = str(session.items()) cart_item = {'pineapples': '10', 'apples': '20', 'mangoes': '30'} if 'cart_item' in session: session['cart_item']['pineapples'] = '100' session.modified = True else: session['cart_item'] = cart_item return res @app.route('/admin/') @login_required def admin(): return render_template('admin.html')
Приведем коды еще некоторых файлов.
models.py
from app import db, login_manager from datetime import datetime from flask_login import (LoginManager, UserMixin, login_required, login_user, current_user, logout_user) from werkzeug.security import generate_password_hash, check_password_hash class Category(db.Model): __tablename__ = 'categories' id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(255), nullable=False, unique=True) slug = db.Column(db.String(255), nullable=False, unique=True) created_on = db.Column(db.DateTime(), default=datetime.utcnow) posts = db.relationship('Post', backref='category', cascade='all,delete-orphan') def __repr__(self): return "<{}:{}>".format(self.id, self.name) post_tags = db.Table('post_tags', db.Column('post_id', db.Integer, db.ForeignKey('posts.id')), db.Column('tag_id', db.Integer, db.ForeignKey('tags.id')) ) class Post(db.Model): __tablename__ = 'posts' id = db.Column(db.Integer(), primary_key=True) title = db.Column(db.String(255), nullable=False) slug = db.Column(db.String(255), nullable=False) content = db.Column(db.Text(), nullable=False) created_on = db.Column(db.DateTime(), default=datetime.utcnow) updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onudate=datetime.utcnow) category_id = db.Column(db.Integer(), db.ForeignKey('categories.id')) def __repr__(self): return "<{}:{}>".format(self.id, self.title[:10]) class Tag(db.Model): __tablename__ = 'tags' id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(255), nullable=False) slug = db.Column(db.String(255), nullable=False) created_on = db.Column(db.DateTime(), default=datetime.utcnow) posts = db.relationship('Post', secondary=post_tags, backref='tags') def __repr__(self): return "<{}:{}>".format(self.id, self.name) class Feedback(db.Model): __tablename__ = 'feedbacks' id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(1000), nullable=False) email = db.Column(db.String(100), nullable=False) message = db.Column(db.Text(), nullable=False) created_on = db.Column(db.DateTime(), default=datetime.utcnow) def __repr__(self): return "<{}:{}>".format(self.id, self.name) class Employee(db.Model): __tablename__ = 'employees' id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(255), nullable=False) designation = db.Column(db.String(255), nullable=False) doj = db.Column(db.Date(), nullable=False) @login_manager.user_loader def load_user(user_id): return db.session.query(User).get(user_id) class User(db.Model, UserMixin): __tablename__ = 'users' id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(100)) username = db.Column(db.String(50), nullable=False, unique=True) email = db.Column(db.String(100), nullable=False, unique=True) password_hash = db.Column(db.String(100), nullable=False) created_on = db.Column(db.DateTime(), default=datetime.utcnow) updated_on = db.Column(db.DateTime(), default=datetime.utcnow, onupdate=datetime.utcnow) def __repr__(self): return "<{}:{}>".format(self.id, self.username) 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) forms.py from flask_wtf import FlaskForm from wtforms import Form, ValidationError from wtforms import StringField, SubmitField, TextAreaField, BooleanField from wtforms.validators import DataRequired, Email class ContactForm(FlaskForm): name = StringField("Name: ", validators=[DataRequired()]) email = StringField("Email: ", validators=[Email()]) message = TextAreaField("Message", validators=[DataRequired()]) submit = SubmitField() class LoginForm(FlaskForm): username = StringField("Username", validators=[DataRequired()]) password = StringField("Password", validators=[DataRequired()]) remember = BooleanField("Remember Me") submit = SubmitField()
utils.py
from . import mail, db from flask import render_template from threading import Thread from app import app from flask_mail import Message def async_send_mail(app, msg): with app.app_context(): mail.send(msg) def send_mail(subject, recipient, template, **kwargs): msg = Message(subject, sender=app.config['MAIL_DEFAULT_SENDER'], recipients=[recipient]) msg.html = render_template(template, **kwargs) thrd = Thread(target=async_send_mail, args=[app, msg]) thrd.start() return thrd runner.py import os from app import app, db from app.models import User, Post, Tag, Category, Employee, Feedback from flask_script import Manager, Shell from flask_migrate import MigrateCommand manager = Manager(app) # эти переменные доступны внутри оболочки без явного импорта def make_shell_context(): return dict(app=app, db=db, User=User, Post=Post, Tag=Tag, Category=Category, Employee=Employee, Feedback=Feedback) manager.add_command('shell', Shell(make_context=make_shell_context)) manager.add_command('db', MigrateCommand) if __name__ == '__main__': manager.run()
Порядок выполнения
Сейчас объясним логику, по которой осуществляется работа с файлами. Изначально запускается файл runner.py. Вторая строка в этом файле осуществляет импорт app и db из пакета app. После того, как интерпретатор Python доходит до этой строки, приложением начинает управлять файл __init__.py, который в этот момент начинает выполняться.
Соответственно, интерпретатор начинает работать с ним. На 7 строке этого файла осуществляется импорт модуля config. После этого начинает выполняться файл config.py. По окончании этого процесса, выполняется тот же самый файл, который был до этого.
Потом выполняются команды, указанные в файле __init__.py, вплоть до 21 строки, на которой осуществляется импорт модуля views. После этого в программе начинает работать файл с одноименным названием и расширением .py. Затем первая строка этого файла импортирует app из одноименного пакета. Повторного импорта не требуется, поскольку он уже содержится в памяти.
Далее идет продвижение по файлу views.py, где с четвертой по шестую строку осуществляется импорт моделей, форм и функции send_mail. Управление передается файлам, которые отвечают за это.
После того, как все инструкции, которые выполнены в этом файле, выполняются, продолжает выполняться файл __init__.py, после чего он завершается. И затем снова берет исполнение на себя runner.py. После этого исполняется та последовательность команд, которая находится на третьей строке, задача которой – импортировать те классы, определение которых было в модуле models.py. Так, как модели уже доступны в views.py, то models.py выполяться не будет.
Так, как runner.py– это основной модуль, а условие на 17 строке выполняется, то manager.run запускает исполнение программы.
Запуск приложения
Теперь возможен запуск проекта. Для того, чтобы это сделать, необходимо открыть терминал и ввести такую команду.
(env) gvido@vm:~/flask_app$ python runner.py runserver * Restarting with stat * Debugger is active! * Debugger PIN: 391-587-440 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Немного ниже под командой показывается вывод терминала о состоянии ее выполнения.
Если переменная среды FLASK_ENV не определена, то если использовать предыдущую команду, программа начнет работать в режиме отладки. При переходе по адресу http://127.0.0.1:5000 откроется страница, в котором будет такое содержание.
Name: Jerry
Помимо этого, чтобы удостовериться в том, что все работает правильно, надо проверить все оставшиеся страницы программы.
Теперь она является гибкой. Таким образом, можно получить совершенно другой набор параметров, воспользовавшись всего одной переменной среды.
Допустим, необходимо перевести программу в рабочий режим. Для этого достаточно создать переменную FLASK_ENV, в качестве значения которой будет config.ProductionConfig.
В терминале надо ввести такую инструкцию, чтобы создать переменную среды.
(env) gvido@vm:~/flask_app$ export FLASK_ENV=config.ProductionConfig
Эта команда может быть выполнена только на Unix-подобных системах. Пользователи Windows должны использовать другую команду.
(env) C:\Users\gvido\flask_app>set FLASK_ENV=config.ProductionConfig
Создание эскиза
Эскизы – это еще один метод организации приложения. В этом случае разделение осуществляется на уровне представления. Точно так же, как и программа, написанная с помощью Flask, эскиз может иметь собственные функции представления, шаблоны и статические файлы.
Допустим, необходимо поработать с блогом и административной панелью. В чертеж будет включаться функция представления, шаблоны и статические файлы, которые подходят исключительно для задач, характерных для блога. При этом эксиз административной панели будет содержать исключительно нужные ему файлы.
Чтобы создать эскиз, нужно создать папку main в папке flask_app/app и разместить там views.py и forms.py. Для этого создается файл инициализации __init__.py, в котором содержится такая последовательность команд.
from flask import Blueprint main = Blueprint('main', __name__) from . import views
Здесь генерируется объект эскиза с использованием класса Blueprint. Чтобы его создать, нужно воспользоваться одноименным конструктором, который принимает два аргумента: имя эскиза и имя пакета, где он находится. Как правило, достаточно передать имя.
Выводы
Мы рассмотрели основные аспекты, которые надо знать для работы с созданием структуры и эскиза приложения. Конечно, это не все. Например, есть такое понятие, как фабрика приложения. Это функция, которая создает объект. Таким образом можно сделать процесс тестирования значительно проще, а также в одном и том же процессе запускать несколько экземпляров одной и той же программы. Но это – другая тема, которая заслуживает рассмотрения в отдельной статье.
Для более профессионального понимания темы необходимо сначала научиться выполнять более простые задачи. Для начала, надо освоить это руководство. Попробуйте, потренируйтесь.