From b55809a1e20986bb2e638b698815a77902e8708d Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 18 Apr 2018 22:24:48 -0700 Subject: [PATCH] Added /-/metadata /-/plugins /-/inspect, closes #225 --- datasette/app.py | 63 ++++++++++++++++++++++++------ datasette/templates/show_json.html | 12 ++++++ datasette/utils.py | 18 +++++++++ tests/fixtures.py | 4 +- tests/test_api.py | 28 +++++++++++++ 5 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 datasette/templates/show_json.html diff --git a/datasette/app.py b/datasette/app.py index 697a1aff..cc3a9c08 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -17,7 +17,6 @@ import json import jinja2 import hashlib import time -import pkg_resources import pint import pluggy import traceback @@ -31,6 +30,7 @@ from .utils import ( escape_sqlite, filters_should_redirect, get_all_foreign_keys, + get_plugins, is_url, InvalidSql, module_from_path, @@ -402,15 +402,16 @@ class IndexView(RenderMixin): } databases.append(database) if as_json: + headers = {} + if self.ds.cors: + headers['Access-Control-Allow-Origin'] = '*' return response.HTTPResponse( json.dumps( {db['name']: db for db in databases}, cls=CustomJSONEncoder ), content_type='application/json', - headers={ - 'Access-Control-Allow-Origin': '*' - } + headers=headers, ) else: return self.render( @@ -423,6 +424,32 @@ class IndexView(RenderMixin): ) +class JsonDataView(RenderMixin): + def __init__(self, datasette, filename, data_callback): + self.ds = datasette + self.jinja_env = datasette.jinja_env + self.filename = filename + self.data_callback = data_callback + + async def get(self, request, as_json): + data = self.data_callback() + if as_json: + headers = {} + if self.ds.cors: + headers['Access-Control-Allow-Origin'] = '*' + return response.HTTPResponse( + json.dumps(data), + content_type='application/json', + headers=headers, + ) + else: + return self.render( + ['show_json.html'], + filename=self.filename, + data=data, + ) + + async def favicon(request): return response.text('') @@ -1302,15 +1329,25 @@ class Datasette: for path, dirname in self.static_mounts: app.static(path, dirname) # Mount any plugin static/ directories - for plugin_module in pm.get_plugins(): - try: - if pkg_resources.resource_isdir(plugin_module.__name__, 'static'): - modpath = '/-/static-plugins/{}/'.format(plugin_module.__name__) - dirpath = pkg_resources.resource_filename(plugin_module.__name__, 'static') - app.static(modpath, dirpath) - except (KeyError, ImportError): - # Caused by --plugins_dir= plugins - KeyError/ImportError thrown in Py3.5 - pass + for plugin in get_plugins(pm): + if plugin['static_path']: + modpath = '/-/static-plugins/{}/'.format(plugin['name']) + app.static(modpath, plugin['static_path']) + app.add_route( + JsonDataView.as_view(self, 'inspect.json', lambda: self.inspect()), + '/-/inspect' + ) + app.add_route( + JsonDataView.as_view(self, 'metadata.json', lambda: self.metadata), + '/-/metadata' + ) + app.add_route( + JsonDataView.as_view(self, 'plugins.json', lambda: [{ + 'name': p['name'], + 'static': p['static_path'] is not None + } for p in get_plugins(pm)]), + '/-/plugins' + ) app.add_route( DatabaseView.as_view(self), '/' diff --git a/datasette/templates/show_json.html b/datasette/templates/show_json.html new file mode 100644 index 00000000..c87ec9f7 --- /dev/null +++ b/datasette/templates/show_json.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block title %}{{ filename }}{% endblock %} + +{% block body_class %}show-json{% endblock %} + +{% block content %} +

{{ filename }}

+ +
{{ data|tojson(indent=4) }}
+ +{% endblock %} diff --git a/datasette/utils.py b/datasette/utils.py index 09795f43..3ef045fb 100644 --- a/datasette/utils.py +++ b/datasette/utils.py @@ -4,6 +4,7 @@ import hashlib import imp import json import os +import pkg_resources import re import shlex import sqlite3 @@ -683,3 +684,20 @@ def module_from_path(path, name): code = compile(file.read(), path, 'exec', dont_inherit=True) exec(code, mod.__dict__) return mod + + +def get_plugins(pm): + plugins = [] + for plugin in pm.get_plugins(): + static_path = None + try: + if pkg_resources.resource_isdir(plugin.__name__, 'static'): + static_path = pkg_resources.resource_filename(plugin.__name__, 'static') + except (KeyError, ImportError): + # Caused by --plugins_dir= plugins - KeyError/ImportError thrown in Py3.5 + pass + plugins.append({ + 'name': plugin.__name__, + 'static_path': static_path, + }) + return plugins diff --git a/tests/fixtures.py b/tests/fixtures.py index 4ba39448..93942dda 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -29,7 +29,9 @@ def app_client(sql_time_limit_ms=None): ds.sqlite_functions.append( ('sleep', 1, lambda n: time.sleep(float(n))), ) - yield ds.app().test_client + client = ds.app().test_client + client.ds = ds + yield client def app_client_longer_time_limit(): diff --git a/tests/test_api.py b/tests/test_api.py index 2b741590..e1db8c46 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -3,6 +3,7 @@ from .fixtures import ( app_client_longer_time_limit, generate_compound_rows, generate_sortable_rows, + METADATA, ) import pytest @@ -629,3 +630,30 @@ def test_plugins_dir_plugin(app_client): gather_request=False ) assert pytest.approx(328.0839) == response.json['rows'][0][0] + + +def test_metadata_json(app_client): + response = app_client.get( + "/-/metadata.json", + gather_request=False + ) + assert METADATA == response.json + + +def test_inspect_json(app_client): + response = app_client.get( + "/-/inspect.json", + gather_request=False + ) + assert app_client.ds.inspect() == response.json + + +def test_plugins_json(app_client): + response = app_client.get( + "/-/plugins.json", + gather_request=False + ) + # This will include any plugins that have been installed into the + # current virtual environment, so we only check for the presence of + # the one we know will definitely be There + assert {'name': 'my_plugin.py', 'static': False} in response.json