Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions sqlite_utils/export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import csv
from collections.abc import Mapping
from typing import Any, Sequence, TextIO, Type, Union

CsvDialect = Union[str, csv.Dialect, Type[csv.Dialect]]


def rows_to_csv_file(
cursor: Any,
file: TextIO,
*,
header: bool = True,
dialect: CsvDialect = "excel",
**writer_kwargs: Any,
) -> None:
"""Write the rows from a DB-API cursor to a CSV-compatible text stream.

``cursor`` should be a DB-API cursor whose ``description`` attribute
contains the result column metadata. Pass ``dialect="excel-tab"`` to
produce TSV output, or provide any dialect accepted by ``csv.writer``.

Additional keyword arguments are forwarded to ``csv.writer``.
"""
description: Union[Sequence[Sequence[Any]], None] = cursor.description
if description is None:
raise ValueError("Cursor does not have result columns")

columns = [column[0] for column in description]
writer = csv.writer(file, dialect=dialect, **writer_kwargs)
if header:
writer.writerow(columns)

def rows():
for row in cursor:
if isinstance(row, Mapping):
yield [row[column] for column in columns]
else:
yield row

writer.writerows(rows())
85 changes: 85 additions & 0 deletions tests/test_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import csv
import io
import sqlite3

import pytest

from sqlite_utils.export import rows_to_csv_file


def _cursor():
db = sqlite3.connect(":memory:")
db.execute("create table creatures (id integer, name text)")
db.executemany(
"insert into creatures (id, name) values (?, ?)",
((1, "Cleo"), (2, "Cardi, Jr.")),
)
return db, db.execute("select id, name from creatures order by id")


def test_rows_to_csv_file():
db, cursor = _cursor()
output = io.StringIO(newline="")
rows_to_csv_file(cursor, output, lineterminator="\n")
assert output.getvalue() == 'id,name\n1,Cleo\n2,"Cardi, Jr."\n'
db.close()


def test_rows_to_csv_file_without_header():
db, cursor = _cursor()
output = io.StringIO(newline="")
rows_to_csv_file(cursor, output, header=False, lineterminator="\n")
assert output.getvalue() == '1,Cleo\n2,"Cardi, Jr."\n'
db.close()


def test_rows_to_csv_file_tsv():
db, cursor = _cursor()
output = io.StringIO(newline="")
rows_to_csv_file(cursor, output, dialect="excel-tab", lineterminator="\n")
assert output.getvalue() == "id\tname\n1\tCleo\n2\tCardi, Jr.\n"
db.close()


def test_rows_to_csv_file_accepts_dialect_class():
class PipeDialect(csv.excel):
delimiter = "|"

db, cursor = _cursor()
output = io.StringIO(newline="")
rows_to_csv_file(cursor, output, dialect=PipeDialect, lineterminator="\n")
assert output.getvalue() == "id|name\n1|Cleo\n2|Cardi, Jr.\n"
db.close()


def test_rows_to_csv_file_with_mapping_row_factory():
db = sqlite3.connect(":memory:")
db.execute("create table creatures (id integer, name text)")
db.executemany(
"insert into creatures (id, name) values (?, ?)",
((1, "Cleo"), (2, "Cardi, Jr.")),
)

def dict_factory(cursor, row):
return {
column[0]: value
for column, value in zip(cursor.description, row)
}

db.row_factory = dict_factory
cursor = db.execute("select id, name from creatures order by id")
output = io.StringIO(newline="")
rows_to_csv_file(cursor, output, lineterminator="\n")
assert output.getvalue() == 'id,name\n1,Cleo\n2,"Cardi, Jr."\n'
db.close()


@pytest.mark.parametrize("header", (True, False))
def test_rows_to_csv_file_requires_result_columns(header):
db = sqlite3.connect(":memory:")
cursor = db.execute("create table creatures (id integer)")
output = io.StringIO(newline="")
with pytest.raises(ValueError, match="Cursor does not have result columns"):
rows_to_csv_file(cursor, output, header=header)
assert output.getvalue() == ""
db.close()
Loading