@ -0,0 +1,2 @@ | |||
FLASK_APP=run.py | |||
FLASK_DEBUG=1 |
@ -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/ |
@ -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() | |||
``` |
@ -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 |
@ -0,0 +1,5 @@ | |||
from flask import Blueprint | |||
bp = Blueprint('main', __name__, template_folder='templates') | |||
from app.main import routes |
@ -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"] | |||
) |
@ -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> |
@ -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 %} |
@ -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) |
@ -0,0 +1 @@ | |||
h1 { color: red; } |
@ -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 |
@ -0,0 +1 @@ | |||
Generic single-database configuration. |
@ -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 |
@ -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() |
@ -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"} |
@ -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 ### |
@ -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 |
@ -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} |