Added --table and --fmt options for table output using tabulate

This commit is contained in:
Simon Willison 2019-02-23 22:45:17 -08:00
commit 6c3f5e6479
4 changed files with 101 additions and 13 deletions

View file

@ -60,7 +60,7 @@ If you want to pretty-print the output further, you can pipe it through ``python
Running queries and returning CSV
=================================
You can use the ``--csv`` option to return results as CSV::
You can use the ``--csv`` option (or ``-c`` shortcut) to return results as CSV::
$ sqlite-utils dogs.db "select * from dogs" --csv
id,age,name
@ -73,6 +73,31 @@ This will default to including the column names as a header row. To exclude the
1,4,Cleo
2,2,Pancakes
.. _cli_query_table:
Running queries and outputting a table
======================================
You can use the ``--table`` option (or ``-t`` shortcut) to output query results as a table::
$ sqlite-utils dogs.db "select * from dogs" --table
id age name
---- ----- --------
1 4 Cleo
2 2 Pancakes
You can use the ``--fmt`` (or ``-f``) option to specify different table formats, for example ``rst`` for reStructuredText::
$ sqlite-utils dogs.db "select * from dogs" --table --fmt rst
==== ===== ========
id age name
==== ===== ========
1 4 Cleo
2 2 Pancakes
==== ===== ========
For a full list of table format options, run ``sqlite-utils query --help``.
.. _cli_rows:
Returning all rows in a table
@ -84,7 +109,7 @@ You can return every row in a specified table using the ``rows`` subcommand::
[{"id": 1, "age": 4, "name": "Cleo"},
{"id": 2, "age": 2, "name": "Pancakes"}]
This command accepts the same output options as ``query`` - so you can pass ``--nl``, ``--csv`` and ``--no-headers``.
This command accepts the same output options as ``query`` - so you can pass ``--nl``, ``--csv``, ``--no-headers``, ``--table`` and ``--fmt``.
.. _cli_tables:
@ -124,7 +149,7 @@ Use ``--columns`` to include a list of columns in each table::
{"table": "Gosh2", "count": 0, "columns": ["c1", "c2", "c3"]},
{"table": "dogs", "count": 2, "columns": ["id", "age", "name"]}]
The ``--nl``, ``--csv`` and ``--no-headers`` options are all available.
The ``--nl``, ``--csv`` and ``--table`` options are all available.
.. _cli_inserting_data:

View file

@ -22,7 +22,7 @@ setup(
version=VERSION,
license="Apache License, Version 2.0",
packages=find_packages(),
install_requires=["click", "click-default-group"],
install_requires=["click", "click-default-group", "tabulate"],
setup_requires=["pytest-runner"],
extras_require={"test": ["pytest", "black"]},
entry_points="""

View file

@ -5,6 +5,7 @@ import itertools
import json
import sys
import csv as csv_std
import tabulate
import sqlite3
@ -23,8 +24,17 @@ def output_options(fn):
is_flag=True,
default=False,
),
click.option("--csv", is_flag=True, help="Output CSV"),
click.option("-c", "--csv", is_flag=True, help="Output CSV"),
click.option("--no-headers", is_flag=True, help="Omit CSV headers"),
click.option("-t", "--table", is_flag=True, help="Output as a table"),
click.option(
"-f",
"--fmt",
help="Table format - one of {}".format(
", ".join(tabulate.tabulate_formats)
),
default="simple",
),
)
):
fn = decorator(fn)
@ -60,7 +70,7 @@ def cli():
is_flag=True,
default=False,
)
def tables(path, fts4, fts5, counts, nl, arrays, csv, no_headers, columns):
def tables(path, fts4, fts5, counts, nl, arrays, csv, no_headers, table, fmt, columns):
"""List the tables in the database"""
db = sqlite_utils.Database(path)
headers = ["table"]
@ -82,7 +92,9 @@ def tables(path, fts4, fts5, counts, nl, arrays, csv, no_headers, columns):
row.append(cols)
yield row
if csv:
if table:
print(tabulate.tabulate(_iter(), headers=headers, tablefmt=fmt))
elif csv:
writer = csv_std.writer(sys.stdout)
if not no_headers:
writer.writerow(headers)
@ -175,7 +187,7 @@ def insert_upsert_options(fn):
click.argument("json_file", type=click.File(), required=True),
click.option("--pk", help="Column to use as the primary key, e.g. id"),
click.option("--nl", is_flag=True, help="Expect newline-delimited JSON"),
click.option("--csv", is_flag=True, help="Expect CSV"),
click.option("-c", "--csv", is_flag=True, help="Expect CSV"),
click.option(
"--batch-size", type=int, default=100, help="Commit every X records"
),
@ -244,12 +256,14 @@ def upsert(path, table, json_file, pk, nl, csv, batch_size):
)
@click.argument("sql")
@output_options
def query(path, sql, nl, arrays, csv, no_headers):
def query(path, sql, nl, arrays, csv, no_headers, table, fmt):
"Execute SQL query and return the results as JSON"
db = sqlite_utils.Database(path)
cursor = iter(db.conn.execute(sql))
headers = [c[0] for c in cursor.description]
if csv:
if table:
print(tabulate.tabulate(list(cursor), headers=headers, tablefmt=fmt))
elif csv:
writer = csv_std.writer(sys.stdout)
if not no_headers:
writer.writerow([c[0] for c in cursor.description])
@ -266,19 +280,21 @@ def query(path, sql, nl, arrays, csv, no_headers):
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
required=True,
)
@click.argument("table")
@click.argument("dbtable")
@output_options
@click.pass_context
def rows(ctx, path, table, nl, arrays, csv, no_headers):
def rows(ctx, path, dbtable, nl, arrays, csv, no_headers, table, fmt):
"Output all rows in the specified table"
ctx.invoke(
query,
path=path,
sql="select * from [{}]".format(table),
sql="select * from [{}]".format(dbtable),
nl=nl,
arrays=arrays,
csv=csv,
no_headers=no_headers,
table=table,
fmt=fmt,
)

View file

@ -69,6 +69,53 @@ def test_tables_counts_and_columns_csv(db_path):
) == result.output.strip()
@pytest.mark.parametrize(
"fmt,expected",
[
(
"simple",
(
"c1 c2 c3\n"
"----- ----- ----------\n"
"verb0 noun0 adjective0\n"
"verb1 noun1 adjective1\n"
"verb2 noun2 adjective2\n"
"verb3 noun3 adjective3"
),
),
(
"rst",
(
"===== ===== ==========\n"
"c1 c2 c3\n"
"===== ===== ==========\n"
"verb0 noun0 adjective0\n"
"verb1 noun1 adjective1\n"
"verb2 noun2 adjective2\n"
"verb3 noun3 adjective3\n"
"===== ===== =========="
),
),
],
)
def test_output_table(db_path, fmt, expected):
db = Database(db_path)
with db.conn:
db["rows"].insert_all(
[
{
"c1": "verb{}".format(i),
"c2": "noun{}".format(i),
"c3": "adjective{}".format(i),
}
for i in range(4)
]
)
result = CliRunner().invoke(cli.cli, ["rows", db_path, "rows", "-t", "-f", fmt])
assert 0 == result.exit_code
assert expected == result.output.strip()
def test_enable_fts(db_path):
assert None == Database(db_path)["Gosh"].detect_fts()
result = CliRunner().invoke(