Haha, j’ai eu exactement le même problème que toi, utilisant Flask pour un projet. C’est un peu le « piège » des micro-frameworks : tu as une marge intéressante de libertés mais c’est facile de se tirer une balle dans le pied…
Bref, voici ma solution (qui n’est peut-être pas la meilleure) :
Renomme ton __init__.py
en flask.py
. Laisse un fichier __init__.py
vide.
Dans ce fichier flask.py
, pas besoin d’importer les modèles. Voici le mien :
from ._app import app
from .api import api
from .backend import backend
from .email_viewer import email_viewer
from .landing import landing
from .payment import payment
from .static_content import static_content
app.register_blueprint(api, url_prefix="/api")
app.register_blueprint(backend, url_prefix="/")
app.register_blueprint(email_viewer, url_prefix="/")
app.register_blueprint(landing, url_prefix="/")
app.register_blueprint(payment, url_prefix="/")
app.register_blueprint(static_content, url_prefix="/")
Comme tu peux le voir, ici j’enregistre mes « blueprint ». De surcroit, c’est pas super élégant, mais ça fonctionne bien.
Dans un fichier _app.py
, j’ai paramétré mon application Flask entre autre choses :
from flask import Flask, render_template
from .conf import settings
app = Flask(__name__)
app.secret_key = settings.SECRET_KEY
app.config["SERVER_NAME"] = settings.SERVER_NAME
app.config["SESSION_COOKIE_DOMAIN"] = settings.SERVER_NAME
@app.template_filter()
def get_settings(name):
return getattr(settings, name)
def render_mail_template(template: str, browser_link: str, **kwargs):
"""Render a mail template."""
return render_template(template, browser_link=browser_link, **kwargs)
Tu peux voir que mes blueprints correspondent à des modules python. Voici, à titre d’exemple, celui du backend
:
import enum
from flask import (
Blueprint,
abort,
flash,
redirect,
render_template,
request,
url_for,
)
from .conf import settings
from .models import db
from .models.user import User, UserForgottenPasswordToken
backend = Blueprint("backend", __name__)
class Error(enum.Enum):
success = 0
passwords_do_not_match = 1
password_too_short = 2
ERROR_DESCRIPTION = {
Error.success: "success",
Error.passwords_do_not_match: "passwords do not match",
Error.password_too_short: "password too short",
}
@backend.route("/reset_password")
def reset_password():
email = request.args.get("email")
token = request.args.get("token")
if token is None or email is None:
abort(400)
user_token = UserForgottenPasswordToken.query.filter(
User.email == email, UserForgottenPasswordToken.token == token
).first()
if user_token is None:
abort(400)
return render_template(
"backend/reset_password.html", email=email, token=token
)
@backend.route("/password_reset")
def password_reset():
return render_template("backend/reset_password_after.html")
@backend.route("/reset_password/post", methods=["POST"])
def reset_password_post():
email = request.form.get("email")
token = request.form.get("token")
password = request.form.get("password")
password_confirm = request.form.get("password_confirm")
if (
email is None
or token is None
or password is None
or password_confirm is None
):
abort(400)
user = User.query.filter_by(email=email).first()
if user is None:
abort(400)
user_token = UserForgottenPasswordToken.query.filter_by(
user=user, token=token
).first()
if user_token is None:
abort(400)
if password != password_confirm:
flash(
f"Error: {ERROR_DESCRIPTION[Error.passwords_do_not_match]}",
"error",
)
return redirect(
url_for("backend.reset_password", email=email, token=token)
)
if len(password) < 6:
flash(f"Error: {ERROR_DESCRIPTION[Error.password_too_short]}", "error")
return redirect(
url_for("backend.reset_password", email=email, token=token)
)
user.hash_password(password)
db.session.delete(user_token)
db.session.commit()
flash("Your password has been reset.", "success")
return redirect(url_for("backend.password_reset"))
Tu peux notamment remarquer deux choses :
from .models import db
from .models.user import User, UserForgottenPasswordToken
Eh oui ! J’ai un module models
qui exporte le symbole db
qui est le « lien » vers ma base de données SQLAlchemy, et plusieurs sous-modules pour regrouper mes modèles par fichiers / thêmes (pour ne pas m’y perdre).
models/__init__.py
:
import importlib
import os
from pathlib import Path
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from .._app import app
from ..conf import settings
app.config["SQLALCHEMY_DATABASE_URI"] = settings.SQLALCHEMY_DATABASE_URI
db = SQLAlchemy(app)
path = Path(os.path.dirname(__file__))
for x in path.iterdir():
if x.is_file():
module = str(x.relative_to(os.path.dirname(__file__))).partition(".")[
0
]
importlib.import_module(
f".models.{module}", package="flexomatic_backend"
)
migrate = Migrate(app, db)
Ici, c’est une sorte de « hack » pour importer automatiquement tous les sous-modules (et donc pour enregistrer les modèles). Ça me permet d’éviter d’avoir à faire from .user import User, UserForgottenPassword
etc. À toi de voir.
Enfin, une partie du fichier user.py
:
import datetime
import enum
from itsdangerous import (
BadSignature,
SignatureExpired,
TimedJSONWebSignatureSerializer as Serializer,
)
from passlib.apps import custom_app_context as pwd_context
from .._app import app
from . import db
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(100), index=True, unique=True)
phone_number = db.Column(db.String(20), index=True, unique=True)
password_hash = db.Column(db.String(255))
is_activated = db.Column(db.Boolean, default=False)
is_banned = db.Column(db.Boolean, default=False)
validation_token = db.relationship(
"UserValidationToken", back_populates="user"
)
stripe_user = db.relationship("StripeUser", back_populates="user")
stripe_source = db.relationship("StripeSource", back_populates="user")
stripe_charges = db.relationship("StripeCharge", back_populates="user")
sponsorship_code = db.relationship(
"UserSponsorshipCode", back_populates="user"
)
sponsored_party = db.relationship(
"UserSponsoredParties",
back_populates="user",
foreign_keys="UserSponsoredParties.user_id",
)
sponsored_parties = db.relationship(
"UserSponsoredParties",
back_populates="sponsor",
foreign_keys="UserSponsoredParties.sponsor_id",
)
connection_logs = db.relationship(
"UserConnectionLog", back_populates="user"
)
forgotten_password_token = db.relationship(
"UserForgottenPasswordToken", back_populates="user"
)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(
db.DateTime,
default=datetime.datetime.utcnow,
onupdate=datetime.datetime.utcnow,
)
def set_email_address(self, email):
self.email = email.lower()
def hash_password(self, password):
self.password_hash = pwd_context.encrypt(password)
def verify_password(self, password):
return pwd_context.verify(password, self.password_hash)
def generate_auth_token(self, expiration=600):
s = Serializer(app.config["SECRET_KEY"], expires_in=expiration)
return s.dumps({"id": self.id})
@staticmethod
def verify_auth_token(token):
s = Serializer(app.config["SECRET_KEY"])
try:
data = s.loads(token)
except (BadSignature, SignatureExpired):
return None
else:
return User.query.get(data["id"])
class UserForgottenPasswordToken(db.Model):
__tablename__ = "user_forgotten_password_tokens"
user_id = db.Column(
db.Integer, db.ForeignKey("users.id"), primary_key=True
)
user = db.relationship("User", back_populates="forgotten_password_token")
token = db.Column(db.String(32))
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(
db.DateTime,
default=datetime.datetime.utcnow,
onupdate=datetime.datetime.utcnow,
)
class UserSponsorshipCode(db.Model):
__tablename__ = "user_sponsorship_codes"
user_id = db.Column(
db.Integer, db.ForeignKey("users.id"), primary_key=True
)
user = db.relationship("User", back_populates="sponsorship_code")
code = db.Column(db.String(8), unique=True)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(
db.DateTime,
default=datetime.datetime.utcnow,
onupdate=datetime.datetime.utcnow,
)
class UserConnectionLog(db.Model):
__tablename__ = "user_connection_logs"
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
user = db.relationship("User", back_populates="connection_logs")
uuid = db.Column(db.String(36))
version = db.Column(db.String(10), default="")
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(
db.DateTime,
default=datetime.datetime.utcnow,
onupdate=datetime.datetime.utcnow,
)
class UserSponsoredParties(db.Model):
"""UserSponsoredParties model.
This model describes a sponsorship between two parties.
The user is the one being sponsored, and sponsor is the sponsor.
"""
__tablename__ = "user_sponsored_parties"
user_id = db.Column(
db.Integer, db.ForeignKey("users.id"), primary_key=True
)
user = db.relationship(
"User", back_populates="sponsored_party", foreign_keys=[user_id]
)
sponsor_id = db.Column(db.Integer, db.ForeignKey("users.id"))
sponsor = db.relationship(
"User", back_populates="sponsored_parties", foreign_keys=[sponsor_id]
)
(J’ai enlevé quelques trucs, parce que, tu comprends, l’Open Source c’est bien, mais pas trop ! )
Voilà, j’ai fait un peu le tour, ça fait beaucoup de choses mais j’espère t’avoir aidé au final.
Ah et dernier truc ! N’oublie pas de mettre sensor_app.flask
dans ta variable d’environnement FLASK_APP
en ce qui te concerne.
À+