finish api basic setup
This commit is contained in:
60
api/__init__.py
Normal file
60
api/__init__.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from flask import Flask, Blueprint
|
||||
from . import commands, login
|
||||
from .settings import ProdConfig, Config
|
||||
from .extensions import db, migrate, jwt
|
||||
from .exceptions import ApiException
|
||||
|
||||
|
||||
def create_app(config: Config = ProdConfig) -> Flask:
|
||||
"""An application factory, as explained here:
|
||||
http://flask.pocoo.org/docs/patterns/appfactories/.
|
||||
|
||||
:param config_object: The configuration object to use.
|
||||
"""
|
||||
app = Flask(__name__.split('.')[0])
|
||||
app.url_map.strict_slashes = False
|
||||
app.config.from_object(config)
|
||||
register_extensions(app)
|
||||
register_blueprints(app)
|
||||
register_errorhandlers(app)
|
||||
register_shellcontext(app)
|
||||
register_commands(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def register_extensions(app: Flask):
|
||||
"""Register Flask extensions."""
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
jwt.init_app(app)
|
||||
|
||||
|
||||
def register_blueprints(app: Flask):
|
||||
"""Register Flask blueprints."""
|
||||
pass
|
||||
|
||||
|
||||
def register_errorhandlers(app: Flask):
|
||||
def errorHandler(error: ApiException):
|
||||
return error.to_response()
|
||||
|
||||
app.errorhandler(ApiException)(errorHandler)
|
||||
pass
|
||||
|
||||
|
||||
def register_shellcontext(app: Flask):
|
||||
"""Register shell context objects."""
|
||||
def shell_context():
|
||||
"""Shell context objects."""
|
||||
return {
|
||||
'db': db,
|
||||
}
|
||||
|
||||
app.shell_context_processor(shell_context)
|
||||
|
||||
|
||||
def register_commands(app: Flask):
|
||||
"""Register Click commands."""
|
||||
app.cli.add_command(commands.clean)
|
||||
app.cli.add_command(commands.urls)
|
||||
81
api/commands.py
Normal file
81
api/commands.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import os
|
||||
import click
|
||||
|
||||
from flask import current_app
|
||||
from flask.cli import with_appcontext
|
||||
from werkzeug.exceptions import MethodNotAllowed, NotFound
|
||||
|
||||
@click.command()
|
||||
def clean():
|
||||
"""Remove *.pyc and *.pyo files recursively starting at current directory.
|
||||
|
||||
Borrowed from Flask-Script, converted to use Click.
|
||||
"""
|
||||
for dirpath, _, filenames in os.walk('.'):
|
||||
for filename in filenames:
|
||||
if filename.endswith('.pyc') or filename.endswith('.pyo'):
|
||||
full_pathname = os.path.join(dirpath, filename)
|
||||
click.echo('Removing {}'.format(full_pathname))
|
||||
os.remove(full_pathname)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--url', default=None,
|
||||
help='Url to test (ex. /static/image.png)')
|
||||
@click.option('--order', default='rule',
|
||||
help='Property on Rule to order by (default: rule)')
|
||||
@with_appcontext
|
||||
def urls(url, order):
|
||||
"""Display all of the url matching routes for the project.
|
||||
|
||||
Borrowed from Flask-Script, converted to use Click.
|
||||
"""
|
||||
rows = []
|
||||
column_headers = ('Rule', 'Endpoint', 'Arguments')
|
||||
|
||||
if url:
|
||||
try:
|
||||
rule, arguments = (
|
||||
current_app.url_map.bind('localhost')
|
||||
.match(url, return_rule=True))
|
||||
rows.append((rule.rule, rule.endpoint, arguments))
|
||||
column_length = 3
|
||||
except (NotFound, MethodNotAllowed) as e:
|
||||
rows.append(('<{}>'.format(e), None, None))
|
||||
column_length = 1
|
||||
else:
|
||||
rules = sorted(
|
||||
current_app.url_map.iter_rules(),
|
||||
key=lambda rule: getattr(rule, order))
|
||||
for rule in rules:
|
||||
rows.append((rule.rule, rule.endpoint, None))
|
||||
column_length = 2
|
||||
|
||||
str_template = ''
|
||||
table_width = 0
|
||||
|
||||
if column_length >= 1:
|
||||
max_rule_length = max(len(r[0]) for r in rows)
|
||||
max_rule_length = max_rule_length if max_rule_length > 4 else 4
|
||||
str_template += '{:' + str(max_rule_length) + '}'
|
||||
table_width += max_rule_length
|
||||
|
||||
if column_length >= 2:
|
||||
max_endpoint_length = max(len(str(r[1])) for r in rows)
|
||||
max_endpoint_length = (
|
||||
max_endpoint_length if max_endpoint_length > 8 else 8)
|
||||
str_template += ' {:' + str(max_endpoint_length) + '}'
|
||||
table_width += 2 + max_endpoint_length
|
||||
|
||||
if column_length >= 3:
|
||||
max_arguments_length = max(len(str(r[2])) for r in rows)
|
||||
max_arguments_length = (
|
||||
max_arguments_length if max_arguments_length > 9 else 9)
|
||||
str_template += ' {:' + str(max_arguments_length) + '}'
|
||||
table_width += 2 + max_arguments_length
|
||||
|
||||
click.echo(str_template.format(*column_headers[:column_length]))
|
||||
click.echo('-' * table_width)
|
||||
|
||||
for row in rows:
|
||||
click.echo(str_template.format(*row[:column_length]))
|
||||
2
api/constants.py
Normal file
2
api/constants.py
Normal file
@@ -0,0 +1,2 @@
|
||||
API_PASS='API_PASS'
|
||||
API_USER='super'
|
||||
43
api/database.py
Normal file
43
api/database.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Database module, including the SQLAlchemy database object and DB-related utilities."""
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from .extensions import db
|
||||
|
||||
# Alias common SQLAlchemy names
|
||||
Column = db.Column
|
||||
relationship = relationship
|
||||
Model = db.Model
|
||||
|
||||
# From Mike Bayer's "Building the app" talk
|
||||
# https://speakerdeck.com/zzzeek/building-the-app
|
||||
class SurrogatePK(object):
|
||||
"""A mixin that adds a surrogate integer 'primary key' column named ``id`` \
|
||||
to any declarative-mapped class.
|
||||
"""
|
||||
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
@classmethod
|
||||
def get_by_id(cls, record_id):
|
||||
"""Get record by ID."""
|
||||
if any(
|
||||
(isinstance(record_id, (str, bytes)) and record_id.isdigit(),
|
||||
isinstance(record_id, (int, float))),
|
||||
):
|
||||
return cls.query.get(int(record_id))
|
||||
|
||||
|
||||
def reference_col(tablename, nullable=False, pk_name='id', **kwargs):
|
||||
"""Column that adds primary key foreign key reference.
|
||||
|
||||
Usage: ::
|
||||
|
||||
category_id = reference_col('category')
|
||||
category = relationship('Category', backref='categories')
|
||||
"""
|
||||
return db.Column(
|
||||
db.ForeignKey('{0}.{1}'.format(tablename, pk_name)),
|
||||
nullable=nullable, **kwargs)
|
||||
22
api/exceptions.py
Normal file
22
api/exceptions.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from flask import jsonify
|
||||
|
||||
class ApiException(Exception):
|
||||
status_code = 500
|
||||
|
||||
def __init__(self, status_code: int, message) -> None:
|
||||
super().__init__()
|
||||
self.status_code = status_code
|
||||
self.message = message
|
||||
|
||||
def to_response(self):
|
||||
rv = jsonify(self.message)
|
||||
rv.status_code = self.status_code
|
||||
return rv
|
||||
|
||||
class NotFoundException(ApiException):
|
||||
def __init__(self, message) -> None:
|
||||
super().__init__(404, message)
|
||||
|
||||
class BadRequestException(ApiException):
|
||||
def __init__(self, message) -> None:
|
||||
super().__init__(400, message)
|
||||
34
api/extensions.py
Normal file
34
api/extensions.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from flask_jwt_extended import JWTManager
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy, Model
|
||||
|
||||
class CRUDMixin(Model):
|
||||
"""Mixin that adds convenience methods for CRUD (create, read, update, delete) operations."""
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
"""Create a new record and save it the database."""
|
||||
instance = cls(**kwargs)
|
||||
return instance.save()
|
||||
|
||||
def update(self, commit=True, **kwargs):
|
||||
"""Update specific fields of a record."""
|
||||
for attr, value in kwargs.items():
|
||||
setattr(self, attr, value)
|
||||
return commit and self.save() or self
|
||||
|
||||
def save(self, commit=True):
|
||||
"""Save the record."""
|
||||
db.session.add(self)
|
||||
if commit:
|
||||
db.session.commit()
|
||||
return self
|
||||
|
||||
def delete(self, commit=True):
|
||||
"""Remove the record from the database."""
|
||||
db.session.delete(self)
|
||||
return commit and db.session.commit()
|
||||
|
||||
db = SQLAlchemy(model_class=CRUDMixin)
|
||||
migrate = Migrate()
|
||||
jwt = JWTManager()
|
||||
52
api/settings.py
Normal file
52
api/settings.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Application configuration."""
|
||||
import os
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""Base configuration."""
|
||||
|
||||
SECRET_KEY = os.environ['SECRET_KEY']
|
||||
API_PASS = os.environ['API_PASS']
|
||||
APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir))
|
||||
DEBUG_TB_INTERCEPT_REDIRECTS = False
|
||||
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc.
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
JWT_AUTH_USERNAME_KEY = 'email'
|
||||
JWT_AUTH_HEADER_PREFIX = 'Token'
|
||||
|
||||
|
||||
class ProdConfig(Config):
|
||||
"""Production configuration."""
|
||||
|
||||
ENV = 'prod'
|
||||
DEBUG = False
|
||||
DB_NAME = os.environ.get('DB_NAME', 'prod.db')
|
||||
DB_PATH = os.environ.get('DB_PATH', os.path.join(Config.PROJECT_ROOT, DB_NAME))
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL',
|
||||
f'sqlite:///{DB_PATH}')
|
||||
|
||||
|
||||
class DevConfig(Config):
|
||||
"""Development configuration."""
|
||||
|
||||
ENV = 'dev'
|
||||
DEBUG = True
|
||||
DB_NAME = 'dev.db'
|
||||
# Put the db file in project root
|
||||
DB_PATH = os.path.join(Config.PROJECT_ROOT, DB_NAME)
|
||||
SQLALCHEMY_DATABASE_URI = f'sqlite:///{DB_PATH}'
|
||||
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc.
|
||||
JWT_ACCESS_TOKEN_EXPIRES = timedelta(60 * 60)
|
||||
|
||||
|
||||
class TestConfig(Config):
|
||||
"""Test configuration."""
|
||||
|
||||
TESTING = True
|
||||
DEBUG = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite://'
|
||||
# For faster tests; needs at least 4 to avoid "ValueError: Invalid rounds"
|
||||
BCRYPT_LOG_ROUNDS = 4
|
||||
Reference in New Issue
Block a user