Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Update application code for Heroku + SQLAlchemy
Updated to accomodate new logging configuration (stdout),
command line options, and to be able to provide sqlalchemy
sessions for database calls.
  • Loading branch information
jiffyclub committed Jul 18, 2017
1 parent 50c5cf6 commit ca35047
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 80 deletions.
133 changes: 81 additions & 52 deletions app/app.py
@@ -1,14 +1,17 @@
import contextlib
import json
import logging
import os

import jsonschema
import sqlalchemy as sa
import tornado.ioloop
import tornado.log
import tornado.options
import tornado.web

from ipythonblocks import BlockGrid
from sqlalchemy.orm import sessionmaker
from twiggy import log

# local imports
Expand All @@ -17,19 +20,13 @@
from .colorize import colorize
from .twiggy_setup import twiggy_setup

tornado.options.define('tornado_log_file',
default='/var/log/ipborg/tornado.log',
type=str)
tornado.options.define('app_log_file',
default='/var/log/ipborg/app.log',
type=str)
tornado.options.parse_command_line()
tornado.options.define('port', default=80, type=int)
tornado.options.define('db_url', type=str)
log = log.name(__name__)


def configure_tornado_logging():
fh = logging.handlers.RotatingFileHandler(
tornado.options.options.tornado_log_file,
maxBytes=2**29, backupCount=10)
fh = logging.StreamHandler()

fmt = logging.Formatter('%(asctime)s:%(levelname)s:%(name)s:%(message)s')
fh.setFormatter(fmt)
Expand All @@ -41,14 +38,6 @@ def configure_tornado_logging():
tornado.log.enable_pretty_logging(logger=logger)


settings = {
'static_path': os.path.join(os.path.dirname(__file__), 'static'),
'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
'debug': True,
'gzip': True
}


class MainHandler(tornado.web.StaticFileHandler):
def parse_url_path(self, url_path):
return 'main.html'
Expand All @@ -59,7 +48,21 @@ def parse_url_path(self, url_path):
return 'about.html'


class PostHandler(tornado.web.RequestHandler):
class DBAccessHandler(tornado.web.RequestHandler):
@contextlib.contextmanager
def session_context(self):
session = self.application.session_factory()
try:
yield session
session.commit()
except:
session.rollback()
raise
finally:
session.close()


class PostHandler(DBAccessHandler):
def post(self):
try:
req_data = json.loads(self.request.body)
Expand All @@ -73,7 +76,8 @@ def post(self):
log.debug('Post JSON validation failed.')
raise tornado.web.HTTPError(400, 'Post JSON validation failed.')

hash_id = dbi.store_grid_entry(req_data)
with self.session_context() as session:
hash_id = dbi.store_grid_entry(session, req_data)

if req_data['secret']:
url = 'http://ipythonblocks.org/secret/{}'
Expand All @@ -85,33 +89,37 @@ def post(self):
self.write({'url': url})


class GetGridSpecHandler(tornado.web.RequestHandler):
class GetGridSpecHandler(DBAccessHandler):
def initialize(self, secret):
self.secret = secret

def get(self, hash_id):
grid_spec = dbi.get_grid_entry(hash_id, self.secret)
if not grid_spec:
raise tornado.web.HTTPError(404, 'Grid not found.')
with self.session_context() as session:
grid_spec = dbi.get_grid_entry(session, hash_id, self.secret)

if not grid_spec:
raise tornado.web.HTTPError(404, 'Grid not found.')

self.write(grid_spec['grid_data'])
self.write(grid_spec.grid_data)


class RandomHandler(tornado.web.RequestHandler):
class RandomHandler(DBAccessHandler):
def get(self):
hash_id = dbi.get_random_hash_id()
with self.session_context() as session:
hash_id = dbi.get_random_hash_id(session)
log.info('redirecting to url /{0}', hash_id)
self.redirect('/' + hash_id, status=303)


class ErrorHandler(tornado.web.RequestHandler):
class ErrorHandler(DBAccessHandler):
def get(self):
self.send_error(404)

def write_error(self, status_code, **kwargs):
if status_code == 404:
self.render('404.html')
else:
super(ErrorHandler, self).send_error(status_code, **kwargs)
super().send_error(status_code, **kwargs)


class RenderGridHandler(ErrorHandler):
Expand All @@ -120,38 +128,59 @@ def initialize(self, secret):

@tornado.web.removeslash
def get(self, hash_id):
grid_spec = dbi.get_grid_entry(hash_id, secret=self.secret)
if not grid_spec:
self.send_error(404)
return
with self.session_context() as session:
grid_spec = dbi.get_grid_entry(session, hash_id, secret=self.secret)

gd = grid_spec['grid_data']
grid = BlockGrid(gd['width'], gd['height'], lines_on=gd['lines_on'])
grid._load_simple_grid(gd['blocks'])
grid_html = grid._repr_html_()
if not grid_spec:
self.send_error(404)
return

code_cells = grid_spec['code_cells'] or []
code_cells = [colorize(c) for c in code_cells]
gd = grid_spec.grid_data
grid = BlockGrid(gd['width'], gd['height'], lines_on=gd['lines_on'])
grid._load_simple_grid(gd['blocks'])
grid_html = grid._repr_html_()

self.render('grid.html', grid_html=grid_html, code_cells=code_cells)
code_cells = grid_spec.code_cells or []
code_cells = [colorize(c) for c in code_cells]

self.render('grid.html', grid_html=grid_html, code_cells=code_cells)


