Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start a new datasette.yaml configuration file, with settings support #2149

Merged
merged 1 commit into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions datasette/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@
cache_headers=True,
cors=False,
inspect_data=None,
config=None,
metadata=None,
sqlite_extensions=None,
template_dir=None,
Expand Down Expand Up @@ -316,16 +317,27 @@
)
self.cache_headers = cache_headers
self.cors = cors
config_files = []
metadata_files = []
if config_dir:
metadata_files = [
config_dir / filename
for filename in ("metadata.json", "metadata.yaml", "metadata.yml")
if (config_dir / filename).exists()
]
config_files = [
config_dir / filename
for filename in ("datasette.json", "datasette.yaml", "datasette.yml")
if (config_dir / filename).exists()
]
if config_dir and metadata_files and not metadata:
with metadata_files[0].open() as fp:
metadata = parse_metadata(fp.read())

if config_dir and config_files and not config:
with config_files[0].open() as fp:
config = parse_metadata(fp.read())

self._metadata_local = metadata or {}
self.sqlite_extensions = []
for extension in sqlite_extensions or []:
Expand All @@ -344,17 +356,19 @@
if config_dir and (config_dir / "static").is_dir() and not static_mounts:
static_mounts = [("static", str((config_dir / "static").resolve()))]
self.static_mounts = static_mounts or []
if config_dir and (config_dir / "config.json").exists():
raise StartupError("config.json should be renamed to settings.json")
if config_dir and (config_dir / "settings.json").exists() and not settings:
settings = json.loads((config_dir / "settings.json").read_text())
# Validate those settings
for key in settings:
if key not in DEFAULT_SETTINGS:
raise StartupError(
"Invalid setting '{}' in settings.json".format(key)
)
self._settings = dict(DEFAULT_SETTINGS, **(settings or {}))
if config_dir and (config_dir / "datasette.json").exists() and not config:
config = json.loads((config_dir / "datasette.json").read_text())

Check warning on line 360 in datasette/app.py

View check run for this annotation

Codecov / codecov/patch

datasette/app.py#L360

Added line #L360 was not covered by tests

config = config or {}
config_settings = config.get("settings") or {}

# validate "settings" keys in datasette.json
for key in config_settings:
if key not in DEFAULT_SETTINGS:
raise StartupError("Invalid setting '{}' in datasette.json".format(key))

# CLI settings should overwrite datasette.json settings
self._settings = dict(DEFAULT_SETTINGS, **(config_settings), **(settings or {}))
self.renderers = {} # File extension -> (renderer, can_render) functions
self.version_note = version_note
if self.setting("num_sql_threads") == 0:
Expand Down
59 changes: 8 additions & 51 deletions datasette/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,46 +50,6 @@
pass


class Config(click.ParamType):
# This will be removed in Datasette 1.0 in favour of class Setting
name = "config"

def convert(self, config, param, ctx):
if ":" not in config:
self.fail(f'"{config}" should be name:value', param, ctx)
return
name, value = config.split(":", 1)
if name not in DEFAULT_SETTINGS:
msg = (
OBSOLETE_SETTINGS.get(name)
or f"{name} is not a valid option (--help-settings to see all)"
)
self.fail(
msg,
param,
ctx,
)
return
# Type checking
default = DEFAULT_SETTINGS[name]
if isinstance(default, bool):
try:
return name, value_as_boolean(value)
except ValueAsBooleanError:
self.fail(f'"{name}" should be on/off/true/false/1/0', param, ctx)
return
elif isinstance(default, int):
if not value.isdigit():
self.fail(f'"{name}" should be an integer', param, ctx)
return
return name, int(value)
elif isinstance(default, str):
return name, value
else:
# Should never happen:
self.fail("Invalid option")


