Структура и эскиз приложения Flask

Ранее вся программа, с которой мы работали с использованием фреймворка 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. Чтобы его создать, нужно воспользоваться одноименным конструктором, который принимает два аргумента: имя эскиза и имя пакета, где он находится. Как правило, достаточно передать имя. 

Выводы

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

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

ОфисГуру
Adblock
detector