Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbukachi committed May 24, 2019
0 parents commit b1a3975
Show file tree
Hide file tree
Showing 33 changed files with 460 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea
venv
*.log
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## api

### Project Setup

1. `pip install -r requirements.txt`
2. `chmod +x unit-tests.sh`
3. `./unit-tests.sh`
46 changes: 46 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import click
from werkzeug.middleware.proxy_fix import ProxyFix

from .factory import Factory


def create_app(environment='development'):
f = Factory(environment)
f.set_flask()
f.set_db()
f.set_migration()
f.set_api()

# from models import Example

app = f.flask

from .views import sample_page

app.register_blueprint(sample_page, url_prefix='/views')

if app.config['TESTING']: # pragma: no cover
# Setup app for testing
@app.before_first_request
def initialize_app():
pass

@app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers',
'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods',
'GET,PUT,POST,DELETE')

return response

app.wsgi_app = ProxyFix(app.wsgi_app)

@app.cli.command()
@click.argument('command')
def setup(command):
pass

return app

34 changes: 34 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os


class Config:
ERROR_404_HELP = False

SECRET_KEY = os.getenv('APP_SECRET', 'secret key')

SQLALCHEMY_DATABASE_URI = 'sqlite:///tutorial.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False

DOC_USERNAME = 'api'
DOC_PASSWORD = 'password'


class DevConfig(Config):
DEBUG = True


class TestConfig(Config):
SQLALCHEMY_DATABASE_URI = 'sqlite://'
TESTING = True
DEBUG = True


class ProdConfig(Config):
DEBUG = False


config = {
'development': DevConfig,
'testing': TestConfig,
'production': ProdConfig
}
48 changes: 48 additions & 0 deletions app/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging
import os
import sys
from logging.handlers import RotatingFileHandler

from flask import Flask

from .config import config


class Factory:

def __init__(self, environment='development'):
self._environment = os.environ.get('APP_ENVIRONMENT', environment)
self.flask = None

@property
def environment(self):
return self._environment

@environment.setter
def environment(self, env):
self._environment = env

def set_flask(self, **kwargs):
self.flask = Flask(__name__, **kwargs)
self.flask.config.from_object(config[self._environment])
# setup logging
file_handler = RotatingFileHandler('api.log', maxBytes=10000, backupCount=1)
file_handler.setLevel(logging.INFO)
self.flask.logger.addHandler(file_handler)
stdout = logging.StreamHandler(sys.stdout)
stdout.setLevel(logging.DEBUG)
self.flask.logger.addHandler(stdout)

return self.flask

def set_db(self):
from .models.database import db
db.init_app(self.flask)

def set_migration(self):
from .models.database import db, migrate
migrate.init_app(self.flask, db)

def set_api(self):
from .resources import api
api.init_app(self.flask, version='1.0.0', title='api')
1 change: 1 addition & 0 deletions app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .base import Example
15 changes: 15 additions & 0 deletions app/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from sqlalchemy import Column, DateTime, func, Integer, String

from .database import db


class Base(db.Model):
__abstract__ = True

id = Column(Integer, primary_key=True)
created_at = Column(DateTime, default=func.now())
updated_at = Column(DateTime, onupdate=func.now())


class Example(Base):
name = Column(String)
5 changes: 5 additions & 0 deletions app/models/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
migrate = Migrate()
22 changes: 22 additions & 0 deletions app/models/datastore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from abc import ABC

from .base import Example


class Datastore(ABC):

def __init__(self, _db=None):
self.session = None
if _db:
self.session = _db.session


class ExampleDatastore(Datastore):

def __init__(self, _db):
super().__init__(_db)

def create_example(self, name):
ex = Example(name=name)
self.session.add(ex)
self.session.commit()
6 changes: 6 additions & 0 deletions app/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .example import ns as example_ns
from ..utils import PatchedApi

api = PatchedApi()

api.add_namespace(example_ns)
18 changes: 18 additions & 0 deletions app/resources/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from flask_restplus import Namespace, Resource, fields

ns = Namespace('example', description='Examples')

success_model = ns.model('Success', {
'message': fields.String
})


@ns.route('', endpoint='index')
class IndexPage(Resource):

@ns.marshal_with(success_model)
def get(self):
"""
Example url
"""
return {'message': 'Success'}
1 change: 1 addition & 0 deletions app/static/css/app.e2713bb0.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file added app/static/img/logo.82b9c7a5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions app/static/js/app.c249faaa.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/static/js/app.c249faaa.js.map

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions app/static/js/chunk-vendors.b10d6c99.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions app/static/js/chunk-vendors.b10d6c99.js.map

