Initial code added for models and services not functional yet. Bootstrap and migrations added for db.

This commit is contained in:
Nicholas Staples
2016-01-07 17:31:17 +00:00
parent 44b0a5f07d
commit a327702ad0
20 changed files with 341 additions and 2 deletions

View File

@@ -2,11 +2,15 @@ import os
from flask._compat import string_types from flask._compat import string_types
from flask import Flask, _request_ctx_stack from flask import Flask, _request_ctx_stack
from flask.ext.sqlalchemy import SQLAlchemy
from werkzeug.local import LocalProxy from werkzeug.local import LocalProxy
from config import configs from config import configs
from utils import logging from utils import logging
db = SQLAlchemy()
api_user = LocalProxy(lambda: _request_ctx_stack.top.api_user) api_user = LocalProxy(lambda: _request_ctx_stack.top.api_user)
@@ -16,6 +20,7 @@ def create_app(config_name):
application.config['NOTIFY_API_ENVIRONMENT'] = config_name application.config['NOTIFY_API_ENVIRONMENT'] = config_name
application.config.from_object(configs[config_name]) application.config.from_object(configs[config_name])
db.init_app(application)
init_app(application) init_app(application)
logging.init_app(application) logging.init_app(application)
@@ -26,6 +31,8 @@ def create_app(config_name):
from .status import status as status_blueprint from .status import status as status_blueprint
application.register_blueprint(status_blueprint) application.register_blueprint(status_blueprint)
from app import models
return application return application

0
app/main/dao/__init__.py Normal file
View File

View File

@@ -0,0 +1,28 @@
from datetime import datetime
from sqlalchemy.orm import load_only
from app import db
from app.models import Service
def create_new_service(service_name,
user,
limit=1000,
active=False,
restricted=True):
service = Service(name=service_name,
created_at=datetime.now(),
limit=limit,
active=active,
restricted=restricted)
db.session.add(service)
service.users.append(user)
db.session.commit()
return service.id
def get_services(user, service_id=None):
if service_id:
return Service.query.filter_by(user=user, service_id=service_id).one()
return Service.query.filter_by(user=user).all()

20
app/main/dao/users_dao.py Normal file
View File

@@ -0,0 +1,20 @@
from datetime import datetime
from sqlalchemy.orm import load_only
from app import db
from app.models import User
def create_new_user(email_address):
user = User(email_address=email_address,
created_at=datetime.now())
db.session.add(user)
db.session.commit()
return user.id
def get_users(user_id=None):
if user_id:
return User.query.filter_by(user_id=user_id).one()
return User.query.filter_by().all()

View File

@@ -2,11 +2,16 @@ from flask import jsonify
from .. import main from .. import main
# TODO need for health check url
# TODO remove
@main.route('/', methods=['GET']) @main.route('/', methods=['GET'])
def get_index(): def get_index():
return jsonify(result="hello world"), 200 return jsonify(result="hello world"), 200
# TODO remove
@main.route('/', methods=['POST']) @main.route('/', methods=['POST'])
def post_index(): def post_index():
return jsonify(result="hello world"), 200 return jsonify(result="hello world"), 200

View File

@@ -0,0 +1,38 @@
from flask import jsonify
from app.main.dao.services_dao import (create_new_service, get_services)
from app.main.dao.users_dao import (get_users)
from .. import main
# TODO auth to be added.
@main.route('/service', methods=['POST'])
def create_service():
return jsonify(result="created"), 201
# TODO auth to be added
@main.route('/service/<int:service_id>', method=['PUT'])
def update_service(service_id):
return jsonify(result="updated")
# TODO auth to be added.
# Should be restricted by user, user id
# is pulled from the token
@main.route('/service/<int:service_id>', method=['GET'])
@main.route('/service', methods=['GET'])
def get_service(service_id=None):
services = get_services
return jsonify(
data=services
)
# TODO auth to be added
# auth should be allow for admin tokens only
@main.route('/user/<int:user_id>/service', method=['GET'])
@main.route('/user/<int:user_id>/service/<int:service_id>', method=['GET'])
def get_service_by_user_id(user_id, service_id=None):
user = get_users(user_id=user_id)
services = get_services(user, service_id=service_id)
return jsonify(data=services)

43
app/models.py Normal file
View File

@@ -0,0 +1,43 @@
from . import db
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email_address = db.Column(db.String(255), nullable=False, index=True, unique=True)
created_at = db.Column(db.DateTime, index=False, unique=False, nullable=False)
updated_at = db.Column(db.DateTime, index=False, unique=False, nullable=True)
user_to_service = db.Table(
'user_to_service',
db.Model.metadata,
db.Column('user_id', db.Integer, db.ForeignKey('users.id')),
db.Column('service_id', db.Integer, db.ForeignKey('services.id'))
)
class Service(db.Model):
__tablename__ = 'services'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
created_at = db.Column(db.DateTime, index=False, unique=False, nullable=False)
active = db.Column(db.Boolean, index=False, unique=False, nullable=False)
limit = db.Column(db.BigInteger, index=False, unique=False, nullable=False)
users = db.relationship('User', secondary=user_to_service, backref=db.backref('user_to_service', lazy='dynamic'))
restricted = db.Column(db.Boolean, index=False, unique=False, nullable=False)
def serialize(self):
serialized = {
'id': self.id,
'name': self.name,
'createdAt': self.created_at.strftime(DATETIME_FORMAT),
'active': self.active,
'restricted': self.restricted,
'limit': self.limit,
'user': self.users.serialize()
}
return filter_null_value_fields(serialized)

