Skip to content

Commit 7fa289d

Browse files
committed
Adds html info page served at /
Also contains exception handling improvements / tests Issue ga4gh#111
1 parent ab0175c commit 7fa289d

File tree

6 files changed

+157
-13
lines changed

6 files changed

+157
-13
lines changed

ga4gh/frontend.py

+49-3
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,54 @@
99

1010
import os
1111
import traceback
12+
import datetime
1213

1314
import flask
1415
import flask.ext.api as api
1516
import flask.ext.cors as cors
17+
import humanize
1618

1719
import ga4gh.frontend_exceptions as frontendExceptions
1820
import ga4gh.protocol as protocol
1921

20-
app = api.FlaskAPI(__name__)
22+
23+
app = flask.Flask(__name__)
24+
25+
26+
class ServerStatus(object):
27+
28+
def __init__(self):
29+
self.startupTime = datetime.datetime.now()
30+
31+
def getStatusInfo(self):
32+
info = {
33+
'urls': self._getUrls(),
34+
'uptime': self._getUptime(),
35+
'version': protocol.version,
36+
}
37+
return info
38+
39+
def _getUptime(self):
40+
uptime = {
41+
'natural': humanize.naturaltime(self.startupTime),
42+
'datetime': self.startupTime.strftime("%H:%M:%S %d %b %Y")
43+
}
44+
return uptime
45+
46+
def _getUrls(self):
47+
urls = []
48+
rules = app.url_map.iter_rules()
49+
excluded_methods = ('OPTIONS', 'HEAD')
50+
excluded_rules = (
51+
'/', '/flask-api/static/<path:filename>',
52+
'/static/<path:filename>')
53+
for rule in rules:
54+
for method in rule.methods:
55+
if (method not in excluded_methods and
56+
rule.rule not in excluded_rules):
57+
urls.append((rule.rule, method))
58+
urls.sort()
59+
return urls
2160

2261

2362
def configure(config="DefaultConfig", config_file=None):
@@ -28,6 +67,7 @@ def configure(config="DefaultConfig", config_file=None):
2867
if config_file is not None:
2968
app.config.from_pyfile(config_file)
3069
cors.CORS(app, allow_headers='Content-Type')
70+
app.serverStatus = ServerStatus()
3171

3272

3373
def handleHTTPPost(request, endpoint):
@@ -52,7 +92,12 @@ def handleHTTPOptions():
5292

5393

5494
@app.errorhandler(Exception)
55-
def handle_error(exception):
95+
def handleException(exception):
96+
exceptionClass = exception.__class__
97+
if exceptionClass in frontendExceptions.exceptionMap:
98+
newExceptionClass = frontendExceptions.exceptionMap[exceptionClass]
99+
exception = newExceptionClass()
100+
56101
if not isinstance(exception, frontendExceptions.FrontendException):
57102
if app.config['DEBUG']:
58103
print(traceback.format_exc(exception))
@@ -68,7 +113,8 @@ def handle_error(exception):
68113

69114
@app.route('/')
70115
def index():
71-
raise frontendExceptions.PathNotFoundException()
116+
return flask.render_template(
117+
'index.html', info=app.serverStatus.getStatusInfo())
72118

73119

74120
@app.route('/references/<id>', methods=['GET'])

ga4gh/frontend_exceptions.py

+19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from __future__ import print_function
77
from __future__ import unicode_literals
88

9+
import flask.ext.api as api
10+
911

1012
class FrontendException(Exception):
1113

@@ -74,3 +76,20 @@ def __init__(self):
7476
self.httpStatus = 500
7577
self.message = "Internal server error"
7678
self.code = 7
79+
80+
81+
class UnsupportedMediaTypeException(FrontendException):
82+
83+
def __init__(self):
84+
super(FrontendException, self).__init__()
85+
self.httpStatus = 415
86+
self.message = "Unsupported media type"
87+
self.code = 8
88+
89+
90+
# exceptions thrown by the underlying system that we want to
91+
# translate to exceptions that we define before they are
92+
# serialized and returned to the client
93+
exceptionMap = {
94+
api.exceptions.UnsupportedMediaType: UnsupportedMediaTypeException,
95+
}

