Skip to content

Commit

Permalink
initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
David Lord committed Jun 25, 2013
0 parents commit 97d224c
Show file tree
Hide file tree
Showing 13 changed files with 283 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
@@ -0,0 +1,3 @@
*.pyc
app.db
.idea
38 changes: 38 additions & 0 deletions README
@@ -0,0 +1,38 @@
Basic Flask
===========

This sample project shows how I organize a project to use Flask,
Flask-SQLAlchemy, and Alembic together. It demonstrates the application
factory pattern and is organized using blueprints.

Demonstrating migration generation
----------------------------------

I used Alembic to autogenerate a migration and then tweaked the results. If you
want to see autogeneration in action, just delete
`alembic/versions/35b593d48d6a_user_models.py`, then run
`alembic revision --autogenerate -m "user models"`.

Set up the database
-------------------

Change `basic_app.config.SQLALCHEMY_DATABASE_URI` to what you want. By default
it points to a sqlite file called `app.db` in the project folder.

Then run `alembic upgrade head` to set up the database through migrations.

Or, permform the following from a Python shell.

In [1]: from basic_app import create_app, db
In [2]: create_app().app_context().push()
In [3]: db.create_all()

Then run `alembic stamp head` to mark migrations as current.

Origin
------

Created in answer to this question on /r/flask and StackOverflow:

* [\[AF\] Has anyone ever gotten Alembic to work with Flask Blueprints? \[SQLAlchemy\]](http://www.reddit.com/r/flask/comments/1h1k5g/af_has_anyone_ever_gotten_alembic_to_work_with/)
* [Alembic autogenerates empty Flask-SQLAlchemy migrations](http://stackoverflow.com/questions/17201800/alembic-autogenerates-empty-flask-sqlalchemy-migrations)
36 changes: 36 additions & 0 deletions alembic.ini
@@ -0,0 +1,36 @@
[alembic]
script_location = alembic

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
66 changes: 66 additions & 0 deletions alembic/env.py
@@ -0,0 +1,66 @@
from logging.config import fileConfig
import os
import sys
from alembic import context
from sqlalchemy import engine_from_config, pool

sys.path.append(os.getcwd())

from basic_app import db, create_app

config = context.config

fileConfig(config.config_file_name)

app = create_app()
config.set_main_option("sqlalchemy.url", app.config["SQLALCHEMY_DATABASE_URI"])

target_metadata = db.metadata


def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""

url = config.get_main_option("sqlalchemy.url")
context.configure(url=url)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""

engine = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool
)
connection = engine.connect()
context.configure(connection=connection, target_metadata=target_metadata)

try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()

if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
21 changes: 21 additions & 0 deletions alembic/script.py.mako
@@ -0,0 +1,21 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""

from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}


def upgrade():
${upgrades if upgrades else "pass"}


def downgrade():
${downgrades if downgrades else "pass"}
40 changes: 40 additions & 0 deletions alembic/versions/35b593d48d6a_user_models.py
@@ -0,0 +1,40 @@
"""user models
Revision ID: 35b593d48d6a
Revises: None
Create Date: 2013-06-25 10:10:53.866281
"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import expression

# revision identifiers, used by Alembic.
revision = "35b593d48d6a"
down_revision = None


def upgrade():
op.create_table("group",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("name", sa.String(100), nullable=False, unique=True, index=True)
)
op.create_table("user",
sa.Column("id", sa.Integer),
sa.Column("username", sa.String(254), nullable=False, unique=True, index=True),
sa.Column("password", sa.String(60)),
sa.Column("name", sa.String(254), nullable=False, server_default=""),
sa.Column("email", sa.String(254), nullable=False, server_default=""),
sa.Column("active", sa.Boolean, nullable=False, server_default=expression.true()),
sa.PrimaryKeyConstraint("id")
)
op.create_table("user_group",
sa.Column("user_id", sa.Integer, sa.ForeignKey("user.id"), primary_key=True),
sa.Column("group_id", sa.Integer, sa.ForeignKey("group.id"), primary_key=True),
)


def downgrade():
op.drop_table("user_group")
op.drop_table("user")
op.drop_table("group")
33 changes: 33 additions & 0 deletions basic_app/__init__.py
@@ -0,0 +1,33 @@
from flask import Flask
from flask.ext.script import Manager
from flask.ext.sqlalchemy import SQLAlchemy
from basic_app import config

db = SQLAlchemy()


def load_models():
from basic_app.auth import models

load_models()


def init_extensions(app):
db.init_app(app)


def init_views(app):
from basic_app import auth

app.register_blueprint(auth.bp, url_prefix="/auth")


def create_app(config=config):
app = Flask(__name__)
app.config.from_object(config)

init_extensions(app)

return app

manager = Manager(create_app)
5 changes: 5 additions & 0 deletions basic_app/auth/__init__.py
@@ -0,0 +1,5 @@
from flask import Blueprint

bp = Blueprint("auth", __name__, template_folder="templates")

from basic_app.auth import views
23 changes: 23 additions & 0 deletions basic_app/auth/models.py
@@ -0,0 +1,23 @@
from sqlalchemy.sql import expression
from basic_app import db


class Group(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True, index=True)


class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(254), nullable=False, unique=True, index=True)
password = db.Column(db.String(60))
name = db.Column(db.String(254), nullable=False, default="", server_default="")
email = db.Column(db.String(254), nullable=False, default="", server_default="")
active = db.Column(db.Boolean, nullable=False, default=False, server_default=expression.true())

groups = db.relationship(Group, lambda: user_group, backref=db.backref("users"))

user_group = db.Table("user_group",
db.Column("user_id", db.Integer, db.ForeignKey(User.id), primary_key=True),
db.Column("group_id", db.Integer, db.ForeignKey(Group.id), primary_key=True)
)
8 changes: 8 additions & 0 deletions basic_app/auth/views.py
@@ -0,0 +1,8 @@
from basic_app.auth import bp
from basic_app.auth.models import User


@bp.route("/<username>")
def hello(username):
u = User.query.filter_by(username=username).first_or_404()
return "Hello, {}!".format(u.username)
4 changes: 4 additions & 0 deletions basic_app/config.py
@@ -0,0 +1,4 @@
DEBUG = True
SECRET_KEY = "dev"

SQLALCHEMY_DATABASE_URI = "sqlite:///app.db"
3 changes: 3 additions & 0 deletions manage.py
@@ -0,0 +1,3 @@
from basic_app import manager

manager.run()
3 changes: 3 additions & 0 deletions requirements.txt
@@ -0,0 +1,3 @@
Flask-Script
Flask-SQLAlchemy
alembic

0 comments on commit 97d224c

Please sign in to comment.