From 1ecb6a02e8d3e95548ced77107169fdecb1e3ada Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Mon, 21 Nov 2022 18:16:28 -0800 Subject: [PATCH] Initial dclient query command, refs #1 --- README.md | 26 ++++++++++++++++++++++++++ dclient/cli.py | 36 ++++++++++++++++++++++++------------ setup.py | 8 +++----- tests/test_dclient.py | 10 ---------- tests/test_query.py | 41 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 27 deletions(-) delete mode 100644 tests/test_dclient.py create mode 100644 tests/test_query.py diff --git a/README.md b/README.md index 6663a01..c2958f1 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,32 @@ You can also use: python -m dclient --help +### dclient query + + +``` +Usage: dclient query [OPTIONS] URL SQL + + Run a SQL query against a Datasette database URL + + Returns a JSON array of objects + +Options: + --help Show this message and exit. + +``` + + ## Development To contribute to this tool, first checkout the code. Then create a new virtual environment: diff --git a/dclient/cli.py b/dclient/cli.py index 99b00f4..9e436bc 100644 --- a/dclient/cli.py +++ b/dclient/cli.py @@ -1,4 +1,6 @@ import click +import httpx +import json @click.group() @@ -7,15 +9,25 @@ def cli(): "A client CLI utility for Datasette instances" -@cli.command(name="command") -@click.argument( - "example" -) -@click.option( - "-o", - "--option", - help="An example option", -) -def first_command(example, option): - "Command description goes here" - click.echo("Here is some output") +@cli.command() +@click.argument("url") +@click.argument("sql") +def query(url, sql): + """ + Run a SQL query against a Datasette database URL + + Returns a JSON array of objects + """ + if not url.endswith(".json"): + url += ".json" + response = httpx.get(url, params={"sql": sql, "_shape": "objects"}) + if response.status_code != 200 or not response.json()["ok"]: + data = response.json() + bits = [] + if data.get("title"): + bits.append(data["title"]) + if data.get("error"): + bits.append(data["error"]) + raise click.ClickException(": ".join(bits)) + else: + click.echo(json.dumps(response.json()["rows"], indent=2)) diff --git a/setup.py b/setup.py index ede667e..a083ca4 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup import os -VERSION = "0.1" +VERSION = "0.1a0" def get_long_description(): @@ -31,9 +31,7 @@ setup( [console_scripts] dclient=dclient.cli:cli """, - install_requires=["click"], - extras_require={ - "test": ["pytest"] - }, + install_requires=["click", "httpx"], + extras_require={"test": ["pytest", "pytest-httpx", "cogapp"]}, python_requires=">=3.7", ) diff --git a/tests/test_dclient.py b/tests/test_dclient.py deleted file mode 100644 index 5b0dc2c..0000000 --- a/tests/test_dclient.py +++ /dev/null @@ -1,10 +0,0 @@ -from click.testing import CliRunner -from dclient.cli import cli - - -def test_version(): - runner = CliRunner() - with runner.isolated_filesystem(): - result = runner.invoke(cli, ["--version"]) - assert result.exit_code == 0 - assert result.output.startswith("cli, version ") diff --git a/tests/test_query.py b/tests/test_query.py new file mode 100644 index 0000000..d89bccb --- /dev/null +++ b/tests/test_query.py @@ -0,0 +1,41 @@ +from click.testing import CliRunner +from dclient.cli import cli +import json + + +def test_query_error(httpx_mock): + httpx_mock.add_response( + json={ + "ok": False, + "error": "Statement must be a SELECT", + "status": 400, + "title": "Invalid SQL", + }, + status_code=400, + ) + runner = CliRunner() + result = runner.invoke(cli, ["query", "https://example.com", "hello"]) + assert result.exit_code == 1 + assert result.output == "Error: Invalid SQL: Statement must be a SELECT\n" + + +def test_query(httpx_mock): + httpx_mock.add_response( + json={ + "ok": True, + "database": "fixtures", + "query_name": None, + "rows": [{"5 * 2": 10}], + "truncated": False, + "columns": ["5 * 2"], + "query": {"sql": "select 5 * 2", "params": {}}, + "error": None, + "private": False, + "allow_execute_sql": True, + }, + status_code=200, + ) + runner = CliRunner() + result = runner.invoke(cli, ["query", "https://example.com", "hello"]) + assert result.exit_code == 0 + assert json.loads(result.output) == [{"5 * 2": 10}]