2020-07-26 17:48:36 -07:00
|
|
|
import base64
|
2018-07-28 06:43:18 -07:00
|
|
|
import click
|
2019-02-22 17:40:21 -08:00
|
|
|
from click_default_group import DefaultGroup
|
2020-07-27 00:08:57 -07:00
|
|
|
from datetime import datetime
|
|
|
|
|
import hashlib
|
|
|
|
|
import pathlib
|
2019-01-24 19:30:47 -08:00
|
|
|
import sqlite_utils
|
2019-02-24 13:33:45 -08:00
|
|
|
from sqlite_utils.db import AlterError
|
2019-01-26 10:58:45 -08:00
|
|
|
import itertools
|
2019-01-29 07:37:01 -08:00
|
|
|
import json
|
2019-01-25 07:50:20 -08:00
|
|
|
import sys
|
2019-02-22 17:40:21 -08:00
|
|
|
import csv as csv_std
|
2019-02-23 22:45:17 -08:00
|
|
|
import tabulate
|
2020-07-26 20:59:15 -07:00
|
|
|
from .utils import sqlite3, decode_base64_values
|
2018-07-28 06:43:18 -07:00
|
|
|
|
2020-05-02 20:55:40 -07:00
|
|
|
VALID_COLUMN_TYPES = ("INTEGER", "TEXT", "FLOAT", "BLOB")
|
|
|
|
|
|
2018-07-28 06:43:18 -07:00
|
|
|
|
2019-02-22 18:12:53 -08:00
|
|
|
def output_options(fn):
|
|
|
|
|
for decorator in reversed(
|
|
|
|
|
(
|
|
|
|
|
click.option(
|
|
|
|
|
"--nl",
|
|
|
|
|
help="Output newline-delimited JSON",
|
|
|
|
|
is_flag=True,
|
|
|
|
|
default=False,
|
|
|
|
|
),
|
|
|
|
|
click.option(
|
|
|
|
|
"--arrays",
|
|
|
|
|
help="Output rows as arrays instead of objects",
|
|
|
|
|
is_flag=True,
|
|
|
|
|
default=False,
|
|
|
|
|
),
|
2019-02-23 22:45:17 -08:00
|
|
|
click.option("-c", "--csv", is_flag=True, help="Output CSV"),
|
2019-02-22 18:12:53 -08:00
|
|
|
click.option("--no-headers", is_flag=True, help="Omit CSV headers"),
|
2019-02-23 22:45:17 -08:00
|
|
|
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",
|
|
|
|
|
),
|
2019-05-24 17:56:44 -07:00
|
|
|
click.option(
|
|
|
|
|
"--json-cols",
|
|
|
|
|
help="Detect JSON cols and output them as JSON, not escaped strings",
|
|
|
|
|
is_flag=True,
|
|
|
|
|
default=False,
|
|
|
|
|
),
|
2019-02-22 18:12:53 -08:00
|
|
|
)
|
|
|
|
|
):
|
|
|
|
|
fn = decorator(fn)
|
|
|
|
|
return fn
|
|
|
|
|
|
|
|
|
|
|
2019-02-22 17:40:21 -08:00
|
|
|
@click.group(cls=DefaultGroup, default="query", default_if_no_args=True)
|
2019-01-24 19:30:47 -08:00
|
|
|
@click.version_option()
|
|
|
|
|
def cli():
|
|
|
|
|
"Commands for interacting with a SQLite database"
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
2019-01-24 19:57:04 -08:00
|
|
|
@click.option(
|
|
|
|
|
"--fts4", help="Just show FTS4 enabled tables", default=False, is_flag=True
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--fts5", help="Just show FTS5 enabled tables", default=False, is_flag=True
|
|
|
|
|
)
|
2019-02-22 18:12:53 -08:00
|
|
|
@click.option(
|
|
|
|
|
"--counts", help="Include row counts per table", default=False, is_flag=True
|
|
|
|
|
)
|
|
|
|
|
@output_options
|
|
|
|
|
@click.option(
|
|
|
|
|
"--columns",
|
|
|
|
|
help="Include list of columns for each table",
|
|
|
|
|
is_flag=True,
|
|
|
|
|
default=False,
|
|
|
|
|
)
|
2020-05-01 10:09:36 -07:00
|
|
|
@click.option(
|
|
|
|
|
"--schema", help="Include schema for each table", is_flag=True, default=False,
|
|
|
|
|
)
|
2019-05-24 17:56:44 -07:00
|
|
|
def tables(
|
|
|
|
|
path,
|
|
|
|
|
fts4,
|
|
|
|
|
fts5,
|
|
|
|
|
counts,
|
|
|
|
|
nl,
|
|
|
|
|
arrays,
|
|
|
|
|
csv,
|
|
|
|
|
no_headers,
|
|
|
|
|
table,
|
|
|
|
|
fmt,
|
|
|
|
|
json_cols,
|
2020-05-01 10:09:36 -07:00
|
|
|
columns,
|
|
|
|
|
schema,
|
2020-05-01 13:38:28 -07:00
|
|
|
views=False,
|
2019-05-24 17:56:44 -07:00
|
|
|
):
|
2019-01-24 19:30:47 -08:00
|
|
|
"""List the tables in the database"""
|
|
|
|
|
db = sqlite_utils.Database(path)
|
2020-05-01 13:38:28 -07:00
|
|
|
headers = ["view" if views else "table"]
|
2019-02-22 18:12:53 -08:00
|
|
|
if counts:
|
|
|
|
|
headers.append("count")
|
|
|
|
|
if columns:
|
|
|
|
|
headers.append("columns")
|
2020-05-01 10:09:36 -07:00
|
|
|
if schema:
|
|
|
|
|
headers.append("schema")
|
2019-02-22 18:12:53 -08:00
|
|
|
|
|
|
|
|
def _iter():
|
2020-05-01 13:38:28 -07:00
|
|
|
if views:
|
|
|
|
|
items = db.view_names()
|
|
|
|
|
else:
|
|
|
|
|
items = db.table_names(fts4=fts4, fts5=fts5)
|
|
|
|
|
for name in items:
|
2019-02-22 18:12:53 -08:00
|
|
|
row = [name]
|
|
|
|
|
if counts:
|
|
|
|
|
row.append(db[name].count)
|
|
|
|
|
if columns:
|
|
|
|
|
cols = [c.name for c in db[name].columns]
|
|
|
|
|
if csv:
|
|
|
|
|
row.append("\n".join(cols))
|
|
|
|
|
else:
|
|
|
|
|
row.append(cols)
|
2020-05-01 10:09:36 -07:00
|
|
|
if schema:
|
|
|
|
|
row.append(db[name].schema)
|
2019-02-22 18:12:53 -08:00
|
|
|
yield row
|
|
|
|
|
|
2019-02-23 22:45:17 -08:00
|
|
|
if table:
|
|
|
|
|
print(tabulate.tabulate(_iter(), headers=headers, tablefmt=fmt))
|
|
|
|
|
elif csv:
|
2019-02-22 18:12:53 -08:00
|
|
|
writer = csv_std.writer(sys.stdout)
|
|
|
|
|
if not no_headers:
|
|
|
|
|
writer.writerow(headers)
|
|
|
|
|
for row in _iter():
|
|
|
|
|
writer.writerow(row)
|
|
|
|
|
else:
|
2019-05-24 17:56:44 -07:00
|
|
|
for line in output_rows(_iter(), headers, nl, arrays, json_cols):
|
2019-02-22 18:12:53 -08:00
|
|
|
click.echo(line)
|
2019-01-24 19:39:04 -08:00
|
|
|
|
|
|
|
|
|
2020-05-01 13:38:28 -07:00
|
|
|
@cli.command()
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--counts", help="Include row counts per view", default=False, is_flag=True
|
|
|
|
|
)
|
|
|
|
|
@output_options
|
|
|
|
|
@click.option(
|
|
|
|
|
"--columns",
|
|
|
|
|
help="Include list of columns for each view",
|
|
|
|
|
is_flag=True,
|
|
|
|
|
default=False,
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--schema", help="Include schema for each view", is_flag=True, default=False,
|
|
|
|
|
)
|
|
|
|
|
def views(
|
|
|
|
|
path, counts, nl, arrays, csv, no_headers, table, fmt, json_cols, columns, schema,
|
|
|
|
|
):
|
|
|
|
|
"""List the views in the database"""
|
|
|
|
|
tables.callback(
|
|
|
|
|
path=path,
|
|
|
|
|
fts4=False,
|
|
|
|
|
fts5=False,
|
|
|
|
|
counts=counts,
|
|
|
|
|
nl=nl,
|
|
|
|
|
arrays=arrays,
|
|
|
|
|
csv=csv,
|
|
|
|
|
no_headers=no_headers,
|
|
|
|
|
table=table,
|
|
|
|
|
fmt=fmt,
|
|
|
|
|
json_cols=json_cols,
|
|
|
|
|
columns=columns,
|
|
|
|
|
schema=schema,
|
|
|
|
|
views=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2019-01-24 19:39:04 -08:00
|
|
|
@cli.command()
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
def vacuum(path):
|
|
|
|
|
"""Run VACUUM against the database"""
|
|
|
|
|
sqlite_utils.Database(path).vacuum()
|
2019-01-24 20:35:51 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.option("--no-vacuum", help="Don't run VACUUM", default=False, is_flag=True)
|
|
|
|
|
def optimize(path, no_vacuum):
|
|
|
|
|
"""Optimize all FTS tables and then run VACUUM - should shrink the database file"""
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
tables = db.table_names(fts4=True) + db.table_names(fts5=True)
|
|
|
|
|
with db.conn:
|
|
|
|
|
for table in tables:
|
|
|
|
|
db[table].optimize()
|
|
|
|
|
if not no_vacuum:
|
|
|
|
|
db.vacuum()
|
2019-01-24 21:06:41 -08:00
|
|
|
|
|
|
|
|
|
2019-02-24 12:04:33 -08:00
|
|
|
@cli.command(name="add-column")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("table")
|
|
|
|
|
@click.argument("col_name")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"col_type",
|
|
|
|
|
type=click.Choice(
|
|
|
|
|
["integer", "float", "blob", "text", "INTEGER", "FLOAT", "BLOB", "TEXT"]
|
|
|
|
|
),
|
2019-02-24 14:24:00 -08:00
|
|
|
required=False,
|
2019-02-24 12:04:33 -08:00
|
|
|
)
|
2019-06-12 18:35:02 -07:00
|
|
|
@click.option(
|
|
|
|
|
"--fk", type=str, required=False, help="Table to reference as a foreign key"
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--fk-col",
|
|
|
|
|
type=str,
|
|
|
|
|
required=False,
|
|
|
|
|
help="Referenced column on that foreign key table - if omitted will automatically use the primary key",
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--not-null-default",
|
|
|
|
|
type=str,
|
|
|
|
|
required=False,
|
|
|
|
|
help="Add NOT NULL DEFAULT 'TEXT' constraint",
|
|
|
|
|
)
|
|
|
|
|
def add_column(path, table, col_name, col_type, fk, fk_col, not_null_default):
|
2019-02-24 12:04:33 -08:00
|
|
|
"Add a column to the specified table"
|
|
|
|
|
db = sqlite_utils.Database(path)
|
2019-06-12 18:35:02 -07:00
|
|
|
db[table].add_column(
|
|
|
|
|
col_name, col_type, fk=fk, fk_col=fk_col, not_null_default=not_null_default
|
|
|
|
|
)
|
2019-02-24 12:04:33 -08:00
|
|
|
|
|
|
|
|
|
2019-02-24 13:33:45 -08:00
|
|
|
@cli.command(name="add-foreign-key")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("table")
|
|
|
|
|
@click.argument("column")
|
2019-06-12 21:51:09 -07:00
|
|
|
@click.argument("other_table", required=False)
|
|
|
|
|
@click.argument("other_column", required=False)
|
2019-02-24 13:33:45 -08:00
|
|
|
def add_foreign_key(path, table, column, other_table, other_column):
|
|
|
|
|
"""
|
|
|
|
|
Add a new foreign key constraint to an existing table. Example usage:
|
|
|
|
|
|
|
|
|
|
$ sqlite-utils add-foreign-key my.db books author_id authors id
|
|
|
|
|
|
|
|
|
|
WARNING: Could corrupt your database! Back up your database file first.
|
|
|
|
|
"""
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
try:
|
|
|
|
|
db[table].add_foreign_key(column, other_table, other_column)
|
|
|
|
|
except AlterError as e:
|
|
|
|
|
raise click.ClickException(e)
|
|
|
|
|
|
|
|
|
|
|
2019-06-30 16:50:54 -07:00
|
|
|
@cli.command(name="index-foreign-keys")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
def index_foreign_keys(path):
|
|
|
|
|
"""
|
|
|
|
|
Ensure every foreign key column has an index on it.
|
|
|
|
|
"""
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
db.index_foreign_keys()
|
|
|
|
|
|
|
|
|
|
|
2019-02-24 11:11:21 -08:00
|
|
|
@cli.command(name="create-index")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("table")
|
|
|
|
|
@click.argument("column", nargs=-1, required=True)
|
|
|
|
|
@click.option("--name", help="Explicit name for the new index")
|
|
|
|
|
@click.option("--unique", help="Make this a unique index", default=False, is_flag=True)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--if-not-exists",
|
|
|
|
|
help="Ignore if index already exists",
|
|
|
|
|
default=False,
|
|
|
|
|
is_flag=True,
|
|
|
|
|
)
|
|
|
|
|
def create_index(path, table, column, name, unique, if_not_exists):
|
|
|
|
|
"Add an index to the specified table covering the specified columns"
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
db[table].create_index(
|
|
|
|
|
column, index_name=name, unique=unique, if_not_exists=if_not_exists
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2019-02-07 21:18:24 -08:00
|
|
|
@cli.command(name="enable-fts")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("table")
|
|
|
|
|
@click.argument("column", nargs=-1, required=True)
|
2019-05-24 17:56:44 -07:00
|
|
|
@click.option("--fts4", help="Use FTS4", default=False, is_flag=True)
|
|
|
|
|
@click.option("--fts5", help="Use FTS5", default=False, is_flag=True)
|
2019-09-02 16:42:28 -07:00
|
|
|
@click.option(
|
|
|
|
|
"--create-triggers",
|
|
|
|
|
help="Create triggers to update the FTS tables when the parent table changes.",
|
|
|
|
|
default=False,
|
|
|
|
|
is_flag=True,
|
|
|
|
|
)
|
|
|
|
|
def enable_fts(path, table, column, fts4, fts5, create_triggers):
|
2019-02-07 21:25:26 -08:00
|
|
|
"Enable FTS for specific table and columns"
|
2019-02-07 21:18:24 -08:00
|
|
|
fts_version = "FTS5"
|
|
|
|
|
if fts4 and fts5:
|
|
|
|
|
click.echo("Can only use one of --fts4 or --fts5", err=True)
|
|
|
|
|
return
|
|
|
|
|
elif fts4:
|
|
|
|
|
fts_version = "FTS4"
|
|
|
|
|
|
|
|
|
|
db = sqlite_utils.Database(path)
|
2019-09-02 16:42:28 -07:00
|
|
|
db[table].enable_fts(
|
|
|
|
|
column, fts_version=fts_version, create_triggers=create_triggers
|
|
|
|
|
)
|
2019-02-07 21:18:24 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command(name="populate-fts")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("table")
|
|
|
|
|
@click.argument("column", nargs=-1, required=True)
|
|
|
|
|
def populate_fts(path, table, column):
|
2019-02-07 21:25:26 -08:00
|
|
|
"Re-populate FTS for specific table and columns"
|
2019-02-07 21:18:24 -08:00
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
db[table].populate_fts(column)
|
|
|
|
|
|
|
|
|
|
|
2020-02-26 20:40:35 -08:00
|
|
|
@cli.command(name="disable-fts")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(exists=True, file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("table")
|
|
|
|
|
def disable_fts(path, table):
|
|
|
|
|
"Disable FTS for specific table"
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
db[table].disable_fts()
|
|
|
|
|
|
|
|
|
|
|
2019-02-06 21:50:25 -08:00
|
|
|
def insert_upsert_options(fn):
|
|
|
|
|
for decorator in reversed(
|
|
|
|
|
(
|
|
|
|
|
click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
),
|
|
|
|
|
click.argument("table"),
|
|
|
|
|
click.argument("json_file", type=click.File(), required=True),
|
2019-07-14 21:28:51 -07:00
|
|
|
click.option(
|
|
|
|
|
"--pk", help="Columns to use as the primary key, e.g. id", multiple=True
|
|
|
|
|
),
|
2019-02-06 21:50:25 -08:00
|
|
|
click.option("--nl", is_flag=True, help="Expect newline-delimited JSON"),
|
2019-02-23 22:45:17 -08:00
|
|
|
click.option("-c", "--csv", is_flag=True, help="Expect CSV"),
|
2019-07-18 21:50:38 -07:00
|
|
|
click.option("--tsv", is_flag=True, help="Expect TSV"),
|
2019-02-06 21:50:25 -08:00
|
|
|
click.option(
|
|
|
|
|
"--batch-size", type=int, default=100, help="Commit every X records"
|
|
|
|
|
),
|
2019-05-24 17:41:04 -07:00
|
|
|
click.option(
|
|
|
|
|
"--alter",
|
|
|
|
|
is_flag=True,
|
|
|
|
|
help="Alter existing table to add any missing columns",
|
|
|
|
|
),
|
2019-06-12 23:30:16 -07:00
|
|
|
click.option(
|
|
|
|
|
"--not-null",
|
|
|
|
|
multiple=True,
|
|
|
|
|
help="Columns that should be created as NOT NULL",
|
|
|
|
|
),
|
|
|
|
|
click.option(
|
|
|
|
|
"--default",
|
|
|
|
|
multiple=True,
|
|
|
|
|
type=(str, str),
|
|
|
|
|
help="Default value that should be set for a column",
|
|
|
|
|
),
|
2019-02-06 21:50:25 -08:00
|
|
|
)
|
|
|
|
|
):
|
|
|
|
|
fn = decorator(fn)
|
|
|
|
|
return fn
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def insert_upsert_implementation(
|
2019-06-12 23:30:16 -07:00
|
|
|
path,
|
|
|
|
|
table,
|
|
|
|
|
json_file,
|
|
|
|
|
pk,
|
|
|
|
|
nl,
|
|
|
|
|
csv,
|
2019-07-18 21:50:38 -07:00
|
|
|
tsv,
|
2019-06-12 23:30:16 -07:00
|
|
|
batch_size,
|
|
|
|
|
alter,
|
|
|
|
|
upsert,
|
|
|
|
|
ignore=False,
|
2019-12-27 09:15:31 +00:00
|
|
|
replace=False,
|
2020-07-06 14:18:23 -07:00
|
|
|
truncate=False,
|
2019-06-12 23:30:16 -07:00
|
|
|
not_null=None,
|
|
|
|
|
default=None,
|
2019-02-06 21:50:25 -08:00
|
|
|
):
|
2019-01-24 21:06:41 -08:00
|
|
|
db = sqlite_utils.Database(path)
|
2019-07-18 21:50:38 -07:00
|
|
|
if (nl + csv + tsv) >= 2:
|
|
|
|
|
raise click.ClickException("Use just one of --nl, --csv or --tsv")
|
2019-07-14 21:28:51 -07:00
|
|
|
if pk and len(pk) == 1:
|
|
|
|
|
pk = pk[0]
|
2019-07-18 21:50:38 -07:00
|
|
|
if csv or tsv:
|
|
|
|
|
dialect = "excel-tab" if tsv else "excel"
|
|
|
|
|
reader = csv_std.reader(json_file, dialect=dialect)
|
2019-01-29 07:28:12 -08:00
|
|
|
headers = next(reader)
|
|
|
|
|
docs = (dict(zip(headers, row)) for row in reader)
|
|
|
|
|
elif nl:
|
2019-01-29 07:37:01 -08:00
|
|
|
docs = (json.loads(line) for line in json_file)
|
2019-01-27 18:17:38 -08:00
|
|
|
else:
|
2019-01-29 07:37:01 -08:00
|
|
|
docs = json.load(json_file)
|
2019-01-27 18:17:38 -08:00
|
|
|
if isinstance(docs, dict):
|
|
|
|
|
docs = [docs]
|
2020-07-06 14:18:23 -07:00
|
|
|
extra_kwargs = {"ignore": ignore, "replace": replace, "truncate": truncate}
|
2019-06-12 23:30:16 -07:00
|
|
|
if not_null:
|
|
|
|
|
extra_kwargs["not_null"] = set(not_null)
|
|
|
|
|
if default:
|
|
|
|
|
extra_kwargs["defaults"] = dict(default)
|
2019-12-29 21:03:43 -08:00
|
|
|
if upsert:
|
|
|
|
|
extra_kwargs["upsert"] = upsert
|
2020-07-26 20:59:15 -07:00
|
|
|
# Apply {"$base64": true, ...} decoding, if needed
|
|
|
|
|
docs = (decode_base64_values(doc) for doc in docs)
|
2019-12-27 09:30:29 +00:00
|
|
|
db[table].insert_all(
|
|
|
|
|
docs, pk=pk, batch_size=batch_size, alter=alter, **extra_kwargs
|
|
|
|
|
)
|
2019-01-24 21:20:10 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
2019-02-06 21:50:25 -08:00
|
|
|
@insert_upsert_options
|
2019-05-28 21:15:57 -07:00
|
|
|
@click.option(
|
|
|
|
|
"--ignore", is_flag=True, default=False, help="Ignore records if pk already exists"
|
|
|
|
|
)
|
2019-12-27 09:15:31 +00:00
|
|
|
@click.option(
|
2019-12-27 09:30:29 +00:00
|
|
|
"--replace",
|
|
|
|
|
is_flag=True,
|
|
|
|
|
default=False,
|
|
|
|
|
help="Replace records if pk already exists",
|
2019-12-27 09:15:31 +00:00
|
|
|
)
|
2020-07-06 14:18:23 -07:00
|
|
|
@click.option(
|
|
|
|
|
"--truncate",
|
|
|
|
|
is_flag=True,
|
|
|
|
|
default=False,
|
|
|
|
|
help="Truncate table before inserting records, if table already exists",
|
|
|
|
|
)
|
2019-06-12 23:30:16 -07:00
|
|
|
def insert(
|
2019-07-18 21:50:38 -07:00
|
|
|
path,
|
|
|
|
|
table,
|
|
|
|
|
json_file,
|
|
|
|
|
pk,
|
|
|
|
|
nl,
|
|
|
|
|
csv,
|
|
|
|
|
tsv,
|
|
|
|
|
batch_size,
|
|
|
|
|
alter,
|
|
|
|
|
ignore,
|
2019-12-27 09:15:31 +00:00
|
|
|
replace,
|
2020-07-06 14:18:23 -07:00
|
|
|
truncate,
|
2019-07-18 21:50:38 -07:00
|
|
|
not_null,
|
|
|
|
|
default,
|
2019-06-12 23:30:16 -07:00
|
|
|
):
|
2019-02-06 21:50:25 -08:00
|
|
|
"""
|
|
|
|
|
Insert records from JSON file into a table, creating the table if it
|
|
|
|
|
does not already exist.
|
|
|
|
|
|
|
|
|
|
Input should be a JSON array of objects, unless --nl or --csv is used.
|
|
|
|
|
"""
|
|
|
|
|
insert_upsert_implementation(
|
2019-05-28 21:15:57 -07:00
|
|
|
path,
|
|
|
|
|
table,
|
|
|
|
|
json_file,
|
|
|
|
|
pk,
|
|
|
|
|
nl,
|
|
|
|
|
csv,
|
2019-07-18 21:50:38 -07:00
|
|
|
tsv,
|
2019-05-28 21:15:57 -07:00
|
|
|
batch_size,
|
|
|
|
|
alter=alter,
|
|
|
|
|
upsert=False,
|
|
|
|
|
ignore=ignore,
|
2019-12-27 09:15:31 +00:00
|
|
|
replace=replace,
|
2020-07-06 14:18:23 -07:00
|
|
|
truncate=truncate,
|
2019-06-12 23:30:16 -07:00
|
|
|
not_null=not_null,
|
|
|
|
|
default=default,
|
2019-02-06 21:50:25 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@cli.command()
|
|
|
|
|
@insert_upsert_options
|
2019-07-18 21:50:38 -07:00
|
|
|
def upsert(
|
|
|
|
|
path, table, json_file, pk, nl, csv, tsv, batch_size, alter, not_null, default
|
|
|
|
|
):
|
2019-02-06 21:50:25 -08:00
|
|
|
"""
|
|
|
|
|
Upsert records based on their primary key. Works like 'insert' but if
|
|
|
|
|
an incoming record has a primary key that matches an existing record
|
2019-12-29 22:05:31 -08:00
|
|
|
the existing record will be updated.
|
2019-02-06 21:50:25 -08:00
|
|
|
"""
|
|
|
|
|
insert_upsert_implementation(
|
2019-06-12 23:30:16 -07:00
|
|
|
path,
|
|
|
|
|
table,
|
|
|
|
|
json_file,
|
|
|
|
|
pk,
|
|
|
|
|
nl,
|
|
|
|
|
csv,
|
2019-07-18 21:50:38 -07:00
|
|
|
tsv,
|
2019-06-12 23:30:16 -07:00
|
|
|
batch_size,
|
|
|
|
|
alter=alter,
|
|
|
|
|
upsert=True,
|
|
|
|
|
not_null=not_null,
|
|
|
|
|
default=default,
|
2019-02-06 21:50:25 -08:00
|
|
|
)
|
2019-01-25 07:50:20 -08:00
|
|
|
|
|
|
|
|
|
2020-05-02 20:55:40 -07:00
|
|
|
@cli.command(name="create-table")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("table")
|
|
|
|
|
@click.argument("columns", nargs=-1, required=True)
|
|
|
|
|
@click.option("--pk", help="Column to use as primary key")
|
2020-05-03 08:09:00 -07:00
|
|
|
@click.option(
|
|
|
|
|
"--not-null", multiple=True, help="Columns that should be created as NOT NULL",
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--default",
|
|
|
|
|
multiple=True,
|
|
|
|
|
type=(str, str),
|
|
|
|
|
help="Default value that should be set for a column",
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--fk",
|
|
|
|
|
multiple=True,
|
|
|
|
|
type=(str, str, str),
|
|
|
|
|
help="Column, other table, other column to set as a foreign key",
|
|
|
|
|
)
|
2020-05-03 08:24:39 -07:00
|
|
|
@click.option(
|
|
|
|
|
"--ignore", is_flag=True, help="If table already exists, do nothing",
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--replace", is_flag=True, help="If table already exists, replace it",
|
|
|
|
|
)
|
|
|
|
|
def create_table(path, table, columns, pk, not_null, default, fk, ignore, replace):
|
2020-05-02 20:55:40 -07:00
|
|
|
"Add an index to the specified table covering the specified columns"
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
if len(columns) % 2 == 1:
|
|
|
|
|
raise click.ClickException(
|
|
|
|
|
"columns must be an even number of 'name' 'type' pairs"
|
|
|
|
|
)
|
|
|
|
|
coltypes = {}
|
|
|
|
|
columns = list(columns)
|
|
|
|
|
while columns:
|
|
|
|
|
name = columns.pop(0)
|
|
|
|
|
ctype = columns.pop(0)
|
|
|
|
|
if ctype.upper() not in VALID_COLUMN_TYPES:
|
|
|
|
|
raise click.ClickException(
|
|
|
|
|
"column types must be one of {}".format(VALID_COLUMN_TYPES)
|
|
|
|
|
)
|
|
|
|
|
coltypes[name] = ctype.upper()
|
2020-05-03 08:24:39 -07:00
|
|
|
# Does table already exist?
|
|
|
|
|
if table in db.table_names():
|
|
|
|
|
if ignore:
|
|
|
|
|
return
|
|
|
|
|
elif replace:
|
|
|
|
|
db[table].drop()
|
|
|
|
|
else:
|
|
|
|
|
raise click.ClickException(
|
|
|
|
|
'Table "{}" already exists. Use --replace to delete and replace it.'.format(
|
|
|
|
|
table
|
|
|
|
|
)
|
|
|
|
|
)
|
2020-05-03 08:09:00 -07:00
|
|
|
db[table].create(
|
|
|
|
|
coltypes, pk=pk, not_null=not_null, defaults=dict(default), foreign_keys=fk
|
|
|
|
|
)
|
2020-05-02 20:55:40 -07:00
|
|
|
|
|
|
|
|
|
2020-05-10 17:44:21 -07:00
|
|
|
@cli.command(name="drop-table")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("table")
|
|
|
|
|
def drop_table(path, table):
|
|
|
|
|
"Drop the specified table"
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
if table in db.table_names():
|
|
|
|
|
db[table].drop()
|
|
|
|
|
else:
|
|
|
|
|
raise click.ClickException('Table "{}" does not exist'.format(table))
|
|
|
|
|
|
|
|
|
|
|
2020-05-03 08:36:29 -07:00
|
|
|
@cli.command(name="create-view")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("view")
|
|
|
|
|
@click.argument("select")
|
|
|
|
|
@click.option(
|
|
|
|
|
"--ignore", is_flag=True, help="If view already exists, do nothing",
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"--replace", is_flag=True, help="If view already exists, replace it",
|
|
|
|
|
)
|
|
|
|
|
def create_view(path, view, select, ignore, replace):
|
|
|
|
|
"Create a view for the provided SELECT query"
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
# Does view already exist?
|
|
|
|
|
if view in db.view_names():
|
|
|
|
|
if ignore:
|
|
|
|
|
return
|
|
|
|
|
elif replace:
|
|
|
|
|
db[view].drop()
|
|
|
|
|
else:
|
|
|
|
|
raise click.ClickException(
|
|
|
|
|
'View "{}" already exists. Use --replace to delete and replace it.'.format(
|
|
|
|
|
view
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
db.create_view(view, select)
|
|
|
|
|
|
|
|
|
|
|
2020-05-10 17:44:21 -07:00
|
|
|
@cli.command(name="drop-view")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("view")
|
|
|
|
|
def drop_view(path, view):
|
|
|
|
|
"Drop the specified view"
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
if view in db.view_names():
|
|
|
|
|
db[view].drop()
|
|
|
|
|
else:
|
|
|
|
|
raise click.ClickException('View "{}" does not exist'.format(view))
|
|
|
|
|
|
|
|
|
|
|
2019-02-22 17:40:21 -08:00
|
|
|
@cli.command()
|
2019-01-25 18:06:29 -08:00
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("sql")
|
2019-02-22 17:40:21 -08:00
|
|
|
@output_options
|
2020-07-26 09:43:45 -07:00
|
|
|
@click.option("-r", "--raw", is_flag=True, help="Raw output, first column of first row")
|
2020-07-26 20:53:51 -07:00
|
|
|
@click.option(
|
|
|
|
|
"-p",
|
|
|
|
|
"--param",
|
|
|
|
|
multiple=True,
|
|
|
|
|
type=(str, str),
|
|
|
|
|
help="Named :parameters for SQL query",
|
|
|
|
|
)
|
|
|
|
|
def query(path, sql, nl, arrays, csv, no_headers, table, fmt, json_cols, raw, param):
|
2019-01-25 18:06:29 -08:00
|
|
|
"Execute SQL query and return the results as JSON"
|
|
|
|
|
db = sqlite_utils.Database(path)
|
2020-07-07 22:14:04 -07:00
|
|
|
with db.conn:
|
2020-07-26 20:53:51 -07:00
|
|
|
cursor = db.conn.execute(sql, dict(param))
|
2020-07-07 22:14:04 -07:00
|
|
|
if cursor.description is None:
|
|
|
|
|
# This was an update/insert
|
|
|
|
|
headers = ["rows_affected"]
|
|
|
|
|
cursor = [[cursor.rowcount]]
|
|
|
|
|
else:
|
|
|
|
|
headers = [c[0] for c in cursor.description]
|
2020-07-26 09:43:45 -07:00
|
|
|
if raw:
|
|
|
|
|
data = cursor.fetchone()[0]
|
|
|
|
|
if isinstance(data, bytes):
|
|
|
|
|
sys.stdout.buffer.write(data)
|
|
|
|
|
else:
|
|
|
|
|
sys.stdout.write(str(data))
|
|
|
|
|
elif table:
|
2020-07-07 22:14:04 -07:00
|
|
|
print(tabulate.tabulate(list(cursor), headers=headers, tablefmt=fmt))
|
|
|
|
|
elif csv:
|
|
|
|
|
writer = csv_std.writer(sys.stdout)
|
|
|
|
|
if not no_headers:
|
|
|
|
|
writer.writerow(headers)
|
|
|
|
|
for row in cursor:
|
|
|
|
|
writer.writerow(row)
|
|
|
|
|
else:
|
|
|
|
|
for line in output_rows(cursor, headers, nl, arrays, json_cols):
|
|
|
|
|
click.echo(line)
|
2019-02-22 17:40:21 -08:00
|
|
|
|
|
|
|
|
|
2019-02-22 17:52:17 -08:00
|
|
|
@cli.command()
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
2019-02-23 22:45:17 -08:00
|
|
|
@click.argument("dbtable")
|
2019-02-22 17:52:17 -08:00
|
|
|
@output_options
|
|
|
|
|
@click.pass_context
|
2019-05-24 17:56:44 -07:00
|
|
|
def rows(ctx, path, dbtable, nl, arrays, csv, no_headers, table, fmt, json_cols):
|
2019-02-22 17:52:17 -08:00
|
|
|
"Output all rows in the specified table"
|
|
|
|
|
ctx.invoke(
|
|
|
|
|
query,
|
|
|
|
|
path=path,
|
2019-02-23 22:45:17 -08:00
|
|
|
sql="select * from [{}]".format(dbtable),
|
2019-02-22 17:52:17 -08:00
|
|
|
nl=nl,
|
|
|
|
|
arrays=arrays,
|
|
|
|
|
csv=csv,
|
|
|
|
|
no_headers=no_headers,
|
2019-02-23 22:45:17 -08:00
|
|
|
table=table,
|
|
|
|
|
fmt=fmt,
|
2019-05-27 17:47:59 -07:00
|
|
|
json_cols=json_cols,
|
2019-02-22 17:52:17 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2020-07-27 00:08:57 -07:00
|
|
|
@cli.command(name="insert-files")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"path",
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=False, allow_dash=False),
|
|
|
|
|
required=True,
|
|
|
|
|
)
|
|
|
|
|
@click.argument("table")
|
|
|
|
|
@click.argument(
|
|
|
|
|
"file_or_dir",
|
|
|
|
|
nargs=-1,
|
|
|
|
|
required=True,
|
|
|
|
|
type=click.Path(file_okay=True, dir_okay=True, allow_dash=True),
|
|
|
|
|
)
|
|
|
|
|
@click.option(
|
|
|
|
|
"-c", "--column", type=str, multiple=True, help="Column definitions for the table",
|
|
|
|
|
)
|
|
|
|
|
@click.option("--pk", type=str, help="Column to use as primary key")
|
|
|
|
|
@click.option("--alter", is_flag=True, help="Alter table to add missing columns")
|
|
|
|
|
@click.option("--replace", is_flag=True, help="Replace files with matching primary key")
|
|
|
|
|
@click.option("--upsert", is_flag=True, help="Upsert files with matching primary key")
|
|
|
|
|
def insert_files(path, table, file_or_dir, column, pk, alter, replace, upsert):
|
|
|
|
|
"""
|
|
|
|
|
Insert one or more files using BLOB columns in the specified table
|
|
|
|
|
|
|
|
|
|
Example usage:
|
|
|
|
|
|
|
|
|
|
\b
|
|
|
|
|
sqlite-utils insert-files pics.db images *.gif \\
|
|
|
|
|
-c name:name \\
|
|
|
|
|
-c content:content \\
|
|
|
|
|
-c content_hash:sha256 \\
|
|
|
|
|
-c created:ctime_iso \\
|
|
|
|
|
-c modified:mtime_iso \\
|
|
|
|
|
-c size:size \\
|
|
|
|
|
--pk name
|
|
|
|
|
"""
|
|
|
|
|
if not column:
|
|
|
|
|
column = ["path:path", "content:content", "size:size"]
|
|
|
|
|
if not pk:
|
|
|
|
|
pk = "path"
|
|
|
|
|
|
|
|
|
|
def yield_paths_and_relative_paths():
|
|
|
|
|
for f_or_d in file_or_dir:
|
|
|
|
|
path = pathlib.Path(f_or_d)
|
|
|
|
|
if path.is_dir():
|
|
|
|
|
for subpath in path.rglob("*"):
|
|
|
|
|
if subpath.is_file():
|
|
|
|
|
yield subpath, subpath.relative_to(path)
|
|
|
|
|
elif path.is_file():
|
|
|
|
|
yield path, path
|
|
|
|
|
|
|
|
|
|
# Load all paths so we can show a progress bar
|
|
|
|
|
paths_and_relative_paths = list(yield_paths_and_relative_paths())
|
|
|
|
|
|
|
|
|
|
with click.progressbar(paths_and_relative_paths) as bar:
|
|
|
|
|
|
|
|
|
|
def to_insert():
|
|
|
|
|
for path, relative_path in bar:
|
|
|
|
|
row = {}
|
|
|
|
|
for coldef in column:
|
|
|
|
|
if ":" in coldef:
|
|
|
|
|
colname, coltype = coldef.rsplit(":", 1)
|
|
|
|
|
else:
|
|
|
|
|
colname, coltype = coldef, coldef
|
|
|
|
|
try:
|
|
|
|
|
if coltype == "path":
|
|
|
|
|
value = str(relative_path)
|
|
|
|
|
else:
|
|
|
|
|
value = FILE_COLUMNS[coltype](path)
|
|
|
|
|
row[colname] = value
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise click.ClickException(
|
|
|
|
|
"'{}' is not a valid column definition - options are {}".format(
|
|
|
|
|
coltype, ", ".join(FILE_COLUMNS.keys())
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
yield row
|
|
|
|
|
|
|
|
|
|
db = sqlite_utils.Database(path)
|
|
|
|
|
with db.conn:
|
|
|
|
|
db[table].insert_all(
|
|
|
|
|
to_insert(), pk=pk, alter=alter, replace=replace, upsert=upsert
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
FILE_COLUMNS = {
|
|
|
|
|
"name": lambda p: p.name,
|
|
|
|
|
"path": lambda p: str(p),
|
|
|
|
|
"fullpath": lambda p: str(p.resolve()),
|
|
|
|
|
"sha256": lambda p: hashlib.sha256(p.resolve().read_bytes()).hexdigest(),
|
|
|
|
|
"md5": lambda p: hashlib.md5(p.resolve().read_bytes()).hexdigest(),
|
|
|
|
|
"mode": lambda p: p.stat().st_mode,
|
|
|
|
|
"content": lambda p: p.resolve().read_bytes(),
|
|
|
|
|
"mtime": lambda p: p.stat().st_mtime,
|
|
|
|
|
"ctime": lambda p: p.stat().st_ctime,
|
|
|
|
|
"mtime_int": lambda p: int(p.stat().st_mtime),
|
|
|
|
|
"ctime_int": lambda p: int(p.stat().st_ctime),
|
|
|
|
|
"mtime_iso": lambda p: datetime.utcfromtimestamp(p.stat().st_mtime).isoformat(),
|
|
|
|
|
"ctime_iso": lambda p: datetime.utcfromtimestamp(p.stat().st_ctime).isoformat(),
|
|
|
|
|
"size": lambda p: p.stat().st_size,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2019-05-24 17:56:44 -07:00
|
|
|
def output_rows(iterator, headers, nl, arrays, json_cols):
|
2019-01-25 18:06:29 -08:00
|
|
|
# We have to iterate two-at-a-time so we can know if we
|
|
|
|
|
# should output a trailing comma or if we have reached
|
|
|
|
|
# the last row.
|
2019-02-22 17:40:21 -08:00
|
|
|
current_iter, next_iter = itertools.tee(iterator, 2)
|
2019-01-26 10:58:45 -08:00
|
|
|
next(next_iter, None)
|
2019-01-25 18:06:29 -08:00
|
|
|
first = True
|
2019-01-26 10:58:45 -08:00
|
|
|
for row, next_row in itertools.zip_longest(current_iter, next_iter):
|
|
|
|
|
is_last = next_row is None
|
2019-01-25 18:06:29 -08:00
|
|
|
data = row
|
2019-05-24 17:56:44 -07:00
|
|
|
if json_cols:
|
|
|
|
|
# Any value that is a valid JSON string should be treated as JSON
|
|
|
|
|
data = [maybe_json(value) for value in data]
|
2019-01-25 18:06:29 -08:00
|
|
|
if not arrays:
|
2019-05-24 17:56:44 -07:00
|
|
|
data = dict(zip(headers, data))
|
2019-01-25 18:06:29 -08:00
|
|
|
line = "{firstchar}{serialized}{maybecomma}{lastchar}".format(
|
|
|
|
|
firstchar=("[" if first else " ") if not nl else "",
|
2020-07-26 17:48:36 -07:00
|
|
|
serialized=json.dumps(data, default=json_binary),
|
2019-01-25 18:06:29 -08:00
|
|
|
maybecomma="," if (not nl and not is_last) else "",
|
|
|
|
|
lastchar="]" if (is_last and not nl) else "",
|
|
|
|
|
)
|
2019-02-22 17:40:21 -08:00
|
|
|
yield line
|
2019-01-25 18:06:29 -08:00
|
|
|
first = False
|
2019-05-24 17:56:44 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def maybe_json(value):
|
|
|
|
|
if not isinstance(value, str):
|
|
|
|
|
return value
|
|
|
|
|
stripped = value.strip()
|
|
|
|
|
if not (stripped.startswith("{") or stripped.startswith("[")):
|
|
|
|
|
return value
|
|
|
|
|
try:
|
|
|
|
|
return json.loads(stripped)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return value
|
2020-07-26 17:48:36 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def json_binary(value):
|
|
|
|
|
if isinstance(value, bytes):
|
|
|
|
|
return {"$base64": True, "encoded": base64.b64encode(value).decode("latin-1")}
|
|
|
|
|
else:
|
|
|
|
|
raise TypeError
|