Initial proof-of-concept .csv export, refs #266

This commit is contained in:
Simon Willison 2018-05-17 07:02:48 -07:00
commit 654fde5792
No known key found for this signature in database
GPG key ID: 17E2DEA2588B7F52
3 changed files with 57 additions and 18 deletions

View file

@ -53,9 +53,9 @@ class JsonDataView(RenderMixin):
self.filename = filename self.filename = filename
self.data_callback = data_callback self.data_callback = data_callback
async def get(self, request, as_json): async def get(self, request, as_ext):
data = self.data_callback() data = self.data_callback()
if as_json: if as_ext:
headers = {} headers = {}
if self.ds.cors: if self.ds.cors:
headers["Access-Control-Allow-Origin"] = "*" headers["Access-Control-Allow-Origin"] = "*"
@ -406,7 +406,7 @@ class Datasette:
self.jinja_env.filters["escape_sqlite"] = escape_sqlite self.jinja_env.filters["escape_sqlite"] = escape_sqlite
self.jinja_env.filters["to_css_class"] = to_css_class self.jinja_env.filters["to_css_class"] = to_css_class
pm.hook.prepare_jinja2_environment(env=self.jinja_env) pm.hook.prepare_jinja2_environment(env=self.jinja_env)
app.add_route(IndexView.as_view(self), "/<as_json:(\.jsono?)?$>") app.add_route(IndexView.as_view(self), "/<as_ext:(\.jsono?|\.csv)?$>")
# TODO: /favicon.ico and /-/static/ deserve far-future cache expires # TODO: /favicon.ico and /-/static/ deserve far-future cache expires
app.add_route(favicon, "/favicon.ico") app.add_route(favicon, "/favicon.ico")
app.static("/-/static/", str(app_root / "datasette" / "static")) app.static("/-/static/", str(app_root / "datasette" / "static"))
@ -419,33 +419,33 @@ class Datasette:
app.static(modpath, plugin["static_path"]) app.static(modpath, plugin["static_path"])
app.add_route( app.add_route(
JsonDataView.as_view(self, "inspect.json", self.inspect), JsonDataView.as_view(self, "inspect.json", self.inspect),
"/-/inspect<as_json:(\.json)?$>", "/-/inspect<as_ext:(\.json)?$>",
) )
app.add_route( app.add_route(
JsonDataView.as_view(self, "metadata.json", lambda: self.metadata), JsonDataView.as_view(self, "metadata.json", lambda: self.metadata),
"/-/metadata<as_json:(\.json)?$>", "/-/metadata<as_ext:(\.json)?$>",
) )
app.add_route( app.add_route(
JsonDataView.as_view(self, "versions.json", self.versions), JsonDataView.as_view(self, "versions.json", self.versions),
"/-/versions<as_json:(\.json)?$>", "/-/versions<as_ext:(\.json)?$>",
) )
app.add_route( app.add_route(
JsonDataView.as_view(self, "plugins.json", self.plugins), JsonDataView.as_view(self, "plugins.json", self.plugins),
"/-/plugins<as_json:(\.json)?$>", "/-/plugins<as_ext:(\.json)?$>",
) )
app.add_route( app.add_route(
DatabaseView.as_view(self), "/<db_name:[^/\.]+?><as_json:(\.jsono?)?$>" DatabaseView.as_view(self), "/<db_name:[^/\.]+?><as_ext:(\.jsono?|\.csv)?$>"
) )
app.add_route( app.add_route(
DatabaseDownload.as_view(self), "/<db_name:[^/]+?><as_db:(\.db)$>" DatabaseDownload.as_view(self), "/<db_name:[^/]+?><as_db:(\.db)$>"
) )
app.add_route( app.add_route(
TableView.as_view(self), TableView.as_view(self),
"/<db_name:[^/]+>/<table:[^/]+?><as_json:(\.jsono?)?$>", "/<db_name:[^/]+>/<table:[^/]+?><as_ext:(\.jsono?|\.csv)?$>",
) )
app.add_route( app.add_route(
RowView.as_view(self), RowView.as_view(self),
"/<db_name:[^/]+>/<table:[^/]+?>/<pk_path:[^/]+?><as_json:(\.jsono?)?$>", "/<db_name:[^/]+>/<table:[^/]+?>/<pk_path:[^/]+?><as_ext:(\.jsono?|\.csv)?$>",
) )
self.register_custom_units() self.register_custom_units()

