diff --git a/README.md b/README.md index 2012f77..96b7764 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/dclient/cli.py b/dclient/cli.py index cf081e0..fcc8047 100644 --- a/dclient/cli.py +++ b/dclient/cli.py @@ -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 , or configure a default:\n\n" " dclient alias add \n" - " dclient alias default \n\n" + " dclient default instance \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 , or configure a default:\n\n" - " dclient alias default-db \n\n" + " dclient default 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 -- diff --git a/docs/aliases.md b/docs/aliases.md index 8399085..4c4255d 100644 --- a/docs/aliases.md +++ b/docs/aliases.md @@ -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 @@ -137,59 +114,3 @@ Options: ``` - -## dclient alias default --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. - -``` - - -## dclient alias default-db --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. - -``` - diff --git a/docs/defaults.md b/docs/defaults.md new file mode 100644 index 0000000..b53c334 --- /dev/null +++ b/docs/defaults.md @@ -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 + +``` +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 + +``` + + +## dclient default instance --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. + +``` + + +## dclient default database --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. + +``` + diff --git a/docs/index.md b/docs/index.md index dda58da..0b43234 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,6 +35,7 @@ maxdepth: 3 --- queries aliases +defaults authentication inserting environment diff --git a/tests/test_alias_v2.py b/tests/test_alias_v2.py index e93902d..173382e 100644 --- a/tests/test_alias_v2.py +++ b/tests/test_alias_v2.py @@ -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"