mirror of
https://github.com/simonw/dclient.git
synced 2026-07-25 18:34:32 +02:00
New `rows` command fetches rows from a Datasette table with support for: - `-f`/`--filter column op value` mapping to `?column__op=value` query params - `--where` for raw SQL WHERE clauses (multiple allowed) - `--sort` and `--sort-desc` for sorting (mutually exclusive) - `eq` operator alias maps to Datasette's `exact` filter - All other operators passed through directly to Datasette https://claude.ai/code/session_01W24ZNk4u2qJRZ51rzx4cN7
272 lines
7.5 KiB
Python
272 lines
7.5 KiB
Python
from click.testing import CliRunner
|
|
from dclient.cli import cli
|
|
import json
|
|
import pathlib
|
|
import pytest
|
|
|
|
|
|
def test_rows_basic(httpx_mock):
|
|
httpx_mock.add_response(
|
|
json={
|
|
"ok": True,
|
|
"rows": [{"id": 1, "name": "Cleo", "age": 4}],
|
|
"next": None,
|
|
},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["rows", "https://example.com/db", "creatures"])
|
|
assert result.exit_code == 0
|
|
assert json.loads(result.output) == [{"id": 1, "name": "Cleo", "age": 4}]
|
|
request = httpx_mock.get_request()
|
|
assert str(request.url) == "https://example.com/db/creatures.json?_shape=objects"
|
|
|
|
|
|
def test_rows_single_filter(httpx_mock):
|
|
httpx_mock.add_response(
|
|
json={"ok": True, "rows": [{"id": 2, "name": "Luna", "age": 7}]},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
["rows", "https://example.com/db", "creatures", "-f", "age", "gte", "5"],
|
|
)
|
|
assert result.exit_code == 0
|
|
request = httpx_mock.get_request()
|
|
assert "age__gte=5" in str(request.url)
|
|
assert "_shape=objects" in str(request.url)
|
|
|
|
|
|
def test_rows_eq_maps_to_exact(httpx_mock):
|
|
httpx_mock.add_response(
|
|
json={"ok": True, "rows": [{"id": 1, "name": "Cleo"}]},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
["rows", "https://example.com/db", "creatures", "-f", "name", "eq", "Cleo"],
|
|
)
|
|
assert result.exit_code == 0
|
|
request = httpx_mock.get_request()
|
|
assert "name__exact=Cleo" in str(request.url)
|
|
|
|
|
|
def test_rows_multiple_filters(httpx_mock):
|
|
httpx_mock.add_response(
|
|
json={"ok": True, "rows": []},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
[
|
|
"rows",
|
|
"https://example.com/db",
|
|
"creatures",
|
|
"-f",
|
|
"species",
|
|
"in",
|
|
"dog,cat",
|
|
"-f",
|
|
"age",
|
|
"gt",
|
|
"3",
|
|
],
|
|
)
|
|
assert result.exit_code == 0
|
|
request = httpx_mock.get_request()
|
|
url_str = str(request.url)
|
|
assert "species__in=" in url_str
|
|
assert "age__gt=3" in url_str
|
|
|
|
|
|
def test_rows_where_clause(httpx_mock):
|
|
httpx_mock.add_response(
|
|
json={"ok": True, "rows": [{"id": 6}]},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
["rows", "https://example.com/db", "creatures", "--where", "id > 5"],
|
|
)
|
|
assert result.exit_code == 0
|
|
request = httpx_mock.get_request()
|
|
assert "_where=" in str(request.url)
|
|
|
|
|
|
def test_rows_multiple_where_clauses(httpx_mock):
|
|
httpx_mock.add_response(
|
|
json={"ok": True, "rows": []},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
[
|
|
"rows",
|
|
"https://example.com/db",
|
|
"creatures",
|
|
"--where",
|
|
"id > 5",
|
|
"--where",
|
|
"age < 10",
|
|
],
|
|
)
|
|
assert result.exit_code == 0
|
|
request = httpx_mock.get_request()
|
|
url_str = str(request.url)
|
|
assert url_str.count("_where=") == 2
|
|
|
|
|
|
def test_rows_sort(httpx_mock):
|
|
httpx_mock.add_response(
|
|
json={"ok": True, "rows": [{"id": 1}]},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
["rows", "https://example.com/db", "creatures", "--sort", "name"],
|
|
)
|
|
assert result.exit_code == 0
|
|
request = httpx_mock.get_request()
|
|
assert "_sort=name" in str(request.url)
|
|
|
|
|
|
def test_rows_sort_desc(httpx_mock):
|
|
httpx_mock.add_response(
|
|
json={"ok": True, "rows": [{"id": 1}]},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
["rows", "https://example.com/db", "creatures", "--sort-desc", "age"],
|
|
)
|
|
assert result.exit_code == 0
|
|
request = httpx_mock.get_request()
|
|
assert "_sort_desc=age" in str(request.url)
|
|
|
|
|
|
def test_rows_sort_mutual_exclusion():
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
[
|
|
"rows",
|
|
"https://example.com/db",
|
|
"creatures",
|
|
"--sort",
|
|
"name",
|
|
"--sort-desc",
|
|
"age",
|
|
],
|
|
)
|
|
assert result.exit_code == 1
|
|
assert "Cannot use both --sort and --sort-desc" in result.output
|
|
|
|
|
|
@pytest.mark.parametrize("with_token", (False, True))
|
|
def test_rows_with_token(httpx_mock, with_token):
|
|
httpx_mock.add_response(
|
|
json={"ok": True, "rows": [{"id": 1}]},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
args = ["rows", "https://example.com/db", "creatures"]
|
|
if with_token:
|
|
args.extend(["--token", "xyz"])
|
|
result = runner.invoke(cli, args)
|
|
assert result.exit_code == 0
|
|
request = httpx_mock.get_request()
|
|
if with_token:
|
|
assert request.headers["authorization"] == "Bearer xyz"
|
|
else:
|
|
assert "authorization" not in request.headers
|
|
|
|
|
|
def test_rows_http_error(httpx_mock):
|
|
httpx_mock.add_response(
|
|
json={"title": "Not Found", "error": "Table not found: creatures"},
|
|
status_code=404,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["rows", "https://example.com/db", "creatures"])
|
|
assert result.exit_code == 1
|
|
assert "404 status code" in result.output
|
|
assert "Table not found: creatures" in result.output
|
|
|
|
|
|
def test_rows_ok_false_error(httpx_mock):
|
|
httpx_mock.add_response(
|
|
json={"ok": False, "error": "Something went wrong"},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["rows", "https://example.com/db", "creatures"])
|
|
assert result.exit_code == 1
|
|
assert "Something went wrong" in result.output
|
|
|
|
|
|
def test_rows_with_alias(httpx_mock, mocker, tmpdir):
|
|
mocker.patch("dclient.cli.get_config_dir", return_value=pathlib.Path(tmpdir))
|
|
aliases_file = pathlib.Path(tmpdir) / "aliases.json"
|
|
aliases_file.write_text(json.dumps({"content": "https://datasette.io/content"}))
|
|
httpx_mock.add_response(
|
|
json={"ok": True, "rows": [{"id": 1}]},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(cli, ["rows", "content", "creatures"])
|
|
assert result.exit_code == 0
|
|
request = httpx_mock.get_request()
|
|
assert request.url.host == "datasette.io"
|
|
assert request.url.path == "/content/creatures.json"
|
|
|
|
|
|
def test_rows_verbose(httpx_mock):
|
|
httpx_mock.add_response(
|
|
json={"ok": True, "rows": []},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
["rows", "https://example.com/db", "creatures", "-v"],
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "creatures.json" in result.output
|
|
|
|
|
|
def test_rows_combined(httpx_mock):
|
|
"""Test filters, where, and sort all together."""
|
|
httpx_mock.add_response(
|
|
json={"ok": True, "rows": [{"id": 1, "name": "Cleo", "age": 4}]},
|
|
status_code=200,
|
|
)
|
|
runner = CliRunner()
|
|
result = runner.invoke(
|
|
cli,
|
|
[
|
|
"rows",
|
|
"https://example.com/db",
|
|
"creatures",
|
|
"-f",
|
|
"species",
|
|
"eq",
|
|
"dog",
|
|
"--where",
|
|
"age > 2",
|
|
"--sort-desc",
|
|
"age",
|
|
],
|
|
)
|
|
assert result.exit_code == 0
|
|
request = httpx_mock.get_request()
|
|
url_str = str(request.url)
|
|
assert "species__exact=dog" in url_str
|
|
assert "_where=" in url_str
|
|
assert "_sort_desc=age" in url_str
|