Added addressable page per row

Refs #1 - only exists for tables with introspectable primary keys.

Still need to link to this page.

Also added first unit tests - refs #9
This commit is contained in:
Simon Willison 2017-10-23 22:54:58 -07:00
commit 6a9fdcc071
2 changed files with 101 additions and 1 deletions

56
app.py
View file

@ -6,6 +6,7 @@ from sanic_jinja2 import SanicJinja2
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
from functools import wraps from functools import wraps
import urllib.parse
import json import json
import hashlib import hashlib
import sys import sys
@ -157,8 +158,37 @@ class TableView(BaseView):
} }
class RowView(BaseView):
template = 'table.html'
def data(self, request, name, hash, table, pk_path):
conn = get_conn(name)
pk_values = compound_pks_from_path(pk_path)
pks = pks_for_table(conn, table)
wheres = [
'{}=?'.format(pk)
for pk in pks
]
sql = 'select * from "{}" where {}'.format(
table, ' AND '.join(wheres)
)
rows = conn.execute(sql, pk_values)
columns = [r[0] for r in rows.description]
rows = list(rows)
if not rows:
raise NotFound('Record not found: {}'.format(pk_values))
return {
'database': name,
'database_hash': hash,
'table': table,
'rows': rows,
'columns': columns,
}
app.add_route(DatabaseView.as_view(), '/<db_name:[^/]+?><as_json:(.json)?$>') app.add_route(DatabaseView.as_view(), '/<db_name:[^/]+?><as_json:(.json)?$>')
app.add_route(TableView.as_view(), '/<db_name:[^/]+>/<table:[^/]+?><as_json:(.json)?$>') app.add_route(TableView.as_view(), '/<db_name:[^/]+>/<table:[^/]+?><as_json:(.json)?$>')
app.add_route(RowView.as_view(), '/<db_name:[^/]+>/<table:[^/]+?>/<pk_path:[^/]+?><as_json:(.json)?$>')
def resolve_db_name(db_name, **kwargs): def resolve_db_name(db_name, **kwargs):
@ -179,7 +209,7 @@ def resolve_db_name(db_name, **kwargs):
try: try:
info = databases[name] info = databases[name]
except KeyError: except KeyError:
raise NotFound() raise NotFound('Database not found: {}'.format(name))
expected = info['hash'][:7] expected = info['hash'][:7]
if expected != hash: if expected != hash:
should_redirect = '/{}-{}'.format( should_redirect = '/{}-{}'.format(
@ -191,6 +221,30 @@ def resolve_db_name(db_name, **kwargs):
return name, expected, None return name, expected, None
def compound_pks_from_path(path):
return [
urllib.parse.unquote_plus(b) for b in path.split(',')
]
def pks_for_table(conn, table):
rows = [
row for row in conn.execute(
'PRAGMA table_info("{}")'.format(table)
).fetchall()
if row[-1]
]
rows.sort(key=lambda row: row[-1])
return [r[1] for r in rows]
def path_from_row_pks(row, pks):
bits = []
for pk in pks:
bits.append(urllib.parse.quote_plus(row[pk]))
return ','.join(bits)
if __name__ == '__main__': if __name__ == '__main__':
if '--build' in sys.argv: if '--build' in sys.argv:
ensure_build_metadata(True) ensure_build_metadata(True)

46
test_helpers.py Normal file
View file

@ -0,0 +1,46 @@
import app
import pytest
import sqlite3
@pytest.mark.parametrize('path,expected', [
('foo', ['foo']),
('foo,bar', ['foo', 'bar']),
('123,433,112', ['123', '433', '112']),
('123%2C433,112', ['123,433', '112']),
('123%2F433%2F112', ['123/433/112']),
])
def test_compound_pks_from_path(path, expected):
assert expected == app.compound_pks_from_path(path)
@pytest.mark.parametrize('sql,table,expected_keys', [
('''
CREATE TABLE `Compound` (
A varchar(5) NOT NULL,
B varchar(10) NOT NULL,
PRIMARY KEY (A, B)
);
''', 'Compound', ['A', 'B']),
('''
CREATE TABLE `Compound2` (
A varchar(5) NOT NULL,
B varchar(10) NOT NULL,
PRIMARY KEY (B, A)
);
''', 'Compound2', ['B', 'A']),
])
def test_pks_for_table(sql, table, expected_keys):
conn = sqlite3.connect(':memory:')
conn.execute(sql)
actual = app.pks_for_table(conn, table)
assert expected_keys == actual
@pytest.mark.parametrize('row,pks,expected_path', [
({'A': 'foo', 'B': 'bar'}, ['A', 'B'], 'foo,bar'),
({'A': 'f,o', 'B': 'bar'}, ['A', 'B'], 'f%2Co,bar'),
])
def test_path_from_row_pks(row, pks, expected_path):
actual_path = app.path_from_row_pks(row, pks)
assert expected_path == actual_path