☆☆ 新着記事 ☆☆

2019年3月4日月曜日

Flaskで大規模アプリを作るときのファイル分割方法(Factory Model)

Flaskの大きな特徴は、1つのアプリケーション・ファイルで、htmlのルーティングから、DBの作成など、全部書けてしまうことです。 大変、便利。

デフォルトで、各機能ごとにファイルを分割して記述しなくてはいけないDjangoとは大きく違うところです。

Flaskは、1つのファイルに全てを書けるので、直観的に分かり易く、学習コストが非常に低いです。
それでも、相当のアプリケーションを書くことができます。

チュートリアルには、最初から、ファイルを分割してアプリを記述する説明をしているものがありますが、そこまでしなくれも、Flaskでは、1つのアプリケーション・ファイルに全てを書いても、相当程度のアプリは作れます。(最初から、分割しないといけないなら、Djangoで良いのでは?)

それでも、1つのアプリファイルの中のコード数が多くなって、可読性が低下してきた場合、ファイルを分割した方が便利な場合があります。 そこで、Flaskで大規模アプリを作るときの、メモしておきます。 (Flaskのblueprintは使わずに、__init__.pyやroutes.pyを自分で作成します。)

分割するとなると、FlaskのPackageやExtensionの仕方に注意をしなければならなくなります。
(Import のサーキュレーション(循環)を回避したい、という時には大事なようです。

基本は、プロジェクト・ディレクトリーの配下に、もう1つディレクトリーを作って、そこに__init__.pyを設置することで、そのディレクトリーに格納されているファイル全体を、1つのパッケージとして管理する、ということのようです。

--------------------------------------------------------------------------------

1.__init.py__

サブフォルダ内に、このファイルがあるフォルダ内のオブジェクトは、1つのまとまったパッケージとして扱われる。 (Flaskのextensionなど、プレ・ディターミンされたものをコントロール)

【基本構文】

① from flask import Flask
② app = Flask (__name__)
③from app import routes


① flask pkgから、アプリケーション・オブジェクトを class Flaskのオブジェクトとして作成する。

②モジュールのスターティング・ポイントとして指定

③ルーティングは一番最後に記述(著名な再読み込み問題を回避する為)

【追加した例】

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

app = Flask(__name__)
    db = SQLAlechemy(app)
    migrate = Migrate(app, db) 

2. routes.py
view functionとも呼ばれる。 urlのディレクトリーとfunctionをmappingする。

【プロジェクト・ディレクトリ/app/routes.py】

from app import app

@app.route('/')
@app.route('/index')
def index()
    return 


3. config.py

コンフィグを記述する場合、一般的には以下のように記述します。

 【プロジェクト・ディレクトリ/config.py】

app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-know'

これをconfig.pyに専用のファイルに記述する場合、

project folder/config.py

import os
class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-know'

とします。 configurationの設定を、Class変数として定義してしまいます。

定義したら、appパッケージ(自作)の__init__pyでインポートします。

project folder/app/__init__.py

from flask import Flask
from config import Config
 app = Flask(__name__)
app.config.from_object(Config)

from app import routes

configは、上述のモジュールで、 Configは、モジュール内で定義されたクラスになります。

4.  forms.py

htmlのformから値を受け取る場合に作る 。 Fieldを定義。
wt_formsを使って、データ入力をするfieldをclassとして定義する。

ここで定義するclassは、基本的にformを含むhtmlページ毎に作成する。

from flask_wtf  import FlaskForm
 from wtforms import StringField, PasswordField,SubmitField
 from wtforms.validators import DataRequired
 class LoginForm(FlaskForm):
     username = StringField('unsername', validators=[DataRequired()])
     password = PasswordField('password', validators=[DataRequired()])
     submit = SubmitField('Sign In')



関数も、このファイルに記述できる。


from flask_wtf  import FlaskForm
 from wtforms import StringField, PasswordField,SubmitField
 from wtforms.validators import DataRequired
 class EditProfile(FlaskForm):
     username = StringField('unsername', validators=[DataRequired()])
     password = PasswordField('password', validators=[DataRequired()])
     submit = SubmitField('Sign In')

    def __init__(self, original_username, *args, ** kwargs):
        super(EditProfileForm, self).__init__(*args, ** kwargs)
        self.original_username = original_username



 *super :親クラスの中で動くようinvokeする。

* route.pyで反映
 @app.route('/edit_profile', methods = ['GET','POST']

5.  models.py

データベースのテーブルを定義。
SQLAlchemyの場合、ここで定義されたclassを RDBのrawに変換してくれる。


from app  import db
 class User(db.Model):
    id = db.Column(db.Integer, primary_key= True)
   user = db.Column(db.String(64), index= True, unique=True)

   def __repr__(self):
     return '<user>'.format(self.username)



実際の例を2つ

どちらも、ユーザ管理の為の「ユーザ登録(Flask-WTF)」「ログイン管理(Flask-login)」を使うアプリになります。 2つとも、ほぼ全く同じ構成になります。

--------------------------------------------------------------------------------


<case1: case="" miguelgrinberg="">


[ Tree ]

この構成は理解しやすいが、完全に機能別に分割されていないので、flask blueprintに途中で構成変更することが推奨されている。この為、パッケージ、モジュールのimportの仕方を理解する1事例として、参考するにとどめる。

projectディレクトリ

--microblog.py
--config.py

project/appディレクトリ

 |-- __init__.py
 |-- forms.py
 |-- models.py
 |-- routes.py
 

--------------------------------------------------------------------------------
<case2:coreymschafer case="">

[ Tree ]
projectディレクトリ

run.py

project/flaskblogディレクトリ
  |-- __init__.py
  |-- forms.py
  |-- models.py  |-- routes.py
  |-- site.db


 (*Database : Flask SQAlchemyを利用している場合)

SQLITE を利用している場合であっても、sqlite::///site.db のような相対パスの記述は出来なくなります。以下のコードで、パスを取得して指定しましょう。

  import OS

  project_dir = os.path.dirname(os.path.abspath(__file__))
  database_file = "sqlite:///{}".format(os.path.join(project_dir, "site.db"))
  app = Flask(__name__)
  app.config["SQLALCHEMY_DATABASE_URI"] = database_file


<miguelgrinberg case>



◇microblog.py (全文)

from app import app, db
from app.models import User, Post

@app.shell_context_processor

def make_shell_context():
    return {'db': db, 'User': User, 'Post': Post}

 ◇config.py (全文)

import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config(object):
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'app.db')

    SQLALCHEMY_TRACK_MODIFICATIONS = False


/app  ♯app directoryの配下

◇ __init__.py(全文)

from flask import Flask

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config
from flask_login import LoginManager

app = Flask(__name__)

app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
login = LoginManager(app)
login.login_view = 'login'

from app import routes, models  #最後にルートをインポートしてくるのがミソ

 ◇forms.py

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, ValidationError
from app.models import User
class RegistrationForm(FlaskForm):

    username = StringField('Username', validators=[DataRequired()])

  …continue, but deleted.

    def validate_username(self, username):
   user = User.query.filter_by(username=username.data).first()
         if user is not None:
       raise ValidationError('Please use a different username.')

   def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
         if user is not None:
             raise ValidationError('Please use a different email address.')

class LoginForm(FlaskForm):

    username = StringField('Username', validators=[DataRequired()])

    …continue, but deleted.

 ◇models.py

from datetime import datetime
from app import db, login
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash

@login.user_loader
     def load_user (id):
         return User.query.get(int(id))

class User (db.Model, UserMixin):
       id = db.Column(db.Integer, primary_key=True)

      …continue, but deleted.

       def __repr__(self):
             return '<user>'.format(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)

class Post (db.Model):
   id = db.Column(db.Integer, primary_key=True)
   body = db.Column(db.String(140))
       …continue, but deleted.

      def __repr__(self):
      return '<post>'.format(self.body)



 Note)
