2019-05-11 21:36:57 +00:00
|
|
|
import asyncio
|
2019-06-24 03:13:09 +00:00
|
|
|
import uvicorn
|
2017-10-27 07:08:24 +00:00
|
|
|
import click
|
2018-05-20 17:01:49 +00:00
|
|
|
from click import formatting
|
2017-11-04 23:53:50 +00:00
|
|
|
from click_default_group import DefaultGroup
|
2017-11-11 20:10:51 +00:00
|
|
|
import json
|
2017-12-03 16:33:36 +00:00
|
|
|
import os
|
2020-04-27 16:30:24 +00:00
|
|
|
import pathlib
|
2017-11-11 16:00:00 +00:00
|
|
|
import shutil
|
publish_subcommand hook + default plugins mechanism, used for publish heroku/now (#349)
This change introduces a new plugin hook, publish_subcommand, which can be
used to implement new subcommands for the "datasette publish" command family.
I've used this new hook to refactor out the "publish now" and "publish heroku"
implementations into separate modules. I've also added unit tests for these
two publishers, mocking the subprocess.call and subprocess.check_output
functions.
As part of this, I introduced a mechanism for loading default plugins. These
are defined in the new "default_plugins" list inside datasette/app.py
Closes #217 (Plugin support for datasette publish)
Closes #348 (Unit tests for "datasette publish")
Refs #14, #59, #102, #103, #146, #236, #347
2018-07-26 05:15:59 +00:00
|
|
|
from subprocess import call
|
2017-11-11 16:00:00 +00:00
|
|
|
import sys
|
2020-08-11 23:54:52 +00:00
|
|
|
from runpy import run_module
|
2020-09-22 14:26:47 +00:00
|
|
|
import webbrowser
|
publish_subcommand hook + default plugins mechanism, used for publish heroku/now (#349)
This change introduces a new plugin hook, publish_subcommand, which can be
used to implement new subcommands for the "datasette publish" command family.
I've used this new hook to refactor out the "publish now" and "publish heroku"
implementations into separate modules. I've also added unit tests for these
two publishers, mocking the subprocess.call and subprocess.check_output
functions.
As part of this, I introduced a mechanism for loading default plugins. These
are defined in the new "default_plugins" list inside datasette/app.py
Closes #217 (Plugin support for datasette publish)
Closes #348 (Unit tests for "datasette publish")
Refs #14, #59, #102, #103, #146, #236, #347
2018-07-26 05:15:59 +00:00
|
|
|
from .app import Datasette, DEFAULT_CONFIG, CONFIG_OPTIONS, pm
|
2018-06-16 16:44:31 +00:00
|
|
|
from .utils import (
|
2020-02-15 17:56:48 +00:00
|
|
|
check_connection,
|
2020-04-02 19:30:53 +00:00
|
|
|
parse_metadata,
|
2020-02-15 17:56:48 +00:00
|
|
|
ConnectionProblem,
|
|
|
|
SpatialiteConnectionProblem,
|
2018-06-16 16:44:31 +00:00
|
|
|
temporary_docker_directory,
|
|
|
|
value_as_boolean,
|
publish_subcommand hook + default plugins mechanism, used for publish heroku/now (#349)
This change introduces a new plugin hook, publish_subcommand, which can be
used to implement new subcommands for the "datasette publish" command family.
I've used this new hook to refactor out the "publish now" and "publish heroku"
implementations into separate modules. I've also added unit tests for these
two publishers, mocking the subprocess.call and subprocess.check_output
functions.
As part of this, I introduced a mechanism for loading default plugins. These
are defined in the new "default_plugins" list inside datasette/app.py
Closes #217 (Plugin support for datasette publish)
Closes #348 (Unit tests for "datasette publish")
Refs #14, #59, #102, #103, #146, #236, #347
2018-07-26 05:15:59 +00:00
|
|
|
StaticMount,
|
2018-06-16 16:44:31 +00:00
|
|
|
ValueAsBooleanError,
|
|
|
|
)
|
2020-08-12 00:24:40 +00:00
|
|
|
from .utils.testing import TestClient
|
2017-10-27 07:08:24 +00:00
|
|
|
|
2017-11-04 23:53:50 +00:00
|
|
|
|
2018-05-20 17:01:49 +00:00
|
|
|
class Config(click.ParamType):
|
|
|
|
name = "config"
|
2018-05-18 05:08:26 +00:00
|
|
|
|
2018-05-25 01:12:27 +00:00
|
|
|
def convert(self, config, param, ctx):
|
|
|
|
if ":" not in config:
|
2019-05-04 02:15:14 +00:00
|
|
|
self.fail('"{}" should be name:value'.format(config), param, ctx)
|
2018-05-25 01:12:27 +00:00
|
|
|
return
|
2020-03-25 00:18:43 +00:00
|
|
|
name, value = config.split(":", 1)
|
2018-05-20 17:01:49 +00:00
|
|
|
if name not in DEFAULT_CONFIG:
|
2018-06-16 16:44:31 +00:00
|
|
|
self.fail(
|
2019-05-04 02:15:14 +00:00
|
|
|
"{} is not a valid option (--help-config to see all)".format(name),
|
|
|
|
param,
|
|
|
|
ctx,
|
2018-06-16 16:44:31 +00:00
|
|
|
)
|
2018-05-25 01:12:27 +00:00
|
|
|
return
|
|
|
|
# Type checking
|
|
|
|
default = DEFAULT_CONFIG[name]
|
|
|
|
if isinstance(default, bool):
|
2018-06-16 16:44:31 +00:00
|
|
|
try:
|
|
|
|
return name, value_as_boolean(value)
|
|
|
|
except ValueAsBooleanError:
|
2018-05-25 01:12:27 +00:00
|
|
|
self.fail(
|
2018-06-16 16:44:31 +00:00
|
|
|
'"{}" should be on/off/true/false/1/0'.format(name), param, ctx
|
2018-05-25 01:12:27 +00:00
|
|
|
)
|
|
|
|
return
|
|
|
|
elif isinstance(default, int):
|
|
|
|
if not value.isdigit():
|
2019-05-04 02:15:14 +00:00
|
|
|
self.fail('"{}" should be an integer'.format(name), param, ctx)
|
2018-05-25 01:12:27 +00:00
|
|
|
return
|
|
|
|
return name, int(value)
|
2020-03-25 00:18:43 +00:00
|
|
|
elif isinstance(default, str):
|
|
|
|
return name, value
|
2018-05-25 01:12:27 +00:00
|
|
|
else:
|
|
|
|
# Should never happen:
|
2019-05-04 02:15:14 +00:00
|
|
|
self.fail("Invalid option")
|
2018-05-18 05:08:26 +00:00
|
|
|
|
|
|
|
|
2018-04-18 14:14:21 +00:00
|
|
|
@click.group(cls=DefaultGroup, default="serve", default_if_no_args=True)
|
2017-11-19 05:59:16 +00:00
|
|
|
@click.version_option()
|
2017-10-27 07:08:24 +00:00
|
|
|
def cli():
|
|
|
|
"""
|
2017-11-10 18:38:35 +00:00
|
|
|
Datasette!
|
2017-10-27 07:08:24 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
2018-04-18 14:14:21 +00:00
|
|
|
@click.argument("files", type=click.Path(exists=True), nargs=-1)
|
2019-05-11 21:36:57 +00:00
|
|
|
@click.option("--inspect-file", default="-")
|
2017-11-26 23:01:53 +00:00
|
|
|
@click.option(
|
2018-04-18 14:14:21 +00:00
|
|
|
"sqlite_extensions",
|
|
|
|
"--load-extension",
|
|
|
|
envvar="SQLITE_EXTENSIONS",
|
|
|
|
multiple=True,
|
|
|
|
type=click.Path(exists=True, resolve_path=True),
|
|
|
|
help="Path to a SQLite extension to load",
|
2017-11-26 23:01:53 +00:00
|
|
|
)
|
2017-12-07 16:57:31 +00:00
|
|
|
def inspect(files, inspect_file, sqlite_extensions):
|
2019-05-11 21:36:57 +00:00
|
|
|
app = Datasette([], immutables=files, sqlite_extensions=sqlite_extensions)
|
|
|
|
if inspect_file == "-":
|
|
|
|
out = sys.stdout
|
|
|
|
else:
|
|
|
|
out = open(inspect_file, "w")
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
inspect_data = loop.run_until_complete(inspect_(files, sqlite_extensions))
|
|
|
|
out.write(json.dumps(inspect_data, indent=2))
|
|
|
|
|
|
|
|
|
|
|
|
async def inspect_(files, sqlite_extensions):
|
|
|
|
app = Datasette([], immutables=files, sqlite_extensions=sqlite_extensions)
|
|
|
|
data = {}
|
|
|
|
for name, database in app.databases.items():
|
|
|
|
counts = await database.table_counts(limit=3600 * 1000)
|
|
|
|
data[name] = {
|
|
|
|
"hash": database.hash,
|
|
|
|
"size": database.size,
|
|
|
|
"file": database.path,
|
|
|
|
"tables": {
|
|
|
|
table_name: {"count": table_count}
|
|
|
|
for table_name, table_count in counts.items()
|
|
|
|
},
|
|
|
|
}
|
|
|
|
return data
|
2017-10-27 07:08:24 +00:00
|
|
|
|
|
|
|
|
2020-04-04 23:04:33 +00:00
|
|
|
@cli.group()
|
publish_subcommand hook + default plugins mechanism, used for publish heroku/now (#349)
This change introduces a new plugin hook, publish_subcommand, which can be
used to implement new subcommands for the "datasette publish" command family.
I've used this new hook to refactor out the "publish now" and "publish heroku"
implementations into separate modules. I've also added unit tests for these
two publishers, mocking the subprocess.call and subprocess.check_output
functions.
As part of this, I introduced a mechanism for loading default plugins. These
are defined in the new "default_plugins" list inside datasette/app.py
Closes #217 (Plugin support for datasette publish)
Closes #348 (Unit tests for "datasette publish")
Refs #14, #59, #102, #103, #146, #236, #347
2018-07-26 05:15:59 +00:00
|
|
|
def publish():
|
|
|
|
"Publish specified SQLite database files to the internet along with a Datasette-powered interface and API"
|
|
|
|
pass
|
2017-11-15 19:53:00 +00:00
|
|
|
|
2018-07-14 13:10:49 +00:00
|
|
|
|
publish_subcommand hook + default plugins mechanism, used for publish heroku/now (#349)
This change introduces a new plugin hook, publish_subcommand, which can be
used to implement new subcommands for the "datasette publish" command family.
I've used this new hook to refactor out the "publish now" and "publish heroku"
implementations into separate modules. I've also added unit tests for these
two publishers, mocking the subprocess.call and subprocess.check_output
functions.
As part of this, I introduced a mechanism for loading default plugins. These
are defined in the new "default_plugins" list inside datasette/app.py
Closes #217 (Plugin support for datasette publish)
Closes #348 (Unit tests for "datasette publish")
Refs #14, #59, #102, #103, #146, #236, #347
2018-07-26 05:15:59 +00:00
|
|
|
# Register publish plugins
|
|
|
|
pm.hook.publish_subcommand(publish=publish)
|
2017-11-13 16:13:38 +00:00
|
|
|
|
2017-11-22 17:42:29 +00:00
|
|
|
|
2019-01-26 20:01:16 +00:00
|
|
|
@cli.command()
|
|
|
|
@click.option("--all", help="Include built-in default plugins", is_flag=True)
|
|
|
|
@click.option(
|
|
|
|
"--plugins-dir",
|
|
|
|
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
|
|
help="Path to directory containing custom plugins",
|
|
|
|
)
|
|
|
|
def plugins(all, plugins_dir):
|
|
|
|
"List currently available plugins"
|
|
|
|
app = Datasette([], plugins_dir=plugins_dir)
|
2020-06-05 23:46:37 +00:00
|
|
|
click.echo(json.dumps(app._plugins(all=all), indent=4))
|
2019-01-26 20:01:16 +00:00
|
|
|
|
|
|
|
|
2017-11-13 16:13:38 +00:00
|
|
|
@cli.command()
|
2018-04-18 14:14:21 +00:00
|
|
|
@click.argument("files", type=click.Path(exists=True), nargs=-1, required=True)
|
|
|
|
@click.option(
|
|
|
|
"-t",
|
|
|
|
"--tag",
|
|
|
|
help="Name for the resulting Docker container, can optionally use name:tag format",
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"-m",
|
|
|
|
"--metadata",
|
|
|
|
type=click.File(mode="r"),
|
2020-04-02 19:30:53 +00:00
|
|
|
help="Path to JSON/YAML file containing metadata to publish",
|
2018-04-18 14:14:21 +00:00
|
|
|
)
|
|
|
|
@click.option("--extra-options", help="Extra options to pass to datasette serve")
|
|
|
|
@click.option("--branch", help="Install datasette from a GitHub branch e.g. master")
|
2017-11-13 16:13:38 +00:00
|
|
|
@click.option(
|
2018-04-18 14:14:21 +00:00
|
|
|
"--template-dir",
|
|
|
|
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
|
|
help="Path to directory containing custom templates",
|
2017-11-13 16:13:38 +00:00
|
|
|
)
|
|
|
|
@click.option(
|
2018-04-18 14:14:21 +00:00
|
|
|
"--plugins-dir",
|
|
|
|
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
|
|
help="Path to directory containing custom plugins",
|
2017-11-13 16:13:38 +00:00
|
|
|
)
|
2018-04-18 14:14:21 +00:00
|
|
|
@click.option(
|
|
|
|
"--static",
|
|
|
|
type=StaticMount(),
|
2019-11-26 02:31:42 +00:00
|
|
|
help="Serve static files from this directory at /MOUNT/...",
|
2018-04-18 14:14:21 +00:00
|
|
|
multiple=True,
|
|
|
|
)
|
2018-04-18 14:48:34 +00:00
|
|
|
@click.option(
|
2019-05-04 02:15:14 +00:00
|
|
|
"--install", help="Additional packages (e.g. plugins) to install", multiple=True
|
2018-05-31 14:16:50 +00:00
|
|
|
)
|
2019-05-04 02:15:14 +00:00
|
|
|
@click.option("--spatialite", is_flag=True, help="Enable SpatialLite extension")
|
2018-06-17 20:14:55 +00:00
|
|
|
@click.option("--version-note", help="Additional note to show on /-/versions")
|
2020-06-11 16:02:03 +00:00
|
|
|
@click.option(
|
|
|
|
"--secret",
|
|
|
|
help="Secret used for signing secure values, such as signed cookies",
|
|
|
|
envvar="DATASETTE_PUBLISH_SECRET",
|
|
|
|
default=lambda: os.urandom(32).hex(),
|
|
|
|
)
|
2020-01-29 22:46:43 +00:00
|
|
|
@click.option(
|
2020-09-02 22:24:55 +00:00
|
|
|
"-p",
|
|
|
|
"--port",
|
|
|
|
default=8001,
|
|
|
|
help="Port to run the server on, defaults to 8001",
|
2020-01-29 22:46:43 +00:00
|
|
|
)
|
2018-04-18 14:14:21 +00:00
|
|
|
@click.option("--title", help="Title for metadata")
|
|
|
|
@click.option("--license", help="License label for metadata")
|
|
|
|
@click.option("--license_url", help="License URL for metadata")
|
|
|
|
@click.option("--source", help="Source label for metadata")
|
|
|
|
@click.option("--source_url", help="Source URL for metadata")
|
2019-03-10 21:37:11 +00:00
|
|
|
@click.option("--about", help="About label for metadata")
|
|
|
|
@click.option("--about_url", help="About URL for metadata")
|
2018-04-18 14:14:21 +00:00
|
|
|
def package(
|
|
|
|
files,
|
|
|
|
tag,
|
|
|
|
metadata,
|
|
|
|
extra_options,
|
|
|
|
branch,
|
|
|
|
template_dir,
|
|
|
|
plugins_dir,
|
|
|
|
static,
|
2018-04-18 14:48:34 +00:00
|
|
|
install,
|
2018-05-31 14:16:50 +00:00
|
|
|
spatialite,
|
2018-06-17 20:14:55 +00:00
|
|
|
version_note,
|
2020-06-11 16:02:03 +00:00
|
|
|
secret,
|
2020-01-29 22:46:43 +00:00
|
|
|
port,
|
2018-04-18 14:14:21 +00:00
|
|
|
**extra_metadata
|
|
|
|
):
|
2017-11-13 16:13:38 +00:00
|
|
|
"Package specified SQLite files into a new datasette Docker container"
|
2018-04-18 14:14:21 +00:00
|
|
|
if not shutil.which("docker"):
|
2017-11-13 16:13:38 +00:00
|
|
|
click.secho(
|
|
|
|
' The package command requires "docker" to be installed and configured ',
|
2018-04-18 14:14:21 +00:00
|
|
|
bg="red",
|
|
|
|
fg="white",
|
2017-11-13 16:13:38 +00:00
|
|
|
bold=True,
|
|
|
|
err=True,
|
|
|
|
)
|
|
|
|
sys.exit(1)
|
2018-04-18 14:14:21 +00:00
|
|
|
with temporary_docker_directory(
|
|
|
|
files,
|
|
|
|
"datasette",
|
2020-06-11 16:02:03 +00:00
|
|
|
metadata=metadata,
|
|
|
|
extra_options=extra_options,
|
|
|
|
branch=branch,
|
|
|
|
template_dir=template_dir,
|
|
|
|
plugins_dir=plugins_dir,
|
|
|
|
static=static,
|
|
|
|
install=install,
|
|
|
|
spatialite=spatialite,
|
|
|
|
version_note=version_note,
|
|
|
|
secret=secret,
|
|
|
|
extra_metadata=extra_metadata,
|
2020-01-29 22:46:43 +00:00
|
|
|
port=port,
|
2018-04-18 14:14:21 +00:00
|
|
|
):
|
|
|
|
args = ["docker", "build"]
|
2017-11-13 16:13:38 +00:00
|
|
|
if tag:
|
2018-04-18 14:14:21 +00:00
|
|
|
args.append("-t")
|
2017-11-13 16:13:38 +00:00
|
|
|
args.append(tag)
|
2018-04-18 14:14:21 +00:00
|
|
|
args.append(".")
|
2017-11-13 16:13:38 +00:00
|
|
|
call(args)
|
2017-11-11 07:25:22 +00:00
|
|
|
|
|
|
|
|
2020-08-11 22:31:47 +00:00
|
|
|
@cli.command()
|
|
|
|
@click.argument("packages", nargs=-1, required=True)
|
2020-08-19 17:20:41 +00:00
|
|
|
@click.option(
|
|
|
|
"-U", "--upgrade", is_flag=True, help="Upgrade packages to latest version"
|
|
|
|
)
|
|
|
|
def install(packages, upgrade):
|
2020-08-11 22:33:16 +00:00
|
|
|
"Install Python packages - e.g. Datasette plugins - into the same environment as Datasette"
|
2020-08-19 17:20:41 +00:00
|
|
|
args = ["pip", "install"]
|
|
|
|
if upgrade:
|
|
|
|
args += ["--upgrade"]
|
|
|
|
args += list(packages)
|
|
|
|
sys.argv = args
|
2020-08-11 23:54:52 +00:00
|
|
|
run_module("pip", run_name="__main__")
|
2020-08-11 22:31:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
@click.argument("packages", nargs=-1, required=True)
|
|
|
|
@click.option("-y", "--yes", is_flag=True, help="Don't ask for confirmation")
|
|
|
|
def uninstall(packages, yes):
|
|
|
|
"Uninstall Python packages (e.g. plugins) from the Datasette environment"
|
2020-08-11 23:54:52 +00:00
|
|
|
sys.argv = ["pip", "uninstall"] + list(packages) + (["-y"] if yes else [])
|
|
|
|
run_module("pip", run_name="__main__")
|
2020-08-11 22:31:47 +00:00
|
|
|
|
|
|
|
|
2017-10-27 07:08:24 +00:00
|
|
|
@cli.command()
|
2018-04-18 14:14:21 +00:00
|
|
|
@click.argument("files", type=click.Path(exists=True), nargs=-1)
|
2019-03-17 23:25:15 +00:00
|
|
|
@click.option(
|
|
|
|
"-i",
|
|
|
|
"--immutable",
|
|
|
|
type=click.Path(exists=True),
|
|
|
|
help="Database files to open in immutable mode",
|
|
|
|
multiple=True,
|
|
|
|
)
|
2017-11-17 14:13:35 +00:00
|
|
|
@click.option(
|
2019-11-01 21:57:49 +00:00
|
|
|
"-h",
|
|
|
|
"--host",
|
|
|
|
default="127.0.0.1",
|
|
|
|
help=(
|
|
|
|
"Host for server. Defaults to 127.0.0.1 which means only connections "
|
|
|
|
"from the local machine will be allowed. Use 0.0.0.0 to listen to "
|
|
|
|
"all IPs and allow access from other machines."
|
|
|
|
),
|
2017-11-17 14:13:35 +00:00
|
|
|
)
|
2019-12-22 15:42:30 +00:00
|
|
|
@click.option(
|
|
|
|
"-p",
|
|
|
|
"--port",
|
|
|
|
default=8001,
|
|
|
|
help="Port for server, defaults to 8001. Use -p 0 to automatically assign an available port.",
|
|
|
|
)
|
2018-04-18 14:14:21 +00:00
|
|
|
@click.option(
|
|
|
|
"--reload",
|
|
|
|
is_flag=True,
|
2018-12-20 15:51:08 +00:00
|
|
|
help="Automatically reload if database or code change detected - useful for development",
|
2018-04-18 14:14:21 +00:00
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--cors", is_flag=True, help="Enable CORS by serving Access-Control-Allow-Origin: *"
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"sqlite_extensions",
|
|
|
|
"--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(
|
|
|
|
"--inspect-file", help='Path to JSON file created using "datasette inspect"'
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"-m",
|
|
|
|
"--metadata",
|
|
|
|
type=click.File(mode="r"),
|
2020-04-02 19:30:53 +00:00
|
|
|
help="Path to JSON/YAML file containing license/source metadata",
|
2018-04-18 14:14:21 +00:00
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--template-dir",
|
|
|
|
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
|
|
help="Path to directory containing custom templates",
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--plugins-dir",
|
|
|
|
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
|
|
help="Path to directory containing custom plugins",
|
|
|
|
)
|
|
|
|
@click.option(
|
|
|
|
"--static",
|
|
|
|
type=StaticMount(),
|
2019-11-26 02:31:42 +00:00
|
|
|
help="Serve static files from this directory at /MOUNT/...",
|
2018-04-18 14:14:21 +00:00
|
|
|
multiple=True,
|
|
|
|
)
|
2019-05-04 02:15:14 +00:00
|
|
|
@click.option("--memory", is_flag=True, help="Make :memory: database available")
|
2018-05-18 05:08:26 +00:00
|
|
|
@click.option(
|
2018-05-20 17:01:49 +00:00
|
|
|
"--config",
|
|
|
|
type=Config(),
|
2020-08-15 23:57:05 +00:00
|
|
|
help="Set config option using configname:value docs.datasette.io/en/stable/config.html",
|
2018-05-18 05:08:26 +00:00
|
|
|
multiple=True,
|
|
|
|
)
|
2020-05-31 22:42:08 +00:00
|
|
|
@click.option(
|
|
|
|
"--secret",
|
|
|
|
help="Secret used for signing secure values, such as signed cookies",
|
|
|
|
envvar="DATASETTE_SECRET",
|
|
|
|
)
|
2020-06-01 01:03:17 +00:00
|
|
|
@click.option(
|
|
|
|
"--root",
|
|
|
|
help="Output URL that sets a cookie authenticating the root user",
|
|
|
|
is_flag=True,
|
|
|
|
)
|
2020-08-12 00:24:40 +00:00
|
|
|
@click.option(
|
2020-09-02 22:24:55 +00:00
|
|
|
"--get",
|
|
|
|
help="Run an HTTP GET request against this path, print results and exit",
|
2020-08-12 00:24:40 +00:00
|
|
|
)
|
2018-06-17 20:14:55 +00:00
|
|
|
@click.option("--version-note", help="Additional note to show on /-/versions")
|
2019-05-04 02:15:14 +00:00
|
|
|
@click.option("--help-config", is_flag=True, help="Show available config options")
|
2020-09-11 18:37:55 +00:00
|
|
|
@click.option("--pdb", is_flag=True, help="Launch debugger on any errors")
|
2020-09-22 15:37:59 +00:00
|
|
|
@click.option(
|
|
|
|
"-o",
|
|
|
|
"--open",
|
|
|
|
"open_browser",
|
|
|
|
is_flag=True,
|
|
|
|
help="Open Datasette in your web browser",
|
|
|
|
)
|
2018-04-18 14:14:21 +00:00
|
|
|
def serve(
|
|
|
|
files,
|
2019-03-17 23:25:15 +00:00
|
|
|
immutable,
|
2018-04-18 14:14:21 +00:00
|
|
|
host,
|
|
|
|
port,
|
|
|
|
reload,
|
|
|
|
cors,
|
|
|
|
sqlite_extensions,
|
|
|
|
inspect_file,
|
|
|
|
metadata,
|
|
|
|
template_dir,
|
|
|
|
plugins_dir,
|
|
|
|
static,
|
2019-03-14 23:42:38 +00:00
|
|
|
memory,
|
2018-05-20 17:01:49 +00:00
|
|
|
config,
|
2020-05-31 22:42:08 +00:00
|
|
|
secret,
|
2020-06-01 01:03:17 +00:00
|
|
|
root,
|
2020-08-12 00:24:40 +00:00
|
|
|
get,
|
2018-06-17 20:14:55 +00:00
|
|
|
version_note,
|
2018-05-20 17:01:49 +00:00
|
|
|
help_config,
|
2020-09-11 18:37:55 +00:00
|
|
|
pdb,
|
2020-09-22 15:37:59 +00:00
|
|
|
open_browser,
|
2020-04-02 19:30:53 +00:00
|
|
|
return_instance=False,
|
2018-04-18 14:14:21 +00:00
|
|
|
):
|
2017-11-13 18:41:53 +00:00
|
|
|
"""Serve up specified SQLite database files with a web UI"""
|
2018-05-20 17:01:49 +00:00
|
|
|
if help_config:
|
|
|
|
formatter = formatting.HelpFormatter()
|
|
|
|
with formatter.section("Config options"):
|
2019-05-04 02:15:14 +00:00
|
|
|
formatter.write_dl(
|
|
|
|
[
|
|
|
|
(option.name, "{} (default={})".format(option.help, option.default))
|
|
|
|
for option in CONFIG_OPTIONS
|
|
|
|
]
|
|
|
|
)
|
2018-05-20 17:01:49 +00:00
|
|
|
click.echo(formatter.getvalue())
|
|
|
|
sys.exit(0)
|
2017-11-09 13:46:16 +00:00
|
|
|
if reload:
|
|
|
|
import hupper
|
2018-04-18 14:14:21 +00:00
|
|
|
|
|
|
|
reloader = hupper.start_reloader("datasette.cli.serve")
|
2020-02-24 19:44:59 +00:00
|
|
|
if immutable:
|
|
|
|
reloader.watch_files(immutable)
|
2017-12-07 16:42:28 +00:00
|
|
|
if metadata:
|
|
|
|
reloader.watch_files([metadata.name])
|
2017-11-09 13:46:16 +00:00
|
|
|
|
2017-11-13 15:20:02 +00:00
|
|
|
inspect_data = None
|
|
|
|
if inspect_file:
|
|
|
|
inspect_data = json.load(open(inspect_file))
|
|
|
|
|
|
|
|
metadata_data = None
|
2017-11-11 20:10:51 +00:00
|
|
|
if metadata:
|
2020-04-02 19:30:53 +00:00
|
|
|
metadata_data = parse_metadata(metadata.read())
|
2017-11-11 20:10:51 +00:00
|
|
|
|
2020-04-27 16:30:24 +00:00
|
|
|
kwargs = dict(
|
2019-03-17 23:25:15 +00:00
|
|
|
immutables=immutable,
|
2020-10-10 23:39:38 +00:00
|
|
|
cache_headers=not reload,
|
2017-11-13 18:17:42 +00:00
|
|
|
cors=cors,
|
2017-11-13 15:20:02 +00:00
|
|
|
inspect_data=inspect_data,
|
|
|
|
metadata=metadata_data,
|
2017-11-17 14:13:35 +00:00
|
|
|
sqlite_extensions=sqlite_extensions,
|
2017-11-30 16:05:01 +00:00
|
|
|
template_dir=template_dir,
|
2018-04-16 05:22:01 +00:00
|
|
|
plugins_dir=plugins_dir,
|
2017-12-03 16:33:36 +00:00
|
|
|
static_mounts=static,
|
2018-05-20 17:01:49 +00:00
|
|
|
config=dict(config),
|
2019-03-14 23:42:38 +00:00
|
|
|
memory=memory,
|
2020-05-31 22:42:08 +00:00
|
|
|
secret=secret,
|
2018-06-17 20:14:55 +00:00
|
|
|
version_note=version_note,
|
2020-09-11 18:37:55 +00:00
|
|
|
pdb=pdb,
|
2017-11-13 18:03:52 +00:00
|
|
|
)
|
2020-04-27 16:30:24 +00:00
|
|
|
|
|
|
|
# if files is a single directory, use that as config_dir=
|
|
|
|
if 1 == len(files) and os.path.isdir(files[0]):
|
|
|
|
kwargs["config_dir"] = pathlib.Path(files[0])
|
|
|
|
files = []
|
|
|
|
|
|
|
|
ds = Datasette(files, **kwargs)
|
|
|
|
|
2020-04-02 19:30:53 +00:00
|
|
|
if return_instance:
|
|
|
|
# Private utility mechanism for writing unit tests
|
|
|
|
return ds
|
|
|
|
|
2020-06-13 17:55:41 +00:00
|
|
|
# Run the "startup" plugin hooks
|
|
|
|
asyncio.get_event_loop().run_until_complete(ds.invoke_startup())
|
|
|
|
|
2019-05-11 23:22:55 +00:00
|
|
|
# Run async sanity checks - but only if we're not under pytest
|
2020-02-15 17:56:48 +00:00
|
|
|
asyncio.get_event_loop().run_until_complete(check_databases(ds))
|
2019-05-11 23:22:55 +00:00
|
|
|
|
2020-08-15 20:38:15 +00:00
|
|
|
if get:
|
2020-10-09 16:11:24 +00:00
|
|
|
client = TestClient(ds)
|
2020-08-15 20:38:15 +00:00
|
|
|
response = client.get(get)
|
|
|
|
click.echo(response.text)
|
2020-09-11 21:32:54 +00:00
|
|
|
exit_code = 0 if response.status == 200 else 1
|
|
|
|
sys.exit(exit_code)
|
2020-08-15 20:38:15 +00:00
|
|
|
return
|
|
|
|
|
2019-05-11 23:22:55 +00:00
|
|
|
# Start the server
|
2020-06-01 01:03:17 +00:00
|
|
|
if root:
|
2020-09-22 14:26:47 +00:00
|
|
|
url = "http://{}:{}/-/auth-token?token={}".format(host, port, ds._root_token)
|
|
|
|
print(url)
|
|
|
|
else:
|
|
|
|
url = "http://{}:{}/".format(host, port)
|
2020-09-22 15:37:59 +00:00
|
|
|
if open_browser:
|
2020-09-22 14:26:47 +00:00
|
|
|
webbrowser.open(url)
|
2020-10-08 23:16:55 +00:00
|
|
|
uvicorn.run(
|
|
|
|
ds.app(), host=host, port=port, log_level="info", lifespan="on", workers=1
|
|
|
|
)
|
2020-02-15 17:56:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
async def check_databases(ds):
|
|
|
|
# Run check_connection against every connected database
|
|
|
|
# to confirm they are all usable
|
|
|
|
for database in list(ds.databases.values()):
|
|
|
|
try:
|
2020-05-08 14:16:39 +00:00
|
|
|
await database.execute_fn(check_connection)
|
2020-02-15 17:56:48 +00:00
|
|
|
except SpatialiteConnectionProblem:
|
|
|
|
raise click.UsageError(
|
|
|
|
"It looks like you're trying to load a SpatiaLite"
|
|
|
|
" database without first loading the SpatiaLite module."
|
2020-08-15 23:57:05 +00:00
|
|
|
"\n\nRead more: https://docs.datasette.io/en/stable/spatialite.html"
|
2020-02-15 17:56:48 +00:00
|
|
|
)
|
|
|
|
except ConnectionProblem as e:
|
|
|
|
raise click.UsageError(
|
2020-02-22 01:32:40 +00:00
|
|
|
"Connection to {} failed check: {}".format(
|
|
|
|
database.path, str(e.args[0])
|
|
|
|
)
|
2020-02-15 17:56:48 +00:00
|
|
|
)
|