View File

@@ -5,14 +5,18 @@ from __future__ import print_function
import os import os
from flask.ext.script import Manager, Server from flask.ext.script import Manager, Server
from flask.ext.migrate import Migrate, MigrateCommand
from app import create_app from app import create_app, db
application = create_app(os.getenv('NOTIFY_API_ENVIRONMENT') or 'development') application = create_app(os.getenv('NOTIFY_API_ENVIRONMENT') or 'development')
manager = Manager(application) manager = Manager(application)
port = int(os.environ.get('PORT', 6011)) port = int(os.environ.get('PORT', 6011))
manager.add_command("runserver", Server(host='0.0.0.0', port=port)) manager.add_command("runserver", Server(host='0.0.0.0', port=port))
migrate = Migrate(application, db)
manager.add_command('db', MigrateCommand)
@manager.command @manager.command
def list_routes(): def list_routes():

View File

@@ -4,6 +4,9 @@ class Config(object):
NOTIFY_LOG_LEVEL = 'DEBUG' NOTIFY_LOG_LEVEL = 'DEBUG'
NOTIFY_APP_NAME = 'api' NOTIFY_APP_NAME = 'api'
NOTIFY_LOG_PATH = '/var/log/notify/application.log' NOTIFY_LOG_PATH = '/var/log/notify/application.log'
SQLALCHEMY_COMMIT_ON_TEARDOWN = False
SQLALCHEMY_RECORD_QUERIES = True
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/notification_api'
class Development(Config): class Development(Config):
@@ -12,11 +15,13 @@ class Development(Config):
class Test(Config): class Test(Config):
DEBUG = True DEBUG = True
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/notification_api_test'
class Live(Config): class Live(Config):
pass pass
configs = { configs = {
'development': Development, 'development': Development,
'test': Test, 'test': Test,

1
migrations/README Normal file
View File

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

45
migrations/alembic.ini Normal file
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 =
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

73
migrations/env.py Normal file
View File

@@ -0,0 +1,73 @@
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
# 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)
# 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)
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()

22
migrations/script.py.mako Normal file
View File

@@ -0,0 +1,22 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -1,5 +1,10 @@
Flask==0.10.1 Flask==0.10.1
Flask-Script==2.0.5 Flask-Script==2.0.5
Flask-Migrate==1.3.1
Flask-SQLAlchemy==2.0
psycopg2==2.6.1
SQLAlchemy==1.0.5
SQLAlchemy-Utils==0.30.5
PyJWT==1.4.0 PyJWT==1.4.0
git+https://github.com/alphagov/notifications-python-client.git@0.1.5#egg=notifications-python-client==0.1.5 git+https://github.com/alphagov/notifications-python-client.git@0.1.5#egg=notifications-python-client==0.1.5

View File

@@ -28,3 +28,10 @@ fi
# Install Python development dependencies # Install Python development dependencies
pip3 install -r requirements_for_test.txt pip3 install -r requirements_for_test.txt
# Create Postgres databases
createdb notification_api
createdb test_notification_api
# Upgrade databases
python application.py db upgrade

View File

@@ -29,5 +29,6 @@ display_result $? 1 "Code style check"
#py.test --cov=app tests/ #py.test --cov=app tests/
#display_result $? 2 "Code coverage" #display_result $? 2 "Code coverage"
py.test -v py.test -v
display_result $? 3 "Unit tests" display_result $? 3 "Unit tests"

View File

View File

@@ -0,0 +1,8 @@
def test_create_service(notify_api):
pass
def test_get_all_services(notify_api):
pass

View File

View File

@@ -1,7 +1,13 @@
import pytest import pytest
import mock import mock
from app import create_app import os
from config import configs from config import configs
from alembic.command import upgrade
from alembic.config import Config
from flask.ext.migrate import Migrate, MigrateCommand
from flask.ext.script import Manager
from app import create_app, db
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
@@ -17,6 +23,27 @@ def notify_api(request):
return app return app
@pytest.fixture(scope='session')
def notify_db(notify_api, request):
Migrate(notify_api, db)
Manager(db, MigrateCommand)
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
ALEMBIC_CONFIG = os.path.join(BASE_DIR, 'migrations')
config = Config(ALEMBIC_CONFIG + '/alembic.ini')
config.set_main_option("script_location", ALEMBIC_CONFIG)
with notify_api.app_context():
upgrade(config, 'head')
def teardown():
db.session.remove()
db.drop_all()
db.engine.execute("drop table alembic_version")
db.get_engine(notify_api).dispose()
request.addfinalizer(teardown)
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def notify_config(notify_api): def notify_config(notify_api):
notify_api.config['NOTIFY_API_ENVIRONMENT'] = 'test' notify_api.config['NOTIFY_API_ENVIRONMENT'] = 'test'