From 7239175f63d150356a7f795cc4cabf7764d2cf68 Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Fri, 9 Oct 2020 20:51:56 -0700
Subject: [PATCH 0001/1525] Fixed broken column header links, closes #1011
---
datasette/templates/_table.html | 4 ++--
tests/test_html.py | 10 +++++-----
tests/test_plugins.py | 4 ++--
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/datasette/templates/_table.html b/datasette/templates/_table.html
index 65789045..1dd94212 100644
--- a/datasette/templates/_table.html
+++ b/datasette/templates/_table.html
@@ -8,9 +8,9 @@
{{ column.name }}
{% else %}
{% if column.name == sort %}
- {{ column.name }} ▼
+ {{ column.name }} ▼
{% else %}
- {{ column.name }}{% if column.name == sort_desc %} ▲{% endif %}
+ {{ column.name }}{% if column.name == sort_desc %} ▲{% endif %}
{% endif %}
{% endif %}
diff --git a/tests/test_html.py b/tests/test_html.py
index 3f8cb178..5691b6c4 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -345,7 +345,7 @@ def test_sort_links(app_client):
attrs_and_link_attrs = [
{
"attrs": th.attrs,
- "a_href": (th.find("a")["href"].split("/")[-1] if th.find("a") else None),
+ "a_href": (th.find("a")["href"] if th.find("a") else None),
}
for th in ths
]
@@ -403,7 +403,7 @@ def test_sort_links(app_client):
"data-column-not-null": "0",
"data-is-pk": "0",
},
- "a_href": "sortable?_sort_desc=sortable",
+ "a_href": "/fixtures/sortable?_sort_desc=sortable",
},
{
"attrs": {
@@ -414,7 +414,7 @@ def test_sort_links(app_client):
"data-column-not-null": "0",
"data-is-pk": "0",
},
- "a_href": "sortable?_sort=sortable_with_nulls",
+ "a_href": "/fixtures/sortable?_sort=sortable_with_nulls",
},
{
"attrs": {
@@ -425,7 +425,7 @@ def test_sort_links(app_client):
"data-column-not-null": "0",
"data-is-pk": "0",
},
- "a_href": "sortable?_sort=sortable_with_nulls_2",
+ "a_href": "/fixtures/sortable?_sort=sortable_with_nulls_2",
},
{
"attrs": {
@@ -436,7 +436,7 @@ def test_sort_links(app_client):
"data-column-not-null": "0",
"data-is-pk": "0",
},
- "a_href": "sortable?_sort=text",
+ "a_href": "/fixtures/sortable?_sort=text",
},
]
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 4b3634ab..08ed2e6b 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -497,9 +497,9 @@ def test_hook_register_output_renderer_can_render(app_client):
.find("p", {"class": "export-links"})
.findAll("a")
)
- actual = [l["href"].split("/")[-1] for l in links]
+ actual = [l["href"] for l in links]
# Should not be present because we sent ?_no_can_render=1
- assert "facetable.testall?_labels=on" not in actual
+ assert "/fixtures/facetable.testall?_labels=on" not in actual
# Check that it was passed the values we expected
assert hasattr(app_client.ds, "_can_render_saw")
assert {
From 0e58ae7600212c075f5b8ae4b52d2af0e1acd4f1 Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Fri, 9 Oct 2020 20:53:47 -0700
Subject: [PATCH 0002/1525] Release 0.50.2
Refs #1011
---
docs/changelog.rst | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index f0e825b3..1d654485 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,7 +4,15 @@
Changelog
=========
-.. _v0_50.1:
+.. _v0_50_2:
+
+0.50.2 (2020-10-09)
+-------------------
+
+- Fixed another bug introduced in 0.50 where column header links on the table page were broken. (`#1011 `__)
+
+
+.. _v0_50_1:
0.50.1 (2020-10-09)
-------------------
From a67cb536f1fde4b3cf38032b61bcc6d38c30d762 Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Sat, 10 Oct 2020 13:54:27 -0700
Subject: [PATCH 0003/1525] Promote the Datasette Weekly newsletter
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 92c898af..66ddf803 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,8 @@ Datasette is aimed at data journalists, museum curators, archivists, local gover
* Live demo of current main: https://latest.datasette.io/
* Support questions, feedback? Join our [GitHub Discussions forum](https://github.com/simonw/datasette/discussions)
+Want to stay up-to-date with the project? Subscribe to the [Datasette Weekly newsletter](https://datasette.substack.com/) for tips, tricks and news on what's new in the Datasette ecosystem.
+
## News
* 9th October 2020: [Datasette 0.50](https://docs.datasette.io/en/stable/changelog.html#v0-50) - New column actions menu. `datasette.client` object for plugins to make internal API requests. Improved documentation on deploying Datasette. [Annotated release notes](https://simonwillison.net/2020/Oct/9/datasette-0-50/).
From 822260fb30c9a6726a36975c9b8b26148bd66818 Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Sat, 10 Oct 2020 16:19:39 -0700
Subject: [PATCH 0004/1525] Improved homebrew instructions
---
docs/installation.rst | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/docs/installation.rst b/docs/installation.rst
index 1a45c594..dcae738a 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -27,12 +27,24 @@ Using Homebrew
If you have a Mac and use `Homebrew `__, you can install Datasette by running this command in your terminal::
- brew install simonw/datasette/datasette
+ brew install datasette
+
+This should install the latest version. You can confirm by running::
+
+ datasette --version
+
+You can upgrade to the latest Homebrew packaged version using::
+
+ brew upgrade datasette
Once you have installed Datasette you can install plugins using the following::
datasette install datasette-vega
+If the latest packaged release of Datasette has not yet been made available through Homebrew, you can upgrade your Homebrew installation in-place using::
+
+ datasette install -U datasette
+
.. _installation_pip:
Using pip
From 7e7064385270dda09dc2aa396d290369a667a03f Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Sat, 10 Oct 2020 16:39:38 -0700
Subject: [PATCH 0005/1525] Removed --debug option, which didn't do anything -
closes #814
---
README.md | 1 -
datasette/cli.py | 6 +-----
docs/changelog.rst | 1 -
docs/datasette-serve-help.txt | 1 -
tests/test_cli.py | 1 -
5 files changed, 1 insertion(+), 9 deletions(-)
diff --git a/README.md b/README.md
index 66ddf803..8670936c 100644
--- a/README.md
+++ b/README.md
@@ -130,7 +130,6 @@ Now visiting http://localhost:8001/History/downloads will show you a web interfa
allowed. Use 0.0.0.0 to listen to all IPs and
allow access from other machines.
-p, --port INTEGER Port for server, defaults to 8001
- --debug Enable debug mode - useful for development
--reload Automatically reload if database or code change
detected - useful for development
--cors Enable CORS by serving Access-Control-Allow-
diff --git a/datasette/cli.py b/datasette/cli.py
index 43e03f0a..55576013 100644
--- a/datasette/cli.py
+++ b/datasette/cli.py
@@ -286,9 +286,6 @@ def uninstall(packages, yes):
default=8001,
help="Port for server, defaults to 8001. Use -p 0 to automatically assign an available port.",
)
-@click.option(
- "--debug", is_flag=True, help="Enable debug mode - useful for development"
-)
@click.option(
"--reload",
is_flag=True,
@@ -366,7 +363,6 @@ def serve(
immutable,
host,
port,
- debug,
reload,
cors,
sqlite_extensions,
@@ -417,7 +413,7 @@ def serve(
kwargs = dict(
immutables=immutable,
- cache_headers=not debug and not reload,
+ cache_headers=not reload,
cors=cors,
inspect_data=inspect_data,
metadata=metadata_data,
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 1d654485..3c56328c 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -11,7 +11,6 @@ Changelog
- Fixed another bug introduced in 0.50 where column header links on the table page were broken. (`#1011 `__)
-
.. _v0_50_1:
0.50.1 (2020-10-09)
diff --git a/docs/datasette-serve-help.txt b/docs/datasette-serve-help.txt
index ac3ca49f..0457a321 100644
--- a/docs/datasette-serve-help.txt
+++ b/docs/datasette-serve-help.txt
@@ -14,7 +14,6 @@ Options:
-p, --port INTEGER Port for server, defaults to 8001. Use -p 0 to automatically
assign an available port.
- --debug Enable debug mode - useful for development
--reload Automatically reload if database or code change detected -
useful for development
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 09864602..0e1745c2 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -89,7 +89,6 @@ def test_metadata_yaml():
immutable=[],
host="127.0.0.1",
port=8001,
- debug=False,
reload=False,
cors=False,
sqlite_extensions=[],
From e34e84901d084ba3aaccecea020c5f9811865c8f Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Sat, 10 Oct 2020 17:18:45 -0700
Subject: [PATCH 0006/1525] Link: HTTP header pagination, closes #1014
---
datasette/renderer.py | 12 +++++++++++-
docs/json_api.rst | 32 ++++++++++++++++++++++++++++++++
tests/test_api.py | 28 ++++++++++++++++++++++++++++
3 files changed, 71 insertions(+), 1 deletion(-)
diff --git a/datasette/renderer.py b/datasette/renderer.py
index 27a5092f..bcde8516 100644
--- a/datasette/renderer.py
+++ b/datasette/renderer.py
@@ -5,6 +5,7 @@ from datasette.utils import (
CustomJSONEncoder,
path_from_row_pks,
)
+from datasette.utils.asgi import Response
def convert_specific_columns_to_json(rows, columns, json_cols):
@@ -44,6 +45,9 @@ def json_renderer(args, data, view_name):
# Deal with the _shape option
shape = args.get("_shape", "arrays")
+
+ next_url = data.get("next_url")
+
if shape == "arrayfirst":
data = [row[0] for row in data["rows"]]
elif shape in ("objects", "object", "array"):
@@ -71,6 +75,7 @@ def json_renderer(args, data, view_name):
data = {"ok": False, "error": error}
elif shape == "array":
data = data["rows"]
+
elif shape == "arrays":
pass
else:
@@ -89,4 +94,9 @@ def json_renderer(args, data, view_name):
else:
body = json.dumps(data, cls=CustomJSONEncoder)
content_type = "application/json; charset=utf-8"
- return {"body": body, "status_code": status_code, "content_type": content_type}
+ headers = {}
+ if next_url:
+ headers["link"] = '<{}>; rel="next"'.format(next_url)
+ return Response(
+ body, status=status_code, headers=headers, content_type=content_type
+ )
diff --git a/docs/json_api.rst b/docs/json_api.rst
index af98eecd..8d45ac6f 100644
--- a/docs/json_api.rst
+++ b/docs/json_api.rst
@@ -1,3 +1,5 @@
+.. _json_api:
+
JSON API
========
@@ -18,6 +20,8 @@ requests to fetch the data.
If you start Datasette without the ``--cors`` option only JavaScript running on
the same domain as Datasette will be able to access the API.
+.. _json_api_shapes:
+
Different shapes
----------------
@@ -138,6 +142,34 @@ this format.
The ``object`` keys are always strings. If your table has a compound primary
key, the ``object`` keys will be a comma-separated string.
+.. _json_api_pagination:
+
+Pagination
+----------
+
+The default JSON representation includes a ``"next_url"`` key which can be used to access the next page of results. If that key is null or missing then it means you have reached the final page of results.
+
+Other representations include pagination information in the ``link`` HTTP header. That header will look something like this::
+
+ link: ; rel="next"
+
+Here is an example Python function built using `requests `__ that returns a list of all of the paginated items from one of these API endpoints:
+
+.. code-block:: python
+
+ def paginate(url):
+ items = []
+ while url:
+ response = requests.get(url)
+ try:
+ url = response.links.get("next").get("url")
+ except AttributeError:
+ url = None
+ items.extend(response.json())
+ return items
+
+.. _json_api_special:
+
Special JSON arguments
----------------------
diff --git a/tests/test_api.py b/tests/test_api.py
index 4aa9811c..1d454ea1 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1828,3 +1828,31 @@ def test_binary_data_in_json(app_client, path, expected_json, expected_text):
assert response.json == expected_json
else:
assert response.text == expected_text
+
+
+@pytest.mark.parametrize(
+ "qs",
+ [
+ "",
+ "?_shape=arrays",
+ "?_shape=arrayfirst",
+ "?_shape=object",
+ "?_shape=objects",
+ "?_shape=array",
+ "?_shape=array&_nl=on",
+ ],
+)
+def test_paginate_using_link_header(app_client, qs):
+ path = "/fixtures/compound_three_primary_keys.json{}".format(qs)
+ num_pages = 0
+ while path:
+ response = app_client.get(path)
+ num_pages += 1
+ link = response.headers.get("link")
+ if link:
+ assert link.startswith("<")
+ assert link.endswith('>; rel="next"')
+ path = link[1:].split(">")[0]
+ else:
+ path = None
+ assert num_pages == 21
From acf07a67722aa74828744726187690b59d342494 Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Sun, 11 Oct 2020 19:53:26 -0700
Subject: [PATCH 0007/1525] x button for clearing filters, refs #1016
---
datasette/static/table.js | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/datasette/static/table.js b/datasette/static/table.js
index 7e839b9c..08c560d6 100644
--- a/datasette/static/table.js
+++ b/datasette/static/table.js
@@ -152,3 +152,33 @@ var DROPDOWN_ICON_SVG = `
diff --git a/datasette/views/database.py b/datasette/views/database.py
index c32ff92f..c06a6cea 100644
--- a/datasette/views/database.py
+++ b/datasette/views/database.py
@@ -2,7 +2,7 @@ import os
import itertools
import jinja2
import json
-from urllib.parse import parse_qsl
+from urllib.parse import parse_qsl, urlencode
from datasette.utils import (
check_visibility,
@@ -11,6 +11,7 @@ from datasette.utils import (
is_url,
path_with_added_args,
path_with_removed_args,
+ InvalidSql,
)
from datasette.utils.asgi import AsgiFileDownload, Response, Forbidden
from datasette.plugins import pm
@@ -301,6 +302,10 @@ class QueryView(DataView):
),
)
+ allow_execute_sql = await self.ds.permission_allowed(
+ request.actor, "execute-sql", database, default=True
+ )
+
async def extra_template():
display_rows = []
for row in results.rows:
@@ -329,12 +334,38 @@ class QueryView(DataView):
)
display_row.append(display_value)
display_rows.append(display_row)
+
+ # Show 'Edit SQL' button only if:
+ # - User is allowed to execute SQL
+ # - SQL is an approved SELECT statement
+ # - No magic parameters, so no :_ in the SQL string
+ edit_sql_url = None
+ is_validated_sql = False
+ try:
+ validate_sql_select(sql)
+ is_validated_sql = True
+ except InvalidSql:
+ pass
+ if allow_execute_sql and is_validated_sql and ":_" not in sql:
+ edit_sql_url = (
+ self.database_url(database)
+ + "?"
+ + urlencode(
+ {
+ **{
+ "sql": sql,
+ },
+ **named_parameter_values,
+ }
+ )
+ )
return {
"display_rows": display_rows,
"custom_sql": True,
"named_parameter_values": named_parameter_values,
"editable": editable,
"canned_query": canned_query,
+ "edit_sql_url": edit_sql_url,
"metadata": metadata,
"config": self.ds.config_dict(),
"request": request,
@@ -352,9 +383,7 @@ class QueryView(DataView):
"columns": columns,
"query": {"sql": sql, "params": params},
"private": private,
- "allow_execute_sql": await self.ds.permission_allowed(
- request.actor, "execute-sql", database, default=True
- ),
+ "allow_execute_sql": allow_execute_sql,
},
extra_template,
templates,
diff --git a/tests/test_html.py b/tests/test_html.py
index 5691b6c4..fb86d9d9 100644
--- a/tests/test_html.py
+++ b/tests/test_html.py
@@ -1403,3 +1403,48 @@ def test_base_url_config(base_url, path):
"href_or_src": href,
"element_parent": str(el.parent),
}
+
+
+@pytest.mark.parametrize(
+ "path,expected",
+ [
+ (
+ "/fixtures/neighborhood_search",
+ "/fixtures?sql=%0Aselect+neighborhood%2C+facet_cities.name%2C+state%0Afrom+facetable%0A++++join+facet_cities%0A++++++++on+facetable.city_id+%3D+facet_cities.id%0Awhere+neighborhood+like+%27%25%27+%7C%7C+%3Atext+%7C%7C+%27%25%27%0Aorder+by+neighborhood%3B%0A&text=",
+ ),
+ (
+ "/fixtures/neighborhood_search?text=ber",
+ "/fixtures?sql=%0Aselect+neighborhood%2C+facet_cities.name%2C+state%0Afrom+facetable%0A++++join+facet_cities%0A++++++++on+facetable.city_id+%3D+facet_cities.id%0Awhere+neighborhood+like+%27%25%27+%7C%7C+%3Atext+%7C%7C+%27%25%27%0Aorder+by+neighborhood%3B%0A&text=ber",
+ ),
+ ("/fixtures/pragma_cache_size", None),
+ (
+ "/fixtures/𝐜𝐢𝐭𝐢𝐞𝐬",
+ "/fixtures?sql=select+id%2C+name+from+facet_cities+order+by+id+limit+1%3B",
+ ),
+ ("/fixtures/magic_parameters", None),
+ ],
+)
+def test_edit_sql_link_on_canned_queries(app_client, path, expected):
+ response = app_client.get(path)
+ expected_link = 'Edit SQL'.format(
+ expected
+ )
+ if expected:
+ assert expected_link in response.text
+ else:
+ assert "Edit SQL" not in response.text
+
+
+@pytest.mark.parametrize("permission_allowed", [True, False])
+def test_edit_sql_link_not_shown_if_user_lacks_permission(permission_allowed):
+ with make_app_client(
+ metadata={
+ "allow_sql": None if permission_allowed else {"id": "not-you"},
+ "databases": {"fixtures": {"queries": {"simple": "select 1 + 1"}}},
+ }
+ ) as client:
+ response = client.get("/fixtures/simple")
+ if permission_allowed:
+ assert "Edit SQL" in response.text
+ else:
+ assert "Edit SQL" not in response.text
From b4a8e70957517ff44d6a9121422d266a3c5fd664 Mon Sep 17 00:00:00 2001
From: "dependabot-preview[bot]"
<27856297+dependabot-preview[bot]@users.noreply.github.com>
Date: Wed, 14 Oct 2020 14:51:34 -0700
Subject: [PATCH 0009/1525] Update asgiref requirement from ~=3.2.10 to
>=3.2.10,<3.4.0 (#1018)
Updates the requirements on [asgiref](https://github.com/django/asgiref) to permit the latest version.
- [Release notes](https://github.com/django/asgiref/releases)
- [Changelog](https://github.com/django/asgiref/blob/master/CHANGELOG.txt)
- [Commits](https://github.com/django/asgiref/compare/3.2.10...3.3.0)
Signed-off-by: dependabot-preview[bot]
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 22d164b0..a7c4fc1e 100644
--- a/setup.py
+++ b/setup.py
@@ -44,7 +44,7 @@ setup(
package_data={"datasette": ["templates/*.html"]},
include_package_data=True,
install_requires=[
- "asgiref~=3.2.10",
+ "asgiref>=3.2.10,<3.4.0",
"click~=7.1.1",
"click-default-group~=1.2.2",
"Jinja2>=2.10.3,<2.12.0",
From 7f2edb5dd2074dce0090659021991695a984844b Mon Sep 17 00:00:00 2001
From: "dependabot-preview[bot]"
<27856297+dependabot-preview[bot]@users.noreply.github.com>
Date: Wed, 14 Oct 2020 14:52:07 -0700
Subject: [PATCH 0010/1525] Update janus requirement from <0.6,>=0.4 to
>=0.4,<0.7 (#1017)
Updates the requirements on [janus](https://github.com/aio-libs/janus) to permit the latest version.
- [Release notes](https://github.com/aio-libs/janus/releases)
- [Changelog](https://github.com/aio-libs/janus/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/janus/compare/v0.4.0...v0.6.0)
Signed-off-by: dependabot-preview[bot]
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index a7c4fc1e..576e6a0a 100644
--- a/setup.py
+++ b/setup.py
@@ -54,7 +54,7 @@ setup(
"pluggy~=0.13.0",
"uvicorn~=0.11",
"aiofiles>=0.4,<0.6",
- "janus>=0.4,<0.6",
+ "janus>=0.4,<0.7",
"asgi-csrf>=0.6",
"PyYAML~=5.3",
"mergedeep>=1.1.1,<1.4.0",
From 4f7c0ebd85ccd8c1853d7aa0147628f7c1b749cc Mon Sep 17 00:00:00 2001
From: Jacob Fenton
Date: Wed, 14 Oct 2020 16:46:46 -0700
Subject: [PATCH 0011/1525] Fix table name in spatialite example command
(#1022)
The example query for creating a new point geometry seems to be using a table called 'museums' but at one point it instead uses 'events'. I *believe* it is intended to be museums.
---
docs/spatialite.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/spatialite.rst b/docs/spatialite.rst
index 58179e70..e71bf340 100644
--- a/docs/spatialite.rst
+++ b/docs/spatialite.rst
@@ -65,7 +65,7 @@ Here's a recipe for taking a table with existing latitude and longitude columns,
conn.execute("SELECT AddGeometryColumn('museums', 'point_geom', 4326, 'POINT', 2);")
# Now update that geometry column with the lat/lon points
conn.execute('''
- UPDATE events SET
+ UPDATE museums SET
point_geom = GeomFromText('POINT('||"longitude"||' '||"latitude"||')',4326);
''')
# Now add a spatial index to that column
From 568bd7bbf590861687db8c318f3d8cfcd1dfb47a Mon Sep 17 00:00:00 2001
From: Taylor Hodge
Date: Sat, 17 Oct 2020 16:05:03 -0400
Subject: [PATCH 0012/1525] Fix broken link in publish docs (#1029)
---
docs/publish.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/publish.rst b/docs/publish.rst
index f72b902c..45048ce1 100644
--- a/docs/publish.rst
+++ b/docs/publish.rst
@@ -52,7 +52,7 @@ Cloud Run provides a URL on the ``.run.app`` domain, but you can also point your
Publishing to Heroku
--------------------
-To publish your data using [Heroku](https://heroku.com/), first create an account there and install and configure the `Heroku CLI tool `_.
+To publish your data using `Heroku `__, first create an account there and install and configure the `Heroku CLI tool `_.
You can publish a database to Heroku using the following command::
From a0e9ae3c258c62221f8603e2944265f27ba07c14 Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Sun, 18 Oct 2020 11:20:33 -0700
Subject: [PATCH 0013/1525] Build extra formats with Read the Docs
---
.readthedocs.yml | 12 ++++++++++++
1 file changed, 12 insertions(+)
create mode 100644 .readthedocs.yml
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 00000000..bfb70bcc
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,12 @@
+# https://docs.readthedocs.io/en/stable/config-file/v2.html
+
+version: 2
+
+sphinx:
+ configuration: docs/conf.py
+
+formats:
+ - pdf
+ - epub
+ - htmlzip
+ - xml
From b0b04bb7c185bed9bdbb9a3f3f24f264999296b6 Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Sun, 18 Oct 2020 11:37:35 -0700
Subject: [PATCH 0014/1525] Delete .readthedocs.yml
It worked fine without configuration, and my attempt to build the xml version failed with an error message:
Problem in your project's configuration. Invalid "formats": expected one of (htmlzip, pdf, epub), got xml
---
.readthedocs.yml | 12 ------------
1 file changed, 12 deletions(-)
delete mode 100644 .readthedocs.yml
diff --git a/.readthedocs.yml b/.readthedocs.yml
deleted file mode 100644
index bfb70bcc..00000000
--- a/.readthedocs.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-# https://docs.readthedocs.io/en/stable/config-file/v2.html
-
-version: 2
-
-sphinx:
- configuration: docs/conf.py
-
-formats:
- - pdf
- - epub
- - htmlzip
- - xml
From f7147260a451896b27f466ebcd6ac648273650f0 Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Sun, 18 Oct 2020 13:56:35 -0700
Subject: [PATCH 0015/1525] Added datasette-atom and datasette-ics
---
docs/ecosystem.rst | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst
index 765f4532..4b80e71e 100644
--- a/docs/ecosystem.rst
+++ b/docs/ecosystem.rst
@@ -112,6 +112,16 @@ datasette-json-html
`datasette-json-html `__ renders HTML in Datasette's table view driven by JSON returned from your SQL queries. This provides a way to embed images, links and lists of links directly in Datasette's main interface, defined using custom SQL statements.
+datasette-atom
+--------------
+
+`datasette-atom `__ can output Datasette query results as Atom feeds, suitable for subscribing to using a feed reader application.
+
+datasette-ics
+-------------
+
+`datasette-ics `__ can output query results as an iCalendar feed, suitable for subscribing to from calendar software such as Google Calendar or Apple Calendar.
+
datasette-init
--------------
From c37a0a93ecb847e66cfe7b6f9452ba210fcae91b Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Sun, 18 Oct 2020 14:35:26 -0700
Subject: [PATCH 0016/1525] Build and deploy docs.db to datasette-docs-latest
---
.github/workflows/deploy-latest.yml | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/.github/workflows/deploy-latest.yml b/.github/workflows/deploy-latest.yml
index 55aabb76..10130253 100644
--- a/.github/workflows/deploy-latest.yml
+++ b/.github/workflows/deploy-latest.yml
@@ -26,10 +26,18 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install -e .[test]
+ python -m pip install -e .[docs]
+ python -m pip install sphinx-to-sqlite
- name: Run tests
run: pytest
- name: Build fixtures.db
run: python tests/fixtures.py fixtures.db fixtures.json
+ - name: Build docs.db
+ run: |-
+ cd docs
+ sphinx-build -b xml . _build
+ sphinx-to-sqlite ../docs.db _build
+ cd ..
- name: Set up Cloud Run
uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
with:
@@ -46,3 +54,9 @@ jobs:
--version-note=$GITHUB_SHA \
--extra-options="--config template_debug:1" \
--service=datasette-latest
+ # Deploy docs.db to a different service
+ datasette publish cloudrun docs.db \
+ --branch=$GITHUB_SHA \
+ --version-note=$GITHUB_SHA \
+ --extra-options="--config template_debug:1" \
+ --service=datasette-docs-latest
From a4def0b8dba68fcaf1d52013212f9e2b93371fbe Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Thu, 15 Oct 2020 11:56:59 -0700
Subject: [PATCH 0017/1525] Clearer _sort_by_desc comment
---
datasette/views/table.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/datasette/views/table.py b/datasette/views/table.py
index 1bdb911e..ea11a51d 100644
--- a/datasette/views/table.py
+++ b/datasette/views/table.py
@@ -360,7 +360,7 @@ class TableView(RowTableShared):
forward_querystring=False,
)
- # Spot ?_sort_by_desc and redirect to _sort_desc=(_sort)
+ # If ?_sort_by_desc=on (from checkbox) redirect to _sort_desc=(_sort)
if "_sort_by_desc" in special_args:
return self.redirect(
request,
From 6aa5886379dd9017215904fb28567b80018902f9 Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Mon, 19 Oct 2020 15:37:31 -0700
Subject: [PATCH 0018/1525] --load-extension=spatialite shortcut, closes #1028
---
datasette/app.py | 11 ++++++++++-
datasette/cli.py | 8 +++++---
datasette/utils/__init__.py | 18 ++++++++++++++++++
docs/installation.rst | 2 +-
docs/spatialite.rst | 10 +++++++++-
tests/test_cli.py | 11 +++++++++++
6 files changed, 54 insertions(+), 6 deletions(-)
diff --git a/datasette/app.py b/datasette/app.py
index e6ece8ad..b768a298 100644
--- a/datasette/app.py
+++ b/datasette/app.py
@@ -49,12 +49,14 @@ from .utils import (
display_actor,
escape_css_string,
escape_sqlite,
+ find_spatialite,
format_bytes,
module_from_path,
parse_metadata,
resolve_env_secrets,
sqlite3,
to_css_class,
+ SpatialiteNotFound,
)
from .utils.asgi import (
AsgiLifespan,
@@ -242,7 +244,14 @@ class Datasette:
metadata = parse_metadata(fp.read())
self._metadata = metadata or {}
self.sqlite_functions = []
- self.sqlite_extensions = sqlite_extensions or []
+ self.sqlite_extensions = []
+ for extension in sqlite_extensions or []:
+ # Resolve spatialite, if requested
+ if extension == "spatialite":
+ # Could raise SpatialiteNotFound
+ self.sqlite_extensions.append(find_spatialite())
+ else:
+ self.sqlite_extensions.append(extension)
if config_dir and (config_dir / "templates").is_dir() and not template_dir:
template_dir = str((config_dir / "templates").resolve())
self.template_dir = template_dir
diff --git a/datasette/cli.py b/datasette/cli.py
index 55576013..ab24cb12 100644
--- a/datasette/cli.py
+++ b/datasette/cli.py
@@ -19,6 +19,7 @@ from .utils import (
SpatialiteConnectionProblem,
temporary_docker_directory,
value_as_boolean,
+ SpatialiteNotFound,
StaticMount,
ValueAsBooleanError,
)
@@ -78,7 +79,6 @@ def cli():
"--load-extension",
envvar="SQLITE_EXTENSIONS",
multiple=True,
- type=click.Path(exists=True, resolve_path=True),
help="Path to a SQLite extension to load",
)
def inspect(files, inspect_file, sqlite_extensions):
@@ -299,7 +299,6 @@ def uninstall(packages, yes):
"--load-extension",
envvar="SQLITE_EXTENSIONS",
multiple=True,
- type=click.Path(exists=True, resolve_path=True),
help="Path to a SQLite extension to load",
)
@click.option(
@@ -433,7 +432,10 @@ def serve(
kwargs["config_dir"] = pathlib.Path(files[0])
files = []
- ds = Datasette(files, **kwargs)
+ try:
+ ds = Datasette(files, **kwargs)
+ except SpatialiteNotFound:
+ raise click.ClickException("Could not find SpatiaLite extension")
if return_instance:
# Private utility mechanism for writing unit tests
diff --git a/datasette/utils/__init__.py b/datasette/utils/__init__.py
index 7b8918a5..7026bdf0 100644
--- a/datasette/utils/__init__.py
+++ b/datasette/utils/__init__.py
@@ -54,6 +54,12 @@ RUN apt-get update && \
ENV SQLITE_EXTENSIONS /usr/lib/x86_64-linux-gnu/mod_spatialite.so
"""
+# Can replace with sqlite-utils when I add that dependency
+SPATIALITE_PATHS = (
+ "/usr/lib/x86_64-linux-gnu/mod_spatialite.so",
+ "/usr/local/lib/mod_spatialite.dylib",
+)
+
# Can replace this with Column from sqlite_utils when I add that dependency
Column = namedtuple(
"Column", ("cid", "name", "type", "notnull", "default_value", "is_pk")
@@ -971,3 +977,15 @@ def display_actor(actor):
if actor.get(key):
return actor[key]
return str(actor)
+
+
+class SpatialiteNotFound(Exception):
+ pass
+
+
+# Can replace with sqlite-utils when I add that dependency
+def find_spatialite():
+ for path in SPATIALITE_PATHS:
+ if os.path.exists(path):
+ return path
+ raise SpatialiteNotFound
diff --git a/docs/installation.rst b/docs/installation.rst
index dcae738a..6ac67f59 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -170,7 +170,7 @@ module, use the following command::
docker run -p 8001:8001 -v `pwd`:/mnt \
datasetteproject/datasette \
datasette -p 8001 -h 0.0.0.0 /mnt/fixtures.db \
- --load-extension=/usr/local/lib/mod_spatialite.so
+ --load-extension=spatialite
You can confirm that SpatiaLite is successfully loaded by visiting
http://127.0.0.1:8001/-/versions
diff --git a/docs/spatialite.rst b/docs/spatialite.rst
index e71bf340..05c5c667 100644
--- a/docs/spatialite.rst
+++ b/docs/spatialite.rst
@@ -8,6 +8,14 @@ The `SpatiaLite module `_ fo
To use it with Datasette, you need to install the ``mod_spatialite`` dynamic library. This can then be loaded into Datasette using the ``--load-extension`` command-line option.
+Datasette can look for SpatiaLite in common installation locations if you run it like this::
+
+ datasette --load-extension=spatialite
+
+If SpatiaLite is in another location, use the full path to the extension instead::
+
+ datasette --load-extension=/usr/local/lib/mod_spatialite.dylib
+
Installation
============
@@ -25,7 +33,7 @@ This will install the ``spatialite`` command-line tool and the ``mod_spatialite`
You can now run Datasette like so::
- datasette --load-extension=/usr/local/lib/mod_spatialite.dylib
+ datasette --load-extension=spatialite
Installing SpatiaLite on Linux
------------------------------
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 0e1745c2..76f94aa1 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -59,6 +59,17 @@ def test_spatialite_error_if_attempt_to_open_spatialite():
assert "trying to load a SpatiaLite database" in result.output
+@mock.patch("datasette.utils.SPATIALITE_PATHS", ["/does/not/exist"])
+def test_spatialite_error_if_cannot_find_load_extension_spatialite():
+ runner = CliRunner()
+ result = runner.invoke(
+ cli, ["serve", str(pathlib.Path(__file__).parent / "spatialite.db"),
+ "--load-extension", "spatialite"]
+ )
+ assert result.exit_code != 0
+ assert "Could not find SpatiaLite extension" in result.output
+
+
def test_plugins_cli(app_client):
runner = CliRunner()
result1 = runner.invoke(cli, ["plugins"])
From c440ffc65a3e20b272ec0cc34e53f1000369379c Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Mon, 19 Oct 2020 17:33:04 -0700
Subject: [PATCH 0019/1525] Updated serve help, refs #1028
---
docs/datasette-serve-help.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/datasette-serve-help.txt b/docs/datasette-serve-help.txt
index 0457a321..5a63d4c4 100644
--- a/docs/datasette-serve-help.txt
+++ b/docs/datasette-serve-help.txt
@@ -18,7 +18,7 @@ Options:
useful for development
--cors Enable CORS by serving Access-Control-Allow-Origin: *
- --load-extension PATH Path to a SQLite extension to load
+ --load-extension TEXT Path to a SQLite extension to load
--inspect-file TEXT Path to JSON file created using "datasette inspect"
-m, --metadata FILENAME Path to JSON/YAML file containing license/source metadata
--template-dir DIRECTORY Path to directory containing custom templates
From 310c3a3e059b89d05a38e373744928c1b54e54db Mon Sep 17 00:00:00 2001
From: Simon Willison
Date: Mon, 19 Oct 2020 17:33:59 -0700
Subject: [PATCH 0020/1525] New datasette.urls URL builders, refs #904
---
datasette/app.py | 32 ++++++++++++++++++++++++++++++-
datasette/templates/database.html | 14 +++++++-------
datasette/templates/index.html | 5 ++---
datasette/templates/query.html | 6 +++---
datasette/templates/row.html | 8 ++++----
datasette/templates/table.html | 10 +++++-----
datasette/utils/__init__.py | 2 ++
datasette/views/base.py | 18 +----------------
datasette/views/database.py | 2 +-
datasette/views/index.py | 2 +-
tests/test_cli.py | 9 +++++++--
11 files changed, 64 insertions(+), 44 deletions(-)
diff --git a/datasette/app.py b/datasette/app.py
index b768a298..b8e27f3c 100644
--- a/datasette/app.py
+++ b/datasette/app.py
@@ -56,7 +56,7 @@ from .utils import (
resolve_env_secrets,
sqlite3,
to_css_class,
- SpatialiteNotFound,
+ HASH_LENGTH,
)
from .utils.asgi import (
AsgiLifespan,
@@ -321,6 +321,10 @@ class Datasette:
self._root_token = secrets.token_hex(32)
self.client = DatasetteClient(self)
+ @property
+ def urls(self):
+ return Urls(self)
+
async def invoke_startup(self):
for hook in pm.hook.startup(datasette=self):
await await_me_maybe(hook)
@@ -748,6 +752,7 @@ class Datasette:
template_context = {
**context,
**{
+ "urls": self.urls,
"actor": request.actor if request else None,
"display_actor": display_actor,
"show_logout": request is not None and "ds_actor" in request.cookies,
@@ -1259,3 +1264,28 @@ class DatasetteClient:
async def request(self, method, path, **kwargs):
async with httpx.AsyncClient(app=self.app) as client:
return await client.request(method, self._fix(path), **kwargs)
+
+
+class Urls:
+ def __init__(self, ds):
+ self.ds = ds
+
+ def instance(self):
+ return self.ds.config("base_url")
+
+ def static(self, path):
+ return "{}-/static/{}".format(self.instance(), path)
+
+ def database(self, database):
+ db = self.ds.databases[database]
+ base_url = self.ds.config("base_url")
+ if self.ds.config("hash_urls") and db.hash:
+ return "{}{}-{}".format(base_url, database, db.hash[:HASH_LENGTH])
+ else:
+ return "{}{}".format(base_url, database)
+
+ def table(self, database, table):
+ return "{}/{}".format(self.database(database), urllib.parse.quote_plus(table))
+
+ def query(self, database, query):
+ return "{}/{}".format(self.database(database), urllib.parse.quote_plus(query))
diff --git a/datasette/templates/database.html b/datasette/templates/database.html
index 5ae51ef7..2f844b6a 100644
--- a/datasette/templates/database.html
+++ b/datasette/templates/database.html
@@ -11,7 +11,7 @@
{% block nav %}
- home
+ home
{{ super() }}
{% endblock %}
@@ -23,7 +23,7 @@
{% block description_source_license %}{% include "_description_source_license.html" %}{% endblock %}
{% if allow_execute_sql %}
-