これらのDBにコマンドラインからアクセスしたい場合、
python
>>> from flaskblog import db
>>> from flaskblog.model import User
>>> user = User.query.all()

のようにしてアクセスできる。 forms.py で、htmlから送信されてくるデータをチェックし、validation後にDBに登録する為に、Userテーブルをインポートしているが、同じ記述方法。



◇routes.py

from flask import render_template, url_for,  flash, redirect, request
from flask_login import login_user, current_user, logout_user, login_required
from werkzeug.urls import url_parse
from app import app, db
from app.forms import LoginForm, RegistrationForm
from app.models import User



@app.route('/')

@app.route('/index')

@login_required

def index():

    return render_template('index.html', title='Home', posts=posts)



@app.route('/register', methods=['GET', 'POST'])

def register():

    if current_user.is_authenticated:

        return redirect(url_for('index'))

    form = RegistrationForm()

    if form.validate_on_submit():

        user = User(username=form.username.data, email=form.email.data)

        user.set_password(form.password.data)

        db.session.add(user)

        db.session.commit()

        flash('Congratulations, you are now a registered user!')

        return redirect(url_for('login'))

    return render_template('register.html', title='Register', form=form)



@app.route('/login', methods=['GET', 'POST'])

def login():

    if current_user.is_authenticated:

        return redirect(url_for('index'))

    form = LoginForm()

    if form.validate_on_submit():

        user = User.query.filter_by(username=form.username.data).first()

        if user is None or not user.check_password(form.password.data):

            flash('Invalid username or password')

            return redirect(url_for('login'))

        login_user(user, remember=form.remember_me.data)

        next_page = request.args.get('next')

        if not next_page or url_parse(next_page).netloc != '':

            next_page = url_for('index')

        return redirect(next_page)

    return render_template('login.html', title='Sign In', form=form)