ga4gh/templates/index.html

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<html>
2+
<head>
3+
<title>GA4GH reference server {{ info.version }}</title>
4+
</head>
5+
<body>
6+
<h2>GA4GH reference server {{ info.version }}</h2>
7+
<div>
8+
<h3>Operations available</h3>
9+
<table>
10+
<th>Method</th>
11+
<th>Path</th>
12+
{% for rule in info.urls %}
13+
<tr>
14+
<td>{{ rule[1] }}</td>
15+
<td>{{ rule[0] }}</td>
16+
</tr>
17+
{% endfor %}
18+
</table>
19+
</div>
20+
<div>
21+
<h3>Uptime</h3>
22+
Running since {{ info.uptime.natural }} ({{ info.uptime.datetime }})
23+
</div>
24+
</body>
25+
</html>

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ avro
55
coverage
66
flake8
77
guppy
8+
humanize
89
mock
910
nose
1011
pep8

tests/test_exceptions.py

+62-9
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,51 @@
99
import unittest
1010
import inspect
1111

12+
import mock
13+
1214
import ga4gh.frontend_exceptions as frontendExceptions
15+
import ga4gh.frontend as frontend
16+
17+
18+
class TestExceptionHandler(unittest.TestCase):
19+
"""
20+
Test that caught exceptions are handled correctly
21+
"""
22+
class UnknownException(Exception):
23+
pass
24+
25+
class DummyFlask(object):
26+
27+
def __call__(self, *args):
28+
return TestExceptionHandler.DummyResponse()
29+
30+
class DummyResponse(object):
31+
pass
32+
33+
def run(self, *args, **kwargs):
34+
# patching is required because flask.jsonify throws an exception
35+
# if not being called in a running app context
36+
dummyFlask = self.DummyFlask()
37+
with mock.patch('flask.jsonify', dummyFlask):
38+
super(TestExceptionHandler, self).run(*args, **kwargs)
39+
40+
def testMappedException(self):
41+
for originalExceptionClass, mappedExceptionClass in \
42+
frontendExceptions.exceptionMap.items():
43+
originalException = originalExceptionClass()
44+
mappedException = mappedExceptionClass()
45+
response = frontend.handleException(originalException)
46+
self.assertEquals(response.status_code, mappedException.httpStatus)
47+
48+
def testFrontendException(self):
49+
exception = frontendExceptions.ObjectNotFoundException()
50+
response = frontend.handleException(exception)
51+
self.assertEquals(response.status_code, 404)
52+
53+
def testUnknownExceptionBecomesServerError(self):
54+
exception = self.UnknownException()
55+
response = frontend.handleException(exception)
56+
self.assertEquals(response.status_code, 500)
1357

1458

1559
class TestFrontendExceptionConsistency(unittest.TestCase):
@@ -18,24 +62,33 @@ class TestFrontendExceptionConsistency(unittest.TestCase):
1862
- every frontend exception has a non-None error code
1963
- except FrontendException, which does
2064
- every frontend exception has a unique error code
65+
- every value in exceptionMap
66+
- is able to instantiate a new no-argument exception instance
67+
- derives from the base frontend exception type
2168
"""
2269

2370
def _getFrontendExceptionClasses(self):
2471

25-
def isClassAndExceptionSubclass(clazz):
26-
return inspect.isclass(clazz) and issubclass(clazz, Exception)
72+
def isClassAndExceptionSubclass(class_):
73+
return inspect.isclass(class_) and issubclass(class_, Exception)
2774

2875
classes = inspect.getmembers(
2976
frontendExceptions, isClassAndExceptionSubclass)
30-
return [clazz for _, clazz in classes]
77+
return [class_ for _, class_ in classes]
3178

3279
def testCodeInvariants(self):
3380
codes = set()
34-
for clazz in self._getFrontendExceptionClasses():
35-
instance = clazz()
36-
assert instance.code not in codes
81+
for class_ in self._getFrontendExceptionClasses():
82+
instance = class_()
83+
self.assertTrue(instance.code not in codes)
3784
codes.add(instance.code)
38-
if clazz == frontendExceptions.FrontendException:
39-
assert instance.code is None
85+
if class_ == frontendExceptions.FrontendException:
86+
self.assertIsNone(instance.code)
4087
else:
41-
assert instance.code is not None
88+
self.assertIsNotNone(instance.code)
89+
90+
def testExceptionMap(self):
91+
for exceptionClass in frontendExceptions.exceptionMap.values():
92+
exception = exceptionClass()
93+
self.assertIsInstance(
94+
exception, frontendExceptions.FrontendException)

tests/test_views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def sendVariantSetsSearch(self, data):
3030
data=data)
3131

3232
def testServer(self):
33-
self.assertEqual(404, self.app.get('/').status_code)
33+
self.assertEqual(404, self.app.get('/doesNotExist').status_code)
3434

3535
def testCors(self):
3636
def assertHeaders(response):

0 commit comments

Comments
 (0)