diff --git a/datasette/publish/heroku.py b/datasette/publish/heroku.py index 171252ce..f576a346 100644 --- a/datasette/publish/heroku.py +++ b/datasette/publish/heroku.py @@ -3,7 +3,9 @@ from datasette import hookimpl import click import json import os +import pathlib import shlex +import shutil from subprocess import call, check_output import tempfile @@ -28,6 +30,11 @@ def publish_subcommand(publish): "--tar", help="--tar option to pass to Heroku, e.g. --tar=/usr/local/bin/gtar", ) + @click.option( + "--generate-dir", + type=click.Path(dir_okay=True, file_okay=False), + help="Output generated application files and stop without deploying", + ) def heroku( files, metadata, @@ -49,6 +56,7 @@ def publish_subcommand(publish): about_url, name, tar, + generate_dir, ): "Publish databases to Datasette running on Heroku" fail_if_publish_binary_not_installed( @@ -105,6 +113,16 @@ def publish_subcommand(publish): secret, extra_metadata, ): + if generate_dir: + # Recursively copy files from current working directory to it + if pathlib.Path(generate_dir).exists(): + raise click.ClickException("Directory already exists") + shutil.copytree(".", generate_dir) + click.echo( + f"Generated files written to {generate_dir}, stopping without deploying", + err=True, + ) + return app_name = None if name: # Check to see if this app already exists @@ -176,7 +194,7 @@ def temporary_heroku_directory( fp.write(json.dumps(metadata_content, indent=2)) with open("runtime.txt", "w") as fp: - fp.write("python-3.8.10") + fp.write("python-3.11.0") if branch: install = [ diff --git a/datasette/templates/_codemirror_foot.html b/datasette/templates/_codemirror_foot.html index 0c9255ab..a624c8a4 100644 --- a/datasette/templates/_codemirror_foot.html +++ b/datasette/templates/_codemirror_foot.html @@ -5,7 +5,7 @@ const schema = {}; {% endif %} - window.onload = () => { + window.addEventListener("DOMContentLoaded", () => { const sqlFormat = document.querySelector("button#sql-format"); const readOnly = document.querySelector("pre#sql-query"); const sqlInput = document.querySelector("textarea#sql-editor"); @@ -38,5 +38,5 @@ }); } } - }; + }); diff --git a/datasette/views/database.py b/datasette/views/database.py index 9e46ec3e..0d03d1f9 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -682,22 +682,6 @@ class TableCreateView(BaseView): return Response.json(details, status=201) -async def _table_columns(datasette, database_name): - internal = datasette.get_database("_internal") - result = await internal.execute( - "select table_name, name from columns where database_name = ?", - [database_name], - ) - table_columns = {} - for row in result.rows: - table_columns.setdefault(row["table_name"], []).append(row["name"]) - # Add views - db = datasette.get_database(database_name) - for view_name in await db.view_names(): - table_columns[view_name] = [] - return table_columns - - class TableCreateView(BaseView): name = "table-create" @@ -831,3 +815,19 @@ class TableCreateView(BaseView): if rows: details["row_count"] = len(rows) return Response.json(details, status=201) + + +async def _table_columns(datasette, database_name): + internal = datasette.get_database("_internal") + result = await internal.execute( + "select table_name, name from columns where database_name = ?", + [database_name], + ) + table_columns = {} + for row in result.rows: + table_columns.setdefault(row["table_name"], []).append(row["name"]) + # Add views + db = datasette.get_database(database_name) + for view_name in await db.view_names(): + table_columns[view_name] = [] + return table_columns diff --git a/docs/changelog.rst b/docs/changelog.rst index aaa2600f..7d228580 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -33,6 +33,13 @@ Write API - ``/db/table/pk/-/update`` API for :ref:`RowUpdateView`. (:issue:`1863`) - ``/db/table/pk/-/delete`` API for :ref:`RowDeleteView`. (:issue:`1864`) +.. _v0_63_2: + +0.63.2 (2022-11-18) +------------------- + +- Fixed a bug in ``datasette publish heroku`` where deployments failed due to an older version of Python being requested. (:issue:`1905`) +- New ``datasette publish heroku --generate-dir `` option for generating a Heroku deployment directory without deploying it. .. _v0_63_1: diff --git a/docs/cli-reference.rst b/docs/cli-reference.rst index 9263223d..4633c73e 100644 --- a/docs/cli-reference.rst +++ b/docs/cli-reference.rst @@ -508,6 +508,8 @@ See :ref:`publish_heroku`. -n, --name TEXT Application name to use when deploying --tar TEXT --tar option to pass to Heroku, e.g. --tar=/usr/local/bin/gtar + --generate-dir DIRECTORY Output generated application files and stop + without deploying --help Show this message and exit. diff --git a/docs/publish.rst b/docs/publish.rst index 4ba94792..7ae0399e 100644 --- a/docs/publish.rst +++ b/docs/publish.rst @@ -73,6 +73,10 @@ This will output some details about the new deployment, including a URL like thi You can specify a custom app name by passing ``-n my-app-name`` to the publish command. This will also allow you to overwrite an existing app. +Rather than deploying directly you can use the ``--generate-dir`` option to output the files that would be deployed to a directory:: + + datasette publish heroku mydatabase.db --generate-dir=/tmp/deploy-this-to-heroku + See :ref:`cli_help_publish_heroku___help` for the full list of options for this command. .. _publish_vercel: diff --git a/tests/test_publish_heroku.py b/tests/test_publish_heroku.py index b5a8af73..cab83654 100644 --- a/tests/test_publish_heroku.py +++ b/tests/test_publish_heroku.py @@ -2,6 +2,7 @@ from click.testing import CliRunner from datasette import cli from unittest import mock import os +import pathlib import pytest @@ -128,3 +129,55 @@ def test_publish_heroku_plugin_secrets( mock.call(["heroku", "builds:create", "-a", "f", "--include-vcs-ignore"]), ] ) + + +@pytest.mark.serial +@mock.patch("shutil.which") +@mock.patch("datasette.publish.heroku.check_output") +@mock.patch("datasette.publish.heroku.call") +def test_publish_heroku_generate_dir( + mock_call, mock_check_output, mock_which, tmp_path_factory +): + mock_which.return_value = True + mock_check_output.side_effect = lambda s: { + "['heroku', 'plugins']": b"heroku-builds", + }[repr(s)] + runner = CliRunner() + os.chdir(tmp_path_factory.mktemp("runner")) + with open("test.db", "w") as fp: + fp.write("data") + output = str(tmp_path_factory.mktemp("generate_dir") / "output") + result = runner.invoke( + cli.cli, + [ + "publish", + "heroku", + "test.db", + "--generate-dir", + output, + ], + ) + assert result.exit_code == 0 + path = pathlib.Path(output) + assert path.exists() + file_names = {str(r.relative_to(path)) for r in path.glob("*")} + assert file_names == { + "requirements.txt", + "bin", + "runtime.txt", + "Procfile", + "test.db", + } + for name, expected in ( + ("requirements.txt", "datasette"), + ("runtime.txt", "python-3.11.0"), + ( + "Procfile", + ( + "web: datasette serve --host 0.0.0.0 -i test.db " + "--cors --port $PORT --inspect-file inspect-data.json" + ), + ), + ): + with open(path / name) as fp: + assert fp.read().strip() == expected