@ -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} |