Initial dclient query command, refs #1

This commit is contained in:
Simon Willison 2022-11-21 18:16:28 -08:00
commit 1ecb6a02e8
5 changed files with 94 additions and 27 deletions

View file

@ -23,6 +23,32 @@ You can also use:
python -m dclient --help
### dclient query
<!-- [[[cog
import cog
from dclient import cli
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(cli.cli, ["query", "--help"])
help = result.output.replace("Usage: cli", "Usage: dclient")
cog.out(
"```\n{}\n```".format(help)
)
]]] -->
```
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.
```
<!-- [[[end]]] -->
## Development
To contribute to this tool, first checkout the code. Then create a new virtual environment:

View file

@ -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))

View file

@ -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",
)

View file

@ -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 ")

41
tests/test_query.py Normal file
View file

@ -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}]