Browse Source

Initial commit

master
Ryan Reed 6 years ago
commit
4bbbe5fec0
18 changed files with 584 additions and 0 deletions
  1. +2
    -0
      .flaskenv
  2. +111
    -0
      .gitignore
  3. +86
    -0
      README.md
  4. +31
    -0
      app/__init__.py
  5. +5
    -0
      app/main/__init__.py
  6. +12
    -0
      app/main/routes.py
  7. +21
    -0
      app/main/templates/base.html
  8. +11
    -0
      app/main/templates/index.html
  9. +37
    -0
      app/models.py
  10. +1
    -0
      app/static/style.css
  11. +18
    -0
      config.py
  12. +1
    -0
      migrations/README
  13. +45
    -0
      migrations/alembic.ini
  14. +95
    -0
      migrations/env.py
  15. +24
    -0
      migrations/script.py.mako
  16. +46
    -0
      migrations/versions/e1b952dc4b28_initial_user_table.py
  17. +16
    -0
      requirements.txt
  18. +22
    -0
      run.py

+ 2
- 0
.flaskenv View File

@ -0,0 +1,2 @@
FLASK_APP=run.py
FLASK_DEBUG=1

+ 111
- 0
.gitignore View File

@ -0,0 +1,111 @@
# Swap files
*.sw[a-p]
# Database file
*.db
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
venv-*/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/

+ 86
- 0
README.md View File