class AppWithSession(tornado.web.Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.engine = sa.create_engine(tornado.options.options.db_url)
self.session_factory = sessionmaker(bind=self.engine)


SETTINGS = {
'static_path': os.path.join(os.path.dirname(__file__), 'static'),
'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
'debug': True,
'gzip': True
}


application = tornado.web.Application([
(r'/()', MainHandler, {'path': settings['template_path']}),
(r'/(about)', AboutHandler, {'path': settings['template_path']}),
(r'/random', RandomHandler),
(r'/post', PostHandler),
(r'/get/(\w{6}\w*)', GetGridSpecHandler, {'secret': False}),
(r'/get/secret/(\w{6}\w*)', GetGridSpecHandler, {'secret': True}),
(r'/(\w{6}\w*)/*', RenderGridHandler, {'secret': False}),
(r'/secret/(\w{6}\w*)/*', RenderGridHandler, {'secret': True}),
(r'/.*', ErrorHandler)
], **settings)
def make_application():
return AppWithSession(handlers=[
(r'/()', MainHandler, {'path': SETTINGS['template_path']}),
(r'/(about)', AboutHandler, {'path': SETTINGS['template_path']}),
(r'/random', RandomHandler),
(r'/post', PostHandler),
(r'/get/(\w{6}\w*)', GetGridSpecHandler, {'secret': False}),
(r'/get/secret/(\w{6}\w*)', GetGridSpecHandler, {'secret': True}),
(r'/(\w{6}\w*)/*', RenderGridHandler, {'secret': False}),
(r'/secret/(\w{6}\w*)/*', RenderGridHandler, {'secret': True}),
(r'/.*', ErrorHandler)
], **SETTINGS)


if __name__ == '__main__':
tornado.options.parse_command_line()
configure_tornado_logging()
twiggy_setup()

application.listen(8877)
log.fields(port=tornado.options.options.port).info('starting server')
application = make_application()
application.listen(tornado.options.options.port)
tornado.ioloop.IOLoop.instance().start()
65 changes: 37 additions & 28 deletions app/test/test_app.py
Expand Up @@ -2,32 +2,31 @@
import os
import tempfile

import dataset
import pytest
import sqlalchemy as sa
import testing.postgresql
import tornado.options
import tornado.testing

import mock
from sqlalchemy.orm import sessionmaker

from .. import app
from .. import dbinterface as dbi
from .. import models


def setup_module(module):
tornado.options.options.public_salt = 'public'
tornado.options.options.secret_salt = 'secret'
dbi.get_memcached().flush_all()
module.PG_FACTORY = testing.postgresql.PostgresqlFactory(
cache_initialized_db=True)


def teardown_module(module):
dbi.get_memcached().flush_all()


def setup_function(function):
_, tornado.options.options.db_file = tempfile.mkstemp()
module.PG_FACTORY.clear_cache()


def teardown_function(function):
os.remove(tornado.options.options.db_file)
@pytest.fixture(autouse=True)
def set_salts(monkeypatch):
monkeypatch.setenv('HASHIDS_PUBLIC_SALT', 'public')
monkeypatch.setenv('HASHIDS_SECRET_SALT', 'secret')


def data_2x2():
Expand All @@ -53,24 +52,30 @@ def request():

class UtilBase(tornado.testing.AsyncHTTPTestCase):
def setup_method(self, method):
_, tornado.options.options.db_file = tempfile.mkstemp()
self.postgresql = PG_FACTORY()
self.engine = sa.create_engine(self.postgresql.url())
models.Base.metadata.create_all(bind=self.engine)
self.Session = sessionmaker(bind=self.engine)
self.session = self.Session()
tornado.options.options.db_url = self.postgresql.url()

def teardown_method(self, method):
os.remove(tornado.options.options.db_file)
self.session.close()
self.Session.close_all()
self.engine.dispose()
self.postgresql.stop()

def get_app(self):
return app.application
return app.make_application()

def get_response(self, body=None):
self.http_client.fetch(
self.get_url(self.app_url), self.stop,
method=self.method, body=body)
return self.wait()
return self.fetch(self.app_url, method=self.method, body=body)

def save_grid(self, secret):
req = request()
req['secret'] = secret
hash_id = dbi.store_grid_entry(req)
hash_id = dbi.store_grid_entry(self.session, req)
self.session.commit()
return hash_id


Expand Down Expand Up @@ -115,9 +120,13 @@ def test_stores_data(self):

body = json.loads(response.body)
hash_id = body['url'].split('/')[-1]
grid_spec = dbi.get_grid_entry(hash_id)
del grid_spec['id']
assert grid_spec == json.loads(json.dumps(req))
grid_spec = dbi.get_grid_entry(self.session, hash_id)
assert grid_spec.id == 1

comp_data= json.loads(json.dumps(req))

for key, value in comp_data.items():
assert getattr(grid_spec, key) == value


class TestGetGrid(UtilBase):
Expand Down Expand Up @@ -175,14 +184,14 @@ def test_render(self):

response = self.get_response()
assert response.code == 200
assert '<table' in response.body
assert 'asdf' in response.body
assert b'<table' in response.body
assert b'asdf' in response.body

def test_render_secret(self):
hash_id = self.save_grid(True)
self.app_url = '/secret/{}'.format(hash_id)

response = self.get_response()
assert response.code == 200
assert '<table' in response.body
assert 'asdf' in response.body
assert b'<table' in response.body
assert b'asdf' in response.body

0 comments on commit ca35047

Please sign in to comment.