View file

@ -1,4 +1,5 @@
import asyncio import asyncio
import csv
import json import json
import re import re
import sqlite3 import sqlite3
@ -120,8 +121,8 @@ class BaseView(RenderMixin):
should_redirect += "/" + kwargs["table"] should_redirect += "/" + kwargs["table"]
if "pk_path" in kwargs: if "pk_path" in kwargs:
should_redirect += "/" + kwargs["pk_path"] should_redirect += "/" + kwargs["pk_path"]
if "as_json" in kwargs: if "as_ext" in kwargs:
should_redirect += kwargs["as_json"] should_redirect += kwargs["as_ext"]
if "as_db" in kwargs: if "as_db" in kwargs:
should_redirect += kwargs["as_db"] should_redirect += kwargs["as_db"]
return name, expected, should_redirect return name, expected, should_redirect
@ -198,11 +199,49 @@ class BaseView(RenderMixin):
return await self.view_get(request, name, hash, **kwargs) return await self.view_get(request, name, hash, **kwargs)
async def as_csv(self, request, name, hash, **kwargs):
try:
response_or_template_contexts = await self.data(
request, name, hash, **kwargs
)
if isinstance(response_or_template_contexts, response.HTTPResponse):
return response_or_template_contexts
else:
data, extra_template_data, templates = response_or_template_contexts
except (sqlite3.OperationalError, InvalidSql) as e:
raise DatasetteError(str(e), title="Invalid SQL", status=400)
except (sqlite3.OperationalError) as e:
raise DatasetteError(str(e))
except DatasetteError:
raise
# Convert rows and columns to CSV
async def stream_fn(r):
writer = csv.writer(r)
writer.writerow(data["columns"])
for row in data["rows"]:
writer.writerow(row)
return response.stream(
stream_fn,
headers={
"Content-Disposition": 'attachment; filename="{}.csv"'.format(
name
)
},
content_type="text/csv; charset=utf-8"
)
async def view_get(self, request, name, hash, **kwargs): async def view_get(self, request, name, hash, **kwargs):
try: try:
as_json = kwargs.pop("as_json") as_ext = kwargs.pop("as_ext")
except KeyError: except KeyError:
as_json = False as_ext = False
if as_ext == ".csv":
return await self.as_csv(request, name, hash, **kwargs)
extra_template_data = {} extra_template_data = {}
start = time.time() start = time.time()
status_code = 200 status_code = 200
@ -231,9 +270,9 @@ class BaseView(RenderMixin):
value = self.ds.metadata.get(key) value = self.ds.metadata.get(key)
if value: if value:
data[key] = value data[key] = value
if as_json: if as_ext:
# Special case for .jsono extension - redirect to _shape=objects # Special case for .jsono extension - redirect to _shape=objects
if as_json == ".jsono": if as_ext == ".jsono":
return self.redirect( return self.redirect(
request, request,
path_with_added_args( path_with_added_args(

View file

@ -16,7 +16,7 @@ class IndexView(RenderMixin):
self.jinja_env = datasette.jinja_env self.jinja_env = datasette.jinja_env
self.executor = datasette.executor self.executor = datasette.executor
async def get(self, request, as_json): async def get(self, request, as_ext):
databases = [] databases = []
for key, info in sorted(self.ds.inspect().items()): for key, info in sorted(self.ds.inspect().items()):
tables = [t for t in info["tables"].values() if not t["hidden"]] tables = [t for t in info["tables"].values() if not t["hidden"]]
@ -38,7 +38,7 @@ class IndexView(RenderMixin):
"views_count": len(info["views"]), "views_count": len(info["views"]),
} }
databases.append(database) databases.append(database)
if as_json: if as_ext:
headers = {} headers = {}
if self.ds.cors: if self.ds.cors:
headers["Access-Control-Allow-Origin"] = "*" headers["Access-Control-Allow-Origin"] = "*"