class Setting(CompositeParamType):
name = "setting"
arity = 2
Expand Down Expand Up @@ -456,9 +416,8 @@
@click.option("--memory", is_flag=True, help="Make /_memory database available")
@click.option(
"--config",
type=Config(),
help="Deprecated: set config option using configname:value. Use --setting instead.",
multiple=True,
type=click.File(mode="r"),
help="Path to JSON/YAML Datasette configuration file",
)
@click.option(
"--setting",
Expand Down Expand Up @@ -568,6 +527,8 @@
reloader = hupper.start_reloader("datasette.cli.serve")
if immutable:
reloader.watch_files(immutable)
if config:
reloader.watch_files([config.name])

Check warning on line 531 in datasette/cli.py

View check run for this annotation

Codecov / codecov/patch

datasette/cli.py#L530-L531

Added lines #L530 - L531 were not covered by tests
if metadata:
reloader.watch_files([metadata.name])

Expand All @@ -580,26 +541,22 @@
if metadata:
metadata_data = parse_metadata(metadata.read())

combined_settings = {}
config_data = None
if config:
click.echo(
"--config name:value will be deprecated in Datasette 1.0, use --setting name value instead",
err=True,
)
combined_settings.update(config)
combined_settings.update(settings)
config_data = parse_metadata(config.read())

Check warning on line 546 in datasette/cli.py

View check run for this annotation

Codecov / codecov/patch

datasette/cli.py#L546

Added line #L546 was not covered by tests

kwargs = dict(
immutables=immutable,
cache_headers=not reload,
cors=cors,
inspect_data=inspect_data,
config=config_data,
metadata=metadata_data,
sqlite_extensions=sqlite_extensions,
template_dir=template_dir,
plugins_dir=plugins_dir,
static_mounts=static,
settings=combined_settings,
settings=dict(settings),
memory=memory,
secret=secret,
version_note=version_note,
Expand Down
3 changes: 1 addition & 2 deletions docs/cli-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,7 @@ Once started you can access it at ``http://localhost:8001``
--static MOUNT:DIRECTORY Serve static files from this directory at
/MOUNT/...
--memory Make /_memory database available
--config CONFIG Deprecated: set config option using
configname:value. Use --setting instead.
--config FILENAME Path to JSON/YAML Datasette configuration file
--setting SETTING... Setting, see
docs.datasette.io/en/stable/settings.html
--secret TEXT Secret used for signing secure values, such as
Expand Down
10 changes: 10 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.. _configuration:

Configuration
========

Datasette offers many way to configure your Datasette instances: server settings, plugin configuration, authentication, and more.

To facilitate this, You can provide a `datasette.yaml` configuration file to datasette with the ``--config``/ ``-c`` flag:

datasette mydatabase.db --config datasette.yaml
2 changes: 1 addition & 1 deletion docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ Datasette will detect the files in that directory and automatically configure it
The files that can be included in this directory are as follows. All are optional.

* ``*.db`` (or ``*.sqlite3`` or ``*.sqlite``) - SQLite database files that will be served by Datasette
* ``datasette.json`` - :ref:`configuration` for the Datasette instance
* ``metadata.json`` - :ref:`metadata` for those databases - ``metadata.yaml`` or ``metadata.yml`` can be used as well
* ``inspect-data.json`` - the result of running ``datasette inspect *.db --inspect-file=inspect-data.json`` from the configuration directory - any database files listed here will be treated as immutable, so they should not be changed while Datasette is running
* ``settings.json`` - settings that would normally be passed using ``--setting`` - here they should be stored as a JSON object of key/value pairs
* ``templates/`` - a directory containing :ref:`customization_custom_templates`
* ``plugins/`` - a directory containing plugins, see :ref:`writing_plugins_one_off`
* ``static/`` - a directory containing static files - these will be served from ``/static/filename.txt``, see :ref:`customization_static_files`
Expand Down
11 changes: 0 additions & 11 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,17 +258,6 @@ def test_setting_default_allow_sql(default_allow_sql):
assert "Forbidden" in result.output


def test_config_deprecated():
# The --config option should show a deprecation message
runner = CliRunner(mix_stderr=False)
result = runner.invoke(
cli, ["--config", "allow_download:off", "--get", "/-/settings.json"]
)
assert result.exit_code == 0
assert not json.loads(result.output)["allow_download"]
assert "will be deprecated in" in result.stderr


def test_sql_errors_logged_to_stderr():
runner = CliRunner(mix_stderr=False)
result = runner.invoke(cli, ["--get", "/_memory.json?sql=select+blah"])
Expand Down
27 changes: 10 additions & 17 deletions tests/test_config_dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ def extra_template_vars():
}
"""
METADATA = {"title": "This is from metadata"}
SETTINGS = {
"default_cache_ttl": 60,
CONFIG = {
"settings": {
"default_cache_ttl": 60,
}
}
CSS = """
body { margin-top: 3em}
Expand All @@ -47,7 +49,7 @@ def config_dir(tmp_path_factory):
(static_dir / "hello.css").write_text(CSS, "utf-8")

(config_dir / "metadata.json").write_text(json.dumps(METADATA), "utf-8")
(config_dir / "settings.json").write_text(json.dumps(SETTINGS), "utf-8")
(config_dir / "datasette.json").write_text(json.dumps(CONFIG), "utf-8")

for dbname in ("demo.db", "immutable.db", "j.sqlite3", "k.sqlite"):
db = sqlite3.connect(str(config_dir / dbname))
Expand Down Expand Up @@ -81,16 +83,16 @@ def config_dir(tmp_path_factory):


def test_invalid_settings(config_dir):
previous = (config_dir / "settings.json").read_text("utf-8")
(config_dir / "settings.json").write_text(
json.dumps({"invalid": "invalid-setting"}), "utf-8"
previous = (config_dir / "datasette.json").read_text("utf-8")
(config_dir / "datasette.json").write_text(
json.dumps({"settings": {"invalid": "invalid-setting"}}), "utf-8"
)
try:
with pytest.raises(StartupError) as ex:
ds = Datasette([], config_dir=config_dir)
assert ex.value.args[0] == "Invalid setting 'invalid' in settings.json"
assert ex.value.args[0] == "Invalid setting 'invalid' in datasette.json"
finally:
(config_dir / "settings.json").write_text(previous, "utf-8")
(config_dir / "datasette.json").write_text(previous, "utf-8")


@pytest.fixture(scope="session")
Expand All @@ -111,15 +113,6 @@ def test_settings(config_dir_client):
assert 60 == response.json["default_cache_ttl"]


def test_error_on_config_json(tmp_path_factory):
config_dir = tmp_path_factory.mktemp("config-dir")
(config_dir / "config.json").write_text(json.dumps(SETTINGS), "utf-8")
runner = CliRunner(mix_stderr=False)
result = runner.invoke(cli, [str(config_dir), "--get", "/-/settings.json"])
assert result.exit_code == 1
assert "config.json should be renamed to settings.json" in result.stderr


def test_plugins(config_dir_client):
response = config_dir_client.get("/-/plugins.json")
assert 200 == response.status
Expand Down
Loading