@app.route('/logout')

def logout():

    logout_user()

    return redirect(url_for('index'))




<coreymschafer case>


[ Tree ]

◇run.py


from flaskblog import app   # miguel db もimport #

if __name__ == '__main__': # miguel  記述なし.

    app.run(debug=True)  # miguel  記述なし
 

 *アプリを実行する際の起点となるファイル。
* __init__.pyを含んだディレクトリ(この場合、flaskblogフォルダ)全体を
 1つのパッケージとしてインポートするだけ。



/flaskblog


◇__init__.py

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_login import LoginManager


app = Flask(__name__)

app.config['SECRET_KEY'] = '5791628bb0b13ce0c676dfde280ba245'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'

db = SQLAlchemy(app)

bcrypt = Bcrypt(app)

login_manager = LoginManager(app)

login_manager.login_view = 'login'

login_manager.login_message_category = 'info'


from flaskblog import routes  # miguel  ,modelsもimport



[相違点は、configを別ファイルにしている点と、最後の行でmodelsもimport]


 ◇forms.py

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Length, Email, EqualTo, ValidationErrorfrom flaskblog.models import User

class RegistrationForm(FlaskForm):

    username = StringField('Username',

                           validators=[DataRequired(), Length(min=2, max=20)])

     …continue, but deleted.


    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()

        if user:
            raise ValidationError('That username is taken. ')


    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user:
            raise ValidationError('That email is taken. ')

class LoginForm(FlaskForm):
    email = StringField('Email',
                        validators=[DataRequired(), Email()])

    …continue, but deleted.


[相違点なし。validate_username, _emailも含め、全く同一]



◇models.py

from datetime import datetime
from flaskblog import db, login_manager
from flask_login import UserMixin

@login_manager.user_loader
def load_user (user_id):

    return User.query.get(int(user_id))


class User (db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)

…continue, but deleted.

   def __repr__(self):

        return f"User('{self.username}', '{self.email}', '{self.image_file}')"

# miguel  Password hash をwerkzeug.security を使って、User Classの中で定義


class Post (db.Model):

    id = db.Column(db.Integer, primary_key=True)

    title = db.Column(db.String(100), nullable=False)

…continue, but deleted.



    def __repr__(self):

        return f"Post('{self.title}', '{self.date_posted}')"





◇routes.py
from flask import render_template, url_for, flash, redirect, request
from flask_login import login_user, current_user, logout_user, login_required
from flaskblog import app, db, bcrypt
from flaskblog.forms import LoginForm, RegistrationForm
from flaskblog.models import User, Post


@app.route("/")
@app.route("/home")
def home():
    return render_template('home.html', posts=posts)

@app.route("/register", methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
            return redirect(url_for('home'))

    form = RegistrationForm()    if form.validate_on_submit():
    hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
    user = User(username=form.username.data, email=form.email.data,   password=hashed_password)

db.session.add(user)
     db.session.commit()

 flash('Your account has been created! You are now able to log in', 'success')

        return redirect(url_for('login'))

return render_template('register.html', title='Register', form=form)


@app.route("/login", methods=['GET', 'POST'])
def login():

    if current_user.is_authenticated:

        return redirect(url_for('home'))

    form = LoginForm()

    if form.validate_on_submit():

        user = User.query.filter_by(email=form.email.data).first()

        if user and bcrypt.check_password_hash(user.password, form.password.data):

            login_user(user, remember=form.remember.data)

            next_page = request.args.get('next')

            return redirect(next_page) if next_page else redirect(url_for('home'))

        else:

            flash('Login Unsuccessful. Please check email and password', 'danger')

    return render_template('login.html', title='Login', form=form)



@app.route("/logout")

def logout():

    logout_user()

    return redirect(url_for('home'))



@app.route("/account")

@login_required

def account():

    return render_template('account.html', title='Account')



[相違点ほぼなし。next_pageのremember_me 処理がこちらにない?]





</coreymschafer></post></user></miguelgrinberg></case2:coreymschafer></case1:></user>

0 件のコメント:

コメントを投稿