Moved default commands to dclient default ..., closes #30

This commit is contained in:
Simon Willison 2026-02-26 20:44:39 -08:00
commit 2f7f2a7378
6 changed files with 220 additions and 129 deletions

View file

@ -32,8 +32,8 @@ datasette install dclient
Add an alias for a Datasette instance:
```bash
dclient alias add latest https://latest.datasette.io
dclient alias default latest
dclient alias default-db latest fixtures
dclient default instance latest
dclient default database latest fixtures
```
Now run queries directly:
```bash

View file

@ -53,8 +53,11 @@ def _resolve_instance(instance, config_file):
)
# Try config default
default = config.get("default_instance")
if default and default in config.get("instances", {}):
return config["instances"][default]["url"]
if default:
if default in config.get("instances", {}):
return config["instances"][default]["url"]
if default.startswith("http://") or default.startswith("https://"):
return default.rstrip("/")
# Try env var
env_url = os.environ.get("DATASETTE_URL")
if env_url:
@ -62,7 +65,7 @@ def _resolve_instance(instance, config_file):
raise click.ClickException(
"No instance specified. Use -i <url-or-alias>, or configure a default:\n\n"
" dclient alias add <name> <url>\n"
" dclient alias default <name>\n\n"
" dclient default instance <name-or-url>\n\n"
"Or set the DATASETTE_URL environment variable."
)
@ -75,8 +78,13 @@ def _resolve_database(database, instance_alias, config_file):
if instance_alias:
config = _load_config(config_file)
instances = config.get("instances", {})
if instance_alias in instances:
default_db = instances[instance_alias].get("default_database")
key = instance_alias
if key not in instances and (
key.startswith("http://") or key.startswith("https://")
):
key = _instance_alias_for_url(key, config_file)
if key in instances:
default_db = instances[key].get("default_database")
if default_db:
return default_db
# Try env var
@ -85,7 +93,7 @@ def _resolve_database(database, instance_alias, config_file):
return env_db
raise click.ClickException(
"No database specified. Use -d <name>, or configure a default:\n\n"
" dclient alias default-db <alias> <database>\n\n"
" dclient default database <alias-or-url> <database>\n\n"
"Or set the DATASETTE_DATABASE environment variable."
)
@ -902,29 +910,56 @@ def alias_remove(name):
raise click.ClickException("No such alias")
@alias.command(name="default")
@click.argument("name", required=False, default=None)
def _resolve_instance_key(alias_or_url, config):
instances = config.get("instances", {})
if alias_or_url in instances:
return alias_or_url
if alias_or_url.startswith("http://") or alias_or_url.startswith("https://"):
normalized = alias_or_url.rstrip("/")
for name, inst in instances.items():
if inst.get("url", "").rstrip("/") == normalized:
return name
raise click.ClickException(f"No such instance URL: {alias_or_url}")
raise click.ClickException(f"No such alias: {alias_or_url}")
# -- default command group --
@cli.group()
def default():
"Manage default instance and database"
@default.command(name="instance")
@click.argument("alias_or_url", required=False, default=None)
@click.option("--clear", is_flag=True, help="Clear default instance")
def alias_default(name, clear):
def default_instance(alias_or_url, clear):
"""
Set or show the default instance
Example usage:
\b
dclient alias default prod
dclient alias default
dclient alias default --clear
dclient default instance prod
dclient default instance https://myapp.datasette.cloud
dclient default instance
dclient default instance --clear
"""
config_file = get_config_dir() / "config.json"
config = _load_config(config_file)
if clear:
config["default_instance"] = None
_save_config(config_file, config)
elif name:
if name not in config.get("instances", {}):
raise click.ClickException(f"No such alias: {name}")
config["default_instance"] = name
elif alias_or_url:
if alias_or_url.startswith("http://") or alias_or_url.startswith("https://"):
try:
key = _resolve_instance_key(alias_or_url, config)
except click.ClickException:
key = alias_or_url.rstrip("/")
else:
key = _resolve_instance_key(alias_or_url, config)
config["default_instance"] = key
_save_config(config_file, config)
else:
default = config.get("default_instance")
@ -934,37 +969,37 @@ def alias_default(name, clear):
click.echo("No default instance set")
@alias.command(name="default-db")
@click.argument("alias_name")
@default.command(name="database")
@click.argument("alias_or_url")
@click.argument("db", required=False, default=None)
@click.option("--clear", is_flag=True, help="Clear default database for this alias")
def alias_default_db(alias_name, db, clear):
@click.option("--clear", is_flag=True, help="Clear default database for this instance")
def default_database(alias_or_url, db, clear):
"""
Set or show the default database for an alias
Set or show the default database for an instance
Example usage:
\b
dclient alias default-db prod main
dclient alias default-db prod
dclient alias default-db prod --clear
dclient default database prod main
dclient default database https://myapp.datasette.cloud main
dclient default database prod
dclient default database prod --clear
"""
config_file = get_config_dir() / "config.json"
config = _load_config(config_file)
if alias_name not in config.get("instances", {}):
raise click.ClickException(f"No such alias: {alias_name}")
instance_key = _resolve_instance_key(alias_or_url, config)
if clear:
config["instances"][alias_name]["default_database"] = None
config["instances"][instance_key]["default_database"] = None
_save_config(config_file, config)
elif db:
config["instances"][alias_name]["default_database"] = db
config["instances"][instance_key]["default_database"] = db
_save_config(config_file, config)
else:
default_db = config["instances"][alias_name].get("default_database")
default_db = config["instances"][instance_key].get("default_database")
if default_db:
click.echo(default_db)
else:
click.echo(f"No default database set for {alias_name}")
click.echo(f"No default database set for {instance_key}")
# -- auth command group --

View file

@ -13,28 +13,7 @@ Once registered, you can pass an alias to commands using the `-i` flag:
dclient query fixtures "select * from news limit 1" -i latest
## Default instance
Set a default instance so you don't need `-i` every time:
dclient alias default latest
Now commands will use `latest` automatically:
dclient databases
dclient tables -d fixtures
## Default database
Set a default database for an alias:
dclient alias default-db latest fixtures
Now you can run bare SQL queries directly:
dclient "select * from facetable limit 5"
This uses the default instance and default database.
See [Defaults](defaults.md) for default instance and default database settings.
## dclient alias --help
<!-- [[[cog
@ -57,11 +36,9 @@ Options:
--help Show this message and exit.
Commands:
add Add an alias for a Datasette instance
default Set or show the default instance
default-db Set or show the default database for an alias
list List aliases
remove Remove an alias
add Add an alias for a Datasette instance
list List aliases
remove Remove an alias
```
<!-- [[[end]]] -->
@ -137,59 +114,3 @@ Options:
```
<!-- [[[end]]] -->
## dclient alias default --help
<!-- [[[cog
import cog
result = runner.invoke(cli.cli, ["alias", "default", "--help"])
help = result.output.replace("Usage: cli", "Usage: dclient")
cog.out(
"```\n{}\n```".format(help)
)
]]] -->
```
Usage: dclient alias default [OPTIONS] [NAME]
Set or show the default instance
Example usage:
dclient alias default prod
dclient alias default
dclient alias default --clear
Options:
--clear Clear default instance
--help Show this message and exit.
```
<!-- [[[end]]] -->
## dclient alias default-db --help
<!-- [[[cog
import cog
result = runner.invoke(cli.cli, ["alias", "default-db", "--help"])
help = result.output.replace("Usage: cli", "Usage: dclient")
cog.out(
"```\n{}\n```".format(help)
)
]]] -->
```
Usage: dclient alias default-db [OPTIONS] ALIAS_NAME [DB]
Set or show the default database for an alias
Example usage:
dclient alias default-db prod main
dclient alias default-db prod
dclient alias default-db prod --clear
Options:
--clear Clear default database for this alias
--help Show this message and exit.
```
<!-- [[[end]]] -->

106
docs/defaults.md Normal file
View file

@ -0,0 +1,106 @@
# Defaults
Set a default instance so you don't need `-i` every time:
dclient default instance latest
Now commands will use `latest` automatically:
dclient databases
dclient tables -d fixtures
Set a default database for an alias or instance URL:
dclient default database latest fixtures
dclient default database https://latest.datasette.io fixtures
Now you can run bare SQL queries directly:
dclient "select * from facetable limit 5"
This uses the default instance and default database.
## dclient default --help
<!-- [[[cog
import cog
from dclient import cli
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(cli.cli, ["default", "--help"])
help = result.output.replace("Usage: cli", "Usage: dclient")
cog.out(
"```\n{}\n```".format(help)
)
]]] -->
```
Usage: dclient default [OPTIONS] COMMAND [ARGS]...
Manage default instance and database
Options:
--help Show this message and exit.
Commands:
database Set or show the default database for an instance
instance Set or show the default instance
```
<!-- [[[end]]] -->
## dclient default instance --help
<!-- [[[cog
import cog
result = runner.invoke(cli.cli, ["default", "instance", "--help"])
help = result.output.replace("Usage: cli", "Usage: dclient")
cog.out(
"```\n{}\n```".format(help)
)
]]] -->
```
Usage: dclient default instance [OPTIONS] [ALIAS_OR_URL]
Set or show the default instance
Example usage:
dclient default instance prod
dclient default instance https://myapp.datasette.cloud
dclient default instance
dclient default instance --clear
Options:
--clear Clear default instance
--help Show this message and exit.
```
<!-- [[[end]]] -->
## dclient default database --help
<!-- [[[cog
import cog
result = runner.invoke(cli.cli, ["default", "database", "--help"])
help = result.output.replace("Usage: cli", "Usage: dclient")
cog.out(
"```\n{}\n```".format(help)
)
]]] -->
```
Usage: dclient default database [OPTIONS] ALIAS_OR_URL [DB]
Set or show the default database for an instance
Example usage:
dclient default database prod main
dclient default database https://myapp.datasette.cloud main
dclient default database prod
dclient default database prod --clear
Options:
--clear Clear default database for this instance
--help Show this message and exit.
```
<!-- [[[end]]] -->

View file

@ -35,6 +35,7 @@ maxdepth: 3
---
queries
aliases
defaults
authentication
inserting
environment

View file

@ -1,4 +1,4 @@
"""Tests for v2 alias command: default, default-db subcommands."""
"""Tests for v2 defaults command: instance and database subcommands."""
from click.testing import CliRunner
from dclient.cli import cli
@ -15,16 +15,16 @@ def test_alias_default_workflow(mocker, tmpdir):
assert result.exit_code == 0
# No default yet
result = runner.invoke(cli, ["alias", "default"])
result = runner.invoke(cli, ["default", "instance"])
assert result.exit_code == 0
assert "No default instance set" in result.output
# Set default
result = runner.invoke(cli, ["alias", "default", "prod"])
result = runner.invoke(cli, ["default", "instance", "prod"])
assert result.exit_code == 0
# Show default
result = runner.invoke(cli, ["alias", "default"])
result = runner.invoke(cli, ["default", "instance"])
assert result.exit_code == 0
assert result.output.strip() == "prod"
@ -34,17 +34,17 @@ def test_alias_default_workflow(mocker, tmpdir):
assert "* prod" in result.output
# Clear default
result = runner.invoke(cli, ["alias", "default", "--clear"])
result = runner.invoke(cli, ["default", "instance", "--clear"])
assert result.exit_code == 0
result = runner.invoke(cli, ["alias", "default"])
result = runner.invoke(cli, ["default", "instance"])
assert "No default instance set" in result.output
def test_alias_default_unknown_alias(mocker, tmpdir):
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
runner = CliRunner()
result = runner.invoke(cli, ["alias", "default", "nonexistent"])
result = runner.invoke(cli, ["default", "instance", "nonexistent"])
assert result.exit_code == 1
assert "No such alias" in result.output
@ -58,16 +58,16 @@ def test_alias_default_db_workflow(mocker, tmpdir):
assert result.exit_code == 0
# No default database yet
result = runner.invoke(cli, ["alias", "default-db", "prod"])
result = runner.invoke(cli, ["default", "database", "prod"])
assert result.exit_code == 0
assert "No default database set" in result.output
# Set default database
result = runner.invoke(cli, ["alias", "default-db", "prod", "main"])
result = runner.invoke(cli, ["default", "database", "prod", "main"])
assert result.exit_code == 0
# Show default database
result = runner.invoke(cli, ["alias", "default-db", "prod"])
result = runner.invoke(cli, ["default", "database", "prod"])
assert result.exit_code == 0
assert result.output.strip() == "main"
@ -76,17 +76,17 @@ def test_alias_default_db_workflow(mocker, tmpdir):
assert "(db: main)" in result.output
# Clear default database
result = runner.invoke(cli, ["alias", "default-db", "prod", "--clear"])
result = runner.invoke(cli, ["default", "database", "prod", "--clear"])
assert result.exit_code == 0
result = runner.invoke(cli, ["alias", "default-db", "prod"])
result = runner.invoke(cli, ["default", "database", "prod"])
assert "No default database set" in result.output
def test_alias_default_db_unknown_alias(mocker, tmpdir):
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
runner = CliRunner()
result = runner.invoke(cli, ["alias", "default-db", "nonexistent", "main"])
result = runner.invoke(cli, ["default", "database", "nonexistent", "main"])
assert result.exit_code == 1
assert "No such alias" in result.output
@ -97,7 +97,7 @@ def test_alias_remove_clears_default(mocker, tmpdir):
runner = CliRunner()
runner.invoke(cli, ["alias", "add", "prod", "https://prod.example.com"])
runner.invoke(cli, ["alias", "default", "prod"])
runner.invoke(cli, ["default", "instance", "prod"])
# Verify it's set
config = json.loads((pathlib.Path(tmpdir) / "config.json").read_text())
@ -109,3 +109,31 @@ def test_alias_remove_clears_default(mocker, tmpdir):
config = json.loads((pathlib.Path(tmpdir) / "config.json").read_text())
assert config["default_instance"] is None
assert "prod" not in config["instances"]
def test_default_instance_accepts_url(mocker, tmpdir):
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
runner = CliRunner()
runner.invoke(cli, ["alias", "add", "prod", "https://prod.example.com"])
result = runner.invoke(cli, ["default", "instance", "https://prod.example.com"])
assert result.exit_code == 0
result = runner.invoke(cli, ["default", "instance"])
assert result.exit_code == 0
assert result.output.strip() == "prod"
def test_default_database_accepts_url(mocker, tmpdir):
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
runner = CliRunner()
runner.invoke(cli, ["alias", "add", "prod", "https://prod.example.com"])
result = runner.invoke(
cli, ["default", "database", "https://prod.example.com", "main"]
)
assert result.exit_code == 0
result = runner.invoke(cli, ["default", "database", "prod"])
assert result.exit_code == 0
assert result.output.strip() == "main"