@ -0,0 +1,86 @@
# Table of Contents
<!-- vim-markdown-toc GFM -->
* [Setting up the environment](#setting-up-the-environment)
* [Install the requirements](#install-the-requirements)
* [Setup the environment variables](#setup-the-environment-variables)
* [Database](#database)
* [Workflow for updating database (new table/field/etc)](#workflow-for-updating-database-new-tablefieldetc)
* [Initial DB Setup for the app](#initial-db-setup-for-the-app)
* [Working with the database](#working-with-the-database)
* [Adding to the database](#adding-to-the-database)
* [Querying the database](#querying-the-database)
* [Clear out the tables](#clear-out-the-tables)
<!-- vim-markdown-toc -->
# Setting up the environment
## Install the requirements
```
pip install -r requirements.txt
```
## Setup the environment variables
The following can be added to a `.env` file in the foot of this directory as well
```bash
APP_ENVIRONMENT="PROD"
SECRET_KEY="myverysecretkey"
DATABASE_URL="sqlite:///app.db"
```
# Database
## Workflow for updating database (new table/field/etc)
1. Export the flask app: `export FLASK_APP=stockpyle.py`
2. Create/modify class for table in `app/models.py`
3. Create migration script: `flask db migrate -m "<class/table> table"`
4. Commit migration script to repo (for upgrading other environments)
5. Make the changes to the database: `flask db upgrade`
## Initial DB Setup for the app
```bash
flask db upgrade
```
## Working with the database
### Adding to the database
```python
u = User(username='john', email='john@example.com')
u.set_password('password')
db.session.add(u)
u = User(username='susan', email='susan@example.com')
u.set_password('password')
db.session.add(u)
db.session.commit()
```
### Querying the database
```python
# get user
u = User.query.get(1)
u
#<User john>
# get all users in reverse alphabetical order
User.query.order_by(User.username.desc()).all()
#[<User susan>, <User john>]
```
### Clear out the tables
```python
users = User.query.all()
for u in users:
db.session.delete(u)
db.session.commit()
```

+ 31
- 0
app/__init__.py View File

@ -0,0 +1,31 @@
from flask import Flask
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from config import Config
db = SQLAlchemy()
migrate = Migrate()
def create_app(config_class=Config):
"""
The application instance to be used when initializing the app
:param config_class: (Default value = Config)
:returns: The flask application
"""
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
migrate.init_app(app, db, render_as_batch=True)
from app.main import bp as bp_main
app.register_blueprint(bp_main)
return app
from app import models

+ 5
- 0
app/main/__init__.py View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('main', __name__, template_folder='templates')
from app.main import routes

+ 12
- 0
app/main/routes.py View File

@ -0,0 +1,12 @@
from flask import current_app, render_template
from app.main import bp
@bp.route("/", methods=["GET", "POST"])
@bp.route("/index", methods=["GET", "POST"])
def index():
return render_template("index.html",
title="Home Page",
environment=current_app.config["ENVIRONMENT"]
)

+ 21
- 0
app/main/templates/base.html View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset=”utf-8”>
<title>[{{ environment }}] {% if title %}{{ title }}{% else %}Home Page{% endif %}</title>
{% block styles %}
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" />
{% endblock %}
{% block scripts %}
{% endblock %}
</head>
<body>
<div class="container">
<h1>[{{ environment }}] Home Page</h1>
{% block app_content %}{% endblock %}
</div>
</body>
</html>

+ 11
- 0
app/main/templates/index.html View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% block app_content %}
<div>
<p>This is the index. To hopefully be expanded upon</p>
</div>
{% endblock %}
{% block styles %}
{{ super() }}{# Preserve existing styles #}
{% endblock %}

+ 37
- 0
app/models.py View File

@ -0,0 +1,37 @@
from datetime import datetime, timedelta
import os
from werkzeug.security import generate_password_hash, check_password_hash
from app import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
role = db.Column(db.String(32), index=True)
def __repr__(self):
return f"<User {self.username}>"
def set_password(self, password):
"""
Set the password_hash field
:param password:
"""
self.password_hash = generate_password_hash(password)
def check_password(self, password):
"""
Check the password against the hash in the database
:param password:
returns: True/False
"""
return check_password_hash(self.password_hash, password)

+ 1
- 0
app/static/style.css View File

@ -0,0 +1 @@
h1 { color: red; }

+ 18
- 0
config.py View File

@ -0,0 +1,18 @@
import os
class Config():
"""
The configuration class. Retrieves needed environment information.
Accessed via app.config or current_app.config
"""
BASEDIR = os.path.abspath(os.path.dirname(__file__))
ENVIRONMENT = os.environ.get("APP_ENVIRONMENT") or "DEV"
SECRET_KEY = os.environ.get("SECRET_KEY") or "you-will-never-guess"
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL") or "sqlite:///{}".format(
os.path.join(BASEDIR, "app.db")
)
SQLALCHEMY_TRACK_MODIFICATIONS = False

+ 1
- 0
migrations/README View File

@ -0,0 +1 @@
Generic single-database configuration.

+ 45
- 0
migrations/alembic.ini View File

@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[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

+ 95
- 0
migrations/env.py View File

@ -0,0 +1,95 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
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, target_metadata=target_metadata, literal_binds=True
)
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.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

+ 24
- 0
migrations/script.py.mako View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
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)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

+ 46
- 0
migrations/versions/e1b952dc4b28_initial_user_table.py View File

@ -0,0 +1,46 @@
"""Initial User table
Revision ID: e1b952dc4b28
Revises:
Create Date: 2019-04-28 14:56:04.793653
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e1b952dc4b28'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=64), nullable=True),
sa.Column('email', sa.String(length=120), nullable=True),
sa.Column('password_hash', sa.String(length=128), nullable=True),
sa.Column('last_seen', sa.DateTime(), nullable=True),
sa.Column('role', sa.String(length=32), nullable=True),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True)
batch_op.create_index(batch_op.f('ix_user_role'), ['role'], unique=False)
batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_user_username'))
batch_op.drop_index(batch_op.f('ix_user_role'))
batch_op.drop_index(batch_op.f('ix_user_email'))
op.drop_table('user')
# ### end Alembic commands ###

+ 16
- 0
requirements.txt View File

@ -0,0 +1,16 @@
alembic==1.0.9
Click==7.0
Flask==1.0.2
Flask-Migrate==2.4.0
Flask-SQLAlchemy==2.4.0
itsdangerous==1.1.0
Jinja2==2.10.1
Mako==1.0.9
MarkupSafe==1.1.1
pkg-resources==0.0.0
python-dateutil==2.8.0
python-dotenv==0.10.1
python-editor==1.0.4
six==1.12.0
SQLAlchemy==1.3.3
Werkzeug==0.15.2

+ 22
- 0
run.py View File

@ -0,0 +1,22 @@
from app import create_app, db
from app.models import User
app = create_app()
@app.shell_context_processor
def make_shell_context():
"""
Make the following easily accessible from 'flask shell'
For instance:
|--$ flask shell
Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
App: spark [production]
Instance: /home/user/app/instance
>>> user = User.query.first()
>>> print(user)
<User johnwhite>
"""
return {"db": db, "User": User}

Loading…
Cancel
Save