Large diffs are not rendered by default.

Binary file added app/templates/favicon.ico
Binary file not shown.
1 change: 1 addition & 0 deletions app/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=favicon.ico><title>web</title><link href=../static/css/app.e2713bb0.css rel=preload as=style><link href=../static/js/app.c249faaa.js rel=preload as=script><link href=../static/js/chunk-vendors.b10d6c99.js rel=preload as=script><link href=../static/css/app.e2713bb0.css rel=stylesheet></head><body><noscript><strong>We're sorry but web doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=../static/js/chunk-vendors.b10d6c99.js></script><script src=../static/js/app.c249faaa.js></script></body></html>
65 changes: 65 additions & 0 deletions app/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from functools import wraps

from flask import url_for, current_app, request, Response, Blueprint
from flask_restplus import Api


def add_basic_auth(blueprint: Blueprint, username, password, realm='api'):
"""
Add HTTP Basic Auth to a blueprint.
Note this is only for casual use!
"""

@blueprint.before_request
def basic_http_auth(*args, **kwargs):
auth = request.authorization
if auth is None or auth.password != password or auth.username != username:
return Response('Please login', 401, {'WWW-Authenticate': f'Basic realm="{realm}"'})


def check_auth(username, password):
"""
This function is called to check if a username /
password combination is valid.
"""
return username == current_app.config['DOC_USERNAME'] and password == current_app.config['DOC_PASSWORD']


def authenticate():
"""
Sends a 401 response that enables basic auth
"""
return Response('Not Authorized', 401, {'WWW-Authenticate': 'Basic realm="api"'})


def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)

return decorated


class PatchedApi(Api):

@property
def specs_url(self):
"""Monkey patch for HTTPS"""
scheme = 'https' if self.is_prod() else 'http'
return url_for(self.endpoint('specs'), _external=True, _scheme=scheme)

@staticmethod
def is_prod():
return not current_app.config['TESTING']

def _register_apidoc(self, app):
app.view_functions['doc'] = requires_auth(app.view_functions['doc'])
return super()._register_apidoc(app)

def handle_error(self, e):
if current_app.config['TESTING'] or current_app.config['APP_ENVIRONMENT'] == 'development':
print(e)
super().handle_error(e)
12 changes: 12 additions & 0 deletions app/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flask import Blueprint, render_template, abort
from jinja2 import TemplateNotFound

sample_page = Blueprint('sample_page', 'sample_page', template_folder='templates')


@sample_page.route('/sample')
def get_sample():
try:
return render_template('index.html')
except TemplateNotFound:
abort(404)
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
addopts = -p no:warnings
env =
APP_ENVIRONMENT=testing
11 changes: 11 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Flask==1.0.2
flask-restplus==0.12.1
Flask-SQLAlchemy==2.4.0
Flask-Migrate==2.4.0
psycopg2-binary==2.7.7
gunicorn==19.9.0
gevent==1.4.0
psycogreen==1.0.1
pytest==4.0.2
pytest-cov==2.6.1
pytest-mock==1.10.1
16 changes: 16 additions & 0 deletions settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import multiprocessing

import gunicorn
from gevent import monkey

monkey.patch_all()

bind = '0.0.0.0:8000'
worker_class = 'gevent'
workers = multiprocessing.cpu_count() * 2 + 1
loglevel = 'debug'
keepalive = 10
timeout = 3600
preload_app = True

gunicorn.SERVER_SOFTWARE = 'Microsoft-IIS/6.0'
Empty file added tests/__init__.py
Empty file.
42 changes: 42 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from gevent import monkey

monkey.patch_all(thread=False)

import psycogreen.gevent

psycogreen.gevent.patch_psycopg()


import pytest
from flask.testing import FlaskClient

from app import create_app
from .utils import JSONResponse


@pytest.fixture(scope='module')
def flask_app():
app = create_app(environment='testing')
from app.models.database import db

with app.app_context():
db.create_all()
yield app
db.session.close_all()
db.drop_all()


@pytest.fixture(scope='module')
def client(flask_app):
app = flask_app
ctx = flask_app.test_request_context()
ctx.push()
app.test_client_class = FlaskClient
app.response_class = JSONResponse
return app.test_client()


@pytest.fixture(scope='module')
def db(flask_app):
from app.models.database import db as db_instance
yield db_instance
12 changes: 12 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flask.testing import FlaskClient

from app.factory import Factory


def test_factory_env_configuration():
factory_app = Factory('testing')
assert factory_app.environment == 'testing'
factory_app.set_flask()
factory_app.environment = 'development'
assert factory_app.environment == 'development'

Loading

0 comments on commit b1a3975

Please sign in to comment.