execsql2 2.6.0__py3-none-any.whl → 2.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- execsql/exporters/__init__.py +3 -3
- execsql/exporters/delimited.py +2 -2
- execsql/exporters/markdown.py +126 -0
- execsql/exporters/xlsx.py +317 -0
- execsql/exporters/yaml.py +87 -0
- execsql/metacommands/__init__.py +23 -2
- execsql/metacommands/dispatch.py +11 -0
- execsql/metacommands/io.py +2 -0
- execsql/metacommands/io_export.py +75 -0
- {execsql2-2.6.0.dist-info → execsql2-2.7.1.dist-info}/METADATA +5 -2
- {execsql2-2.6.0.dist-info → execsql2-2.7.1.dist-info}/RECORD +30 -27
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.6.0.data → execsql2-2.7.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.6.0.dist-info → execsql2-2.7.1.dist-info}/WHEEL +0 -0
- {execsql2-2.6.0.dist-info → execsql2-2.7.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.6.0.dist-info → execsql2-2.7.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.6.0.dist-info → execsql2-2.7.1.dist-info}/licenses/NOTICE +0 -0
execsql/exporters/__init__.py
CHANGED
|
@@ -9,9 +9,9 @@ handlers can access them via ``_state.write_query_to_csv`` etc. without
|
|
|
9
9
|
importing directly from here.
|
|
10
10
|
|
|
11
11
|
Sub-modules: ``base``, ``delimited``, ``json``, ``xml``, ``html``,
|
|
12
|
-
``latex``, ``ods``, ``xls``, ``zip``, ``raw``, ``pretty``,
|
|
13
|
-
``templates``, ``feather``, ``parquet``, ``duckdb``,
|
|
14
|
-
``protocol``.
|
|
12
|
+
``latex``, ``markdown``, ``ods``, ``xls``, ``zip``, ``raw``, ``pretty``,
|
|
13
|
+
``values``, ``templates``, ``feather``, ``parquet``, ``duckdb``,
|
|
14
|
+
``sqlite``, ``protocol``.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
from execsql.exporters.protocol import QueryExporter, RowsetExporter
|
execsql/exporters/delimited.py
CHANGED
|
@@ -27,7 +27,7 @@ from execsql.exceptions import ErrInfo
|
|
|
27
27
|
from execsql.models import DataTable
|
|
28
28
|
from execsql.utils.errors import exception_desc
|
|
29
29
|
from execsql.utils.fileio import filewriter_close
|
|
30
|
-
from execsql.utils.strings import clean_words, fold_words
|
|
30
|
+
from execsql.utils.strings import clean_words, dedup_words, fold_words
|
|
31
31
|
|
|
32
32
|
__all__ = ["LineDelimiter", "CsvFile", "CsvWriter", "DelimitedWriter", "write_delimited_file"]
|
|
33
33
|
|
|
@@ -677,7 +677,7 @@ class CsvFile(EncodedFile):
|
|
|
677
677
|
if conf.fold_col_hdrs != "no":
|
|
678
678
|
colnames = fold_words(colnames, conf.fold_col_hdrs)
|
|
679
679
|
if conf.dedup_col_hdrs:
|
|
680
|
-
colnames =
|
|
680
|
+
colnames = dedup_words(colnames)
|
|
681
681
|
return colnames
|
|
682
682
|
|
|
683
683
|
def column_headers(self) -> list[str]:
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
GitHub-Flavored Markdown (GFM) pipe table export for execsql.
|
|
5
|
+
|
|
6
|
+
Provides :func:`write_query_to_markdown`, which serializes a query result
|
|
7
|
+
set as a GFM pipe table suitable for inclusion in GitHub README files,
|
|
8
|
+
wikis, and any Markdown renderer that supports the pipe-table extension.
|
|
9
|
+
|
|
10
|
+
Example output::
|
|
11
|
+
|
|
12
|
+
| id | name | score |
|
|
13
|
+
|----|--------|-------|
|
|
14
|
+
| 1 | Alice | 95.2 |
|
|
15
|
+
| 2 | Bob | 87.0 |
|
|
16
|
+
|
|
17
|
+
No optional dependencies — pure Python.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import execsql.state as _state
|
|
23
|
+
from execsql.exceptions import ErrInfo
|
|
24
|
+
from execsql.exporters.zip import ZipWriter
|
|
25
|
+
from execsql.utils.errors import exception_desc
|
|
26
|
+
from execsql.utils.fileio import filewriter_close
|
|
27
|
+
|
|
28
|
+
__all__ = ["write_query_to_markdown"]
|
|
29
|
+
|
|
30
|
+
_PIPE_ESCAPE = str.maketrans({"|": r"\|", "\\": "\\\\"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _cell(value: Any) -> str:
|
|
34
|
+
"""Render a single cell value as a Markdown-safe string.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
value: The cell value from the result set. ``None`` is rendered as
|
|
38
|
+
an empty string. Pipe characters are escaped so they do not
|
|
39
|
+
break the table structure.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A string safe to embed between pipe characters in a GFM table row.
|
|
43
|
+
"""
|
|
44
|
+
if value is None:
|
|
45
|
+
return ""
|
|
46
|
+
return str(value).translate(_PIPE_ESCAPE)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def write_query_to_markdown(
|
|
50
|
+
select_stmt: str,
|
|
51
|
+
db: Any,
|
|
52
|
+
outfile: str,
|
|
53
|
+
append: bool = False,
|
|
54
|
+
desc: str | None = None,
|
|
55
|
+
zipfile: str | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Execute *select_stmt* and write the result set as a GFM pipe table.
|
|
58
|
+
|
|
59
|
+
Writes a GitHub-Flavored Markdown pipe table to *outfile* (or into
|
|
60
|
+
*zipfile* when provided). Column widths are derived from the widest
|
|
61
|
+
value in each column (including the header), so the table renders
|
|
62
|
+
legibly in plain-text editors as well as in Markdown renderers.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
select_stmt: SQL SELECT statement to execute.
|
|
66
|
+
db: Database connection object exposing ``select_rowsource()``.
|
|
67
|
+
outfile: Destination file path, or ``"stdout"`` for console output.
|
|
68
|
+
append: When ``True`` open the file in append mode. A blank line
|
|
69
|
+
is written before the table so consecutive appended tables are
|
|
70
|
+
visually separated.
|
|
71
|
+
desc: Optional human-readable description. When provided it is
|
|
72
|
+
written as an HTML comment (``<!-- desc -->``), which is valid
|
|
73
|
+
Markdown and invisible in rendered output.
|
|
74
|
+
zipfile: When set, write into this ZIP archive instead of a plain
|
|
75
|
+
file. *outfile* becomes the entry name inside the archive.
|
|
76
|
+
"""
|
|
77
|
+
conf = _state.conf
|
|
78
|
+
try:
|
|
79
|
+
hdrs, rows = db.select_rowsource(select_stmt)
|
|
80
|
+
except ErrInfo:
|
|
81
|
+
raise
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
84
|
+
|
|
85
|
+
# Materialise the full result set so we can compute column widths in one
|
|
86
|
+
# pass before writing. GFM tables require consistent column widths for
|
|
87
|
+
# readability; width computation requires seeing all rows first.
|
|
88
|
+
str_hdrs: list[str] = [_cell(h) for h in hdrs]
|
|
89
|
+
str_rows: list[list[str]] = [[_cell(v) for v in row] for row in rows]
|
|
90
|
+
|
|
91
|
+
# Minimum separator width is 3 dashes (GFM spec minimum for alignment row).
|
|
92
|
+
col_widths: list[int] = [max(3, len(h)) for h in str_hdrs]
|
|
93
|
+
for row in str_rows:
|
|
94
|
+
for i, cell in enumerate(row):
|
|
95
|
+
if len(cell) > col_widths[i]:
|
|
96
|
+
col_widths[i] = len(cell)
|
|
97
|
+
|
|
98
|
+
def _format_row(cells: list[str]) -> str:
|
|
99
|
+
padded = (f" {c:<{col_widths[i]}} " for i, c in enumerate(cells))
|
|
100
|
+
return "|" + "|".join(padded) + "|\n"
|
|
101
|
+
|
|
102
|
+
def _format_separator() -> str:
|
|
103
|
+
dashes = (f" {'-' * col_widths[i]} " for i in range(len(str_hdrs)))
|
|
104
|
+
return "|" + "|".join(dashes) + "|\n"
|
|
105
|
+
|
|
106
|
+
if zipfile is None:
|
|
107
|
+
filewriter_close(outfile)
|
|
108
|
+
from execsql.utils.fileio import EncodedFile
|
|
109
|
+
|
|
110
|
+
ef = EncodedFile(outfile, conf.output_encoding)
|
|
111
|
+
f = ef.open("at" if append else "wt")
|
|
112
|
+
else:
|
|
113
|
+
f = ZipWriter(zipfile, outfile, append)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
if append:
|
|
117
|
+
# Blank line separates consecutive tables when appending.
|
|
118
|
+
f.write("\n")
|
|
119
|
+
if desc is not None:
|
|
120
|
+
f.write(f"<!-- {desc} -->\n\n")
|
|
121
|
+
f.write(_format_row(str_hdrs))
|
|
122
|
+
f.write(_format_separator())
|
|
123
|
+
for row in str_rows:
|
|
124
|
+
f.write(_format_row(row))
|
|
125
|
+
finally:
|
|
126
|
+
f.close()
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
XLSX (Excel Open XML) export for execsql.
|
|
5
|
+
|
|
6
|
+
Provides :func:`write_query_to_xlsx` (single-sheet export) and
|
|
7
|
+
:func:`write_queries_to_xlsx` (multi-sheet export). Requires the
|
|
8
|
+
``openpyxl`` package (``execsql2[excel]``).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
import getpass
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import execsql.state as _state
|
|
18
|
+
from execsql.exceptions import ErrInfo
|
|
19
|
+
from execsql.exporters.base import ExportRecord
|
|
20
|
+
from execsql.exporters.pretty import prettyprint_query
|
|
21
|
+
from execsql.script import current_script_line
|
|
22
|
+
from execsql.utils.errors import exception_desc, fatal_error
|
|
23
|
+
from execsql.utils.fileio import filewriter_close
|
|
24
|
+
from execsql.utils.strings import unquoted
|
|
25
|
+
|
|
26
|
+
__all__ = ["write_query_to_xlsx", "write_queries_to_xlsx"]
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Internal helpers
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _require_openpyxl() -> Any:
|
|
34
|
+
"""Import and return the openpyxl module, raising a fatal error if absent."""
|
|
35
|
+
try:
|
|
36
|
+
import openpyxl
|
|
37
|
+
|
|
38
|
+
return openpyxl
|
|
39
|
+
except ImportError:
|
|
40
|
+
fatal_error("The openpyxl library is needed to write Excel (.xlsx) spreadsheets (install execsql2[excel]).")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _cell_value(item: Any) -> Any:
|
|
44
|
+
"""Return a value suitable for writing directly to an openpyxl cell.
|
|
45
|
+
|
|
46
|
+
openpyxl natively handles int, float, bool, str, datetime.datetime,
|
|
47
|
+
datetime.date, and None. datetime.time is converted to a string because
|
|
48
|
+
openpyxl does not have a native time-only cell type.
|
|
49
|
+
"""
|
|
50
|
+
if item is None:
|
|
51
|
+
return None
|
|
52
|
+
if isinstance(item, bool):
|
|
53
|
+
# bool must be checked before int — bool is a subclass of int.
|
|
54
|
+
return item
|
|
55
|
+
if isinstance(item, int | float):
|
|
56
|
+
return item
|
|
57
|
+
if isinstance(item, datetime.datetime):
|
|
58
|
+
return item
|
|
59
|
+
if isinstance(item, datetime.date):
|
|
60
|
+
return item
|
|
61
|
+
if isinstance(item, datetime.time):
|
|
62
|
+
# openpyxl has no native time-only type; store as HH:MM:SS string.
|
|
63
|
+
return item.strftime("%H:%M:%S")
|
|
64
|
+
return str(item)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Public API
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def write_query_to_xlsx(
|
|
73
|
+
select_stmt: str,
|
|
74
|
+
db: Any,
|
|
75
|
+
outfile: str,
|
|
76
|
+
append: bool = False,
|
|
77
|
+
desc: str | None = None,
|
|
78
|
+
sheetname: str | None = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Execute *select_stmt* and write the result to a single worksheet in an XLSX file.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
select_stmt: SQL SELECT statement to execute.
|
|
84
|
+
db: An execsql database adapter with a ``select_rowsource()`` method.
|
|
85
|
+
outfile: Destination ``.xlsx`` file path.
|
|
86
|
+
append: If ``True`` and *outfile* exists, add a new sheet to the
|
|
87
|
+
existing workbook. If ``False``, overwrite any existing file.
|
|
88
|
+
desc: Optional human-readable description stored in the inventory sheet.
|
|
89
|
+
sheetname: Name for the new worksheet. Defaults to ``"Sheet1"``
|
|
90
|
+
(or ``"Sheet2"``, ``"Sheet3"``, etc. when appending to avoid
|
|
91
|
+
name collisions).
|
|
92
|
+
"""
|
|
93
|
+
openpyxl = _require_openpyxl()
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
hdrs, rows = db.select_rowsource(select_stmt)
|
|
97
|
+
except ErrInfo:
|
|
98
|
+
raise
|
|
99
|
+
except Exception as e:
|
|
100
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
# Determine sheet name and open/create workbook
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
if append and Path(outfile).is_file():
|
|
106
|
+
wb = openpyxl.load_workbook(outfile)
|
|
107
|
+
existing_names = wb.sheetnames
|
|
108
|
+
base = sheetname or "Sheet"
|
|
109
|
+
sheet_name = base
|
|
110
|
+
sheet_no = 1
|
|
111
|
+
while sheet_name in existing_names:
|
|
112
|
+
sheet_no += 1
|
|
113
|
+
sheet_name = f"{base}{sheet_no}"
|
|
114
|
+
else:
|
|
115
|
+
sheet_name = sheetname or "Sheet1"
|
|
116
|
+
if Path(outfile).is_file():
|
|
117
|
+
filewriter_close(outfile)
|
|
118
|
+
os.unlink(outfile)
|
|
119
|
+
wb = openpyxl.Workbook()
|
|
120
|
+
# openpyxl creates a default sheet named "Sheet"; remove it so we
|
|
121
|
+
# start with a clean workbook.
|
|
122
|
+
if wb.sheetnames:
|
|
123
|
+
del wb[wb.sheetnames[0]]
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
# Ensure the inventory sheet exists
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
inventory_name = "Datasheets"
|
|
129
|
+
if inventory_name not in wb.sheetnames:
|
|
130
|
+
inv_ws = wb.create_sheet(inventory_name)
|
|
131
|
+
bold_font = openpyxl.styles.Font(bold=True)
|
|
132
|
+
for col_idx, hdr in enumerate(
|
|
133
|
+
("datasheet_name", "created_on", "created_by", "description", "source"),
|
|
134
|
+
start=1,
|
|
135
|
+
):
|
|
136
|
+
cell = inv_ws.cell(row=1, column=col_idx, value=hdr)
|
|
137
|
+
cell.font = bold_font
|
|
138
|
+
else:
|
|
139
|
+
inv_ws = wb[inventory_name]
|
|
140
|
+
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
# Write data to a new sheet
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
ws = wb.create_sheet(sheet_name)
|
|
145
|
+
bold_font = openpyxl.styles.Font(bold=True)
|
|
146
|
+
|
|
147
|
+
# Header row
|
|
148
|
+
for col_idx, hdr in enumerate(hdrs, start=1):
|
|
149
|
+
cell = ws.cell(row=1, column=col_idx, value=str(hdr))
|
|
150
|
+
cell.font = bold_font
|
|
151
|
+
|
|
152
|
+
# Data rows
|
|
153
|
+
for row_idx, row in enumerate(rows, start=2):
|
|
154
|
+
for col_idx, item in enumerate(row, start=1):
|
|
155
|
+
ws.cell(row=row_idx, column=col_idx, value=_cell_value(item))
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
# Update inventory sheet
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
script, lno = current_script_line()
|
|
161
|
+
src = (
|
|
162
|
+
f"{select_stmt} with database {_state.dbs.current().name()}, "
|
|
163
|
+
f"with script {str(Path(script).resolve())}, line {lno}"
|
|
164
|
+
)
|
|
165
|
+
next_row = inv_ws.max_row + 1
|
|
166
|
+
for col_idx, value in enumerate(
|
|
167
|
+
(
|
|
168
|
+
sheet_name,
|
|
169
|
+
datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
|
|
170
|
+
getpass.getuser(),
|
|
171
|
+
desc,
|
|
172
|
+
src,
|
|
173
|
+
),
|
|
174
|
+
start=1,
|
|
175
|
+
):
|
|
176
|
+
inv_ws.cell(row=next_row, column=col_idx, value=value)
|
|
177
|
+
|
|
178
|
+
wb.save(outfile)
|
|
179
|
+
wb.close()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def write_queries_to_xlsx(
|
|
183
|
+
table_list: str,
|
|
184
|
+
db: Any,
|
|
185
|
+
outfile: str,
|
|
186
|
+
append: bool = False,
|
|
187
|
+
tee: bool = False,
|
|
188
|
+
desc: str | None = None,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Write multiple tables/queries to separate worksheets in a single XLSX workbook.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
table_list: Comma-separated list of table names (optionally schema-qualified).
|
|
194
|
+
db: An execsql database adapter with a ``select_rowsource()`` method.
|
|
195
|
+
outfile: Destination ``.xlsx`` file path.
|
|
196
|
+
append: If ``True`` and *outfile* exists, add new sheets to the existing
|
|
197
|
+
workbook rather than replacing it.
|
|
198
|
+
tee: If ``True``, also pretty-print each query result to stdout.
|
|
199
|
+
desc: Optional description(s). A single string is applied to every
|
|
200
|
+
sheet; a comma-separated string with the same count as *table_list*
|
|
201
|
+
assigns individual descriptions per sheet.
|
|
202
|
+
"""
|
|
203
|
+
openpyxl = _require_openpyxl()
|
|
204
|
+
|
|
205
|
+
tables = [t.strip() for t in table_list.split(",")]
|
|
206
|
+
if desc is not None:
|
|
207
|
+
descriptions = [d.strip() for d in desc.split(",")]
|
|
208
|
+
one_desc = len(descriptions) != len(tables)
|
|
209
|
+
else:
|
|
210
|
+
descriptions = []
|
|
211
|
+
one_desc = False
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
# Open or create workbook
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
if Path(outfile).is_file() and not append:
|
|
217
|
+
filewriter_close(outfile)
|
|
218
|
+
os.unlink(outfile)
|
|
219
|
+
|
|
220
|
+
if Path(outfile).is_file():
|
|
221
|
+
wb = openpyxl.load_workbook(outfile)
|
|
222
|
+
else:
|
|
223
|
+
wb = openpyxl.Workbook()
|
|
224
|
+
# Remove the default empty sheet created by openpyxl.
|
|
225
|
+
if wb.sheetnames:
|
|
226
|
+
del wb[wb.sheetnames[0]]
|
|
227
|
+
|
|
228
|
+
# ------------------------------------------------------------------
|
|
229
|
+
# Ensure the inventory sheet exists
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
inventory_name = "Datasheets"
|
|
232
|
+
if inventory_name not in wb.sheetnames:
|
|
233
|
+
inv_ws = wb.create_sheet(inventory_name)
|
|
234
|
+
bold_font = openpyxl.styles.Font(bold=True)
|
|
235
|
+
for col_idx, hdr in enumerate(
|
|
236
|
+
("datasheet_name", "created_on", "created_by", "description", "source"),
|
|
237
|
+
start=1,
|
|
238
|
+
):
|
|
239
|
+
cell = inv_ws.cell(row=1, column=col_idx, value=hdr)
|
|
240
|
+
cell.font = bold_font
|
|
241
|
+
else:
|
|
242
|
+
inv_ws = wb[inventory_name]
|
|
243
|
+
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
# Write each table to its own sheet
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
bold_font = openpyxl.styles.Font(bold=True)
|
|
248
|
+
|
|
249
|
+
for i, t in enumerate(tables):
|
|
250
|
+
# Determine the table name used for the sheet label.
|
|
251
|
+
if "." in t:
|
|
252
|
+
st = t.split(".")
|
|
253
|
+
if len(st) != 2:
|
|
254
|
+
raise ErrInfo("cmd", other_msg=f"Unrecognized table specification in <{t}>")
|
|
255
|
+
tblname = unquoted(st[1])
|
|
256
|
+
else:
|
|
257
|
+
tblname = unquoted(t)
|
|
258
|
+
|
|
259
|
+
# Avoid duplicate sheet names.
|
|
260
|
+
existing_names = wb.sheetnames
|
|
261
|
+
sheet_name = tblname
|
|
262
|
+
sheet_no = 1
|
|
263
|
+
while sheet_name in existing_names:
|
|
264
|
+
sheet_name = f"{tblname}_{sheet_no}"
|
|
265
|
+
sheet_no += 1
|
|
266
|
+
|
|
267
|
+
# Fetch data.
|
|
268
|
+
select_stmt = f"select * from {t};"
|
|
269
|
+
try:
|
|
270
|
+
hdrs, rows = db.select_rowsource(select_stmt)
|
|
271
|
+
except ErrInfo:
|
|
272
|
+
raise
|
|
273
|
+
except Exception as e:
|
|
274
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
275
|
+
|
|
276
|
+
# Write data sheet.
|
|
277
|
+
ws = wb.create_sheet(sheet_name)
|
|
278
|
+
|
|
279
|
+
for col_idx, hdr in enumerate(hdrs, start=1):
|
|
280
|
+
cell = ws.cell(row=1, column=col_idx, value=str(hdr))
|
|
281
|
+
cell.font = bold_font
|
|
282
|
+
|
|
283
|
+
for row_idx, row in enumerate(rows, start=2):
|
|
284
|
+
for col_idx, item in enumerate(row, start=1):
|
|
285
|
+
ws.cell(row=row_idx, column=col_idx, value=_cell_value(item))
|
|
286
|
+
|
|
287
|
+
# Determine per-sheet description.
|
|
288
|
+
if desc is None:
|
|
289
|
+
d = None
|
|
290
|
+
elif one_desc:
|
|
291
|
+
d = desc
|
|
292
|
+
else:
|
|
293
|
+
d = descriptions[i]
|
|
294
|
+
|
|
295
|
+
# Update inventory sheet.
|
|
296
|
+
script, lno = current_script_line()
|
|
297
|
+
src = f"From database {_state.dbs.current().name()}, with script {str(Path(script).resolve())}, line {lno}"
|
|
298
|
+
next_row = inv_ws.max_row + 1
|
|
299
|
+
for col_idx, value in enumerate(
|
|
300
|
+
(
|
|
301
|
+
sheet_name,
|
|
302
|
+
datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
|
|
303
|
+
getpass.getuser(),
|
|
304
|
+
d,
|
|
305
|
+
src,
|
|
306
|
+
),
|
|
307
|
+
start=1,
|
|
308
|
+
):
|
|
309
|
+
inv_ws.cell(row=next_row, column=col_idx, value=value)
|
|
310
|
+
|
|
311
|
+
if tee and outfile.lower() != "stdout":
|
|
312
|
+
prettyprint_query(select_stmt, db, "stdout", False, desc=d)
|
|
313
|
+
|
|
314
|
+
_state.export_metadata.add(ExportRecord(queryname=select_stmt, outfile=outfile, zipfile=None, description=d))
|
|
315
|
+
|
|
316
|
+
wb.save(outfile)
|
|
317
|
+
wb.close()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
YAML export for execsql.
|
|
5
|
+
|
|
6
|
+
Provides :func:`write_query_to_yaml` which serialises a query result set as a
|
|
7
|
+
YAML sequence of mappings (one mapping per row).
|
|
8
|
+
|
|
9
|
+
Requires the ``PyYAML`` package (``pip install PyYAML`` or
|
|
10
|
+
``pip install 'execsql2[formats]'``).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import execsql.state as _state
|
|
16
|
+
from execsql.exceptions import ErrInfo
|
|
17
|
+
from execsql.exporters.zip import ZipWriter
|
|
18
|
+
from execsql.utils.errors import exception_desc
|
|
19
|
+
from execsql.utils.fileio import filewriter_close
|
|
20
|
+
|
|
21
|
+
__all__ = ["write_query_to_yaml"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def write_query_to_yaml(
|
|
25
|
+
select_stmt: str,
|
|
26
|
+
db: Any,
|
|
27
|
+
outfile: str,
|
|
28
|
+
append: bool = False,
|
|
29
|
+
desc: str | None = None,
|
|
30
|
+
zipfile: str | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Execute *select_stmt* and write the result set to *outfile* as YAML.
|
|
33
|
+
|
|
34
|
+
The output is a YAML sequence of mappings — one mapping per row with
|
|
35
|
+
column headers as keys. Python types are preserved: integers stay
|
|
36
|
+
integers, floats stay floats, ``None`` becomes ``null``.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
select_stmt: SQL SELECT statement to execute.
|
|
40
|
+
db: Database connection object exposing ``select_rowsource()``.
|
|
41
|
+
outfile: Destination file path, or ``"stdout"``.
|
|
42
|
+
append: When ``True`` the YAML sequence is appended to an existing
|
|
43
|
+
file. Note that concatenating two bare YAML sequences in one
|
|
44
|
+
file produces a multi-document stream; callers are responsible
|
|
45
|
+
for ensuring the resulting file is valid for their use-case.
|
|
46
|
+
desc: Optional description string. Ignored in plain YAML output
|
|
47
|
+
(YAML does not have a standard metadata header), but accepted
|
|
48
|
+
for API consistency with other exporters.
|
|
49
|
+
zipfile: When provided, write *outfile* as a member of this zip
|
|
50
|
+
archive instead of writing to the filesystem directly.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
import yaml # type: ignore[import-untyped]
|
|
54
|
+
except ImportError as exc:
|
|
55
|
+
raise ErrInfo(
|
|
56
|
+
"error",
|
|
57
|
+
other_msg=("PyYAML is required for FORMAT YAML export. Install it with: pip install PyYAML"),
|
|
58
|
+
) from exc
|
|
59
|
+
|
|
60
|
+
conf = _state.conf
|
|
61
|
+
try:
|
|
62
|
+
hdrs, rows = db.select_rowsource(select_stmt)
|
|
63
|
+
except ErrInfo:
|
|
64
|
+
raise
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
67
|
+
|
|
68
|
+
# Build the list of dicts in memory. YAML output is human-readable and
|
|
69
|
+
# typically used for small-to-medium result sets; loading into memory is
|
|
70
|
+
# acceptable and required by yaml.dump().
|
|
71
|
+
uhdrs = [str(h) for h in hdrs]
|
|
72
|
+
data = [dict(zip(uhdrs, row)) for row in rows]
|
|
73
|
+
yaml_text = yaml.dump(data, default_flow_style=False, allow_unicode=True)
|
|
74
|
+
|
|
75
|
+
if zipfile is None:
|
|
76
|
+
filewriter_close(outfile)
|
|
77
|
+
from execsql.utils.fileio import EncodedFile
|
|
78
|
+
|
|
79
|
+
ef = EncodedFile(outfile, conf.output_encoding)
|
|
80
|
+
f = ef.open("at" if append else "wt")
|
|
81
|
+
else:
|
|
82
|
+
f = ZipWriter(zipfile, outfile, append)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
f.write(yaml_text)
|
|
86
|
+
finally:
|
|
87
|
+
f.close()
|
execsql/metacommands/__init__.py
CHANGED
|
@@ -105,6 +105,7 @@ from execsql.metacommands.io import (
|
|
|
105
105
|
x_export_query_with_template,
|
|
106
106
|
x_export_with_template,
|
|
107
107
|
x_export_ods_multiple,
|
|
108
|
+
x_export_xlsx_multiple,
|
|
108
109
|
x_export_metadata,
|
|
109
110
|
x_export_metadata_table,
|
|
110
111
|
x_import,
|
|
@@ -303,6 +304,7 @@ __all__ = [
|
|
|
303
304
|
"x_export_query_with_template",
|
|
304
305
|
"x_export_with_template",
|
|
305
306
|
"x_export_ods_multiple",
|
|
307
|
+
"x_export_xlsx_multiple",
|
|
306
308
|
"x_export_metadata",
|
|
307
309
|
"x_export_metadata_table",
|
|
308
310
|
"x_import",
|
|
@@ -427,12 +429,31 @@ TEXT_FORMATS = ["TXT", "TXT-AND", "PLAIN"]
|
|
|
427
429
|
JSON_VARIANT_FORMATS = ["JSON_TS", "JSON_TABLESCHEMA"]
|
|
428
430
|
|
|
429
431
|
QUERY_EXPORT_FORMATS = (
|
|
430
|
-
DELIMITED_FORMATS
|
|
432
|
+
DELIMITED_FORMATS
|
|
433
|
+
+ TEXT_FORMATS
|
|
434
|
+
+ ["ODS", "XLSX", "JSON", "HTML", "CGI-HTML", "VALUES", "LATEX", "RAW", "B64", "FEATHER", "YAML", "MARKDOWN", "MD"]
|
|
431
435
|
)
|
|
432
436
|
TABLE_EXPORT_FORMATS = (
|
|
433
437
|
DELIMITED_FORMATS
|
|
434
438
|
+ TEXT_FORMATS
|
|
435
|
-
+ [
|
|
439
|
+
+ [
|
|
440
|
+
"JSON",
|
|
441
|
+
"XML",
|
|
442
|
+
"VALUES",
|
|
443
|
+
"HTML",
|
|
444
|
+
"CGI-HTML",
|
|
445
|
+
"SQLITE",
|
|
446
|
+
"DUCKDB",
|
|
447
|
+
"LATEX",
|
|
448
|
+
"RAW",
|
|
449
|
+
"B64",
|
|
450
|
+
"FEATHER",
|
|
451
|
+
"HDF5",
|
|
452
|
+
"XLSX",
|
|
453
|
+
"YAML",
|
|
454
|
+
"MARKDOWN",
|
|
455
|
+
"MD",
|
|
456
|
+
]
|
|
436
457
|
)
|
|
437
458
|
SERVE_FORMATS = ["BINARY", "CSV", "TXT", "TEXT", "ODS", "JSON", "HTML", "PDF", "ZIP"]
|
|
438
459
|
METADATA_FORMATS = ["CSV", "TAB", "TSV", "TABQ", "TSVQ", "TXT", "TEXT"]
|
execsql/metacommands/dispatch.py
CHANGED
|
@@ -107,6 +107,7 @@ from execsql.metacommands.io import (
|
|
|
107
107
|
x_export_metadata,
|
|
108
108
|
x_export_metadata_table,
|
|
109
109
|
x_export_ods_multiple,
|
|
110
|
+
x_export_xlsx_multiple,
|
|
110
111
|
x_export_query,
|
|
111
112
|
x_export_query_with_template,
|
|
112
113
|
x_export_row_buffer,
|
|
@@ -395,6 +396,16 @@ def build_dispatch_table() -> MetaCommandList:
|
|
|
395
396
|
),
|
|
396
397
|
x_export_ods_multiple,
|
|
397
398
|
)
|
|
399
|
+
mcl.add(
|
|
400
|
+
ins_table_list_rxs(
|
|
401
|
+
r"^\s*EXPORT\s+",
|
|
402
|
+
ins_fn_rxs(
|
|
403
|
+
r"\s+(?P<tee>TEE\s+)?(?P<append>APPEND\s+)?TO\s+",
|
|
404
|
+
r'\s+AS\s+XLSX(?:\s+DESCRIP(?:TION)?\s+"(?P<description>[^"]*)")?\s*$',
|
|
405
|
+
),
|
|
406
|
+
),
|
|
407
|
+
x_export_xlsx_multiple,
|
|
408
|
+
)
|
|
398
409
|
|
|
399
410
|
# ------------------------------------------------------------------
|
|
400
411
|
# IMPORT_FILE
|
execsql/metacommands/io.py
CHANGED
|
@@ -22,6 +22,7 @@ from execsql.metacommands.io_export import ( # noqa: F401
|
|
|
22
22
|
x_export_metadata,
|
|
23
23
|
x_export_metadata_table,
|
|
24
24
|
x_export_ods_multiple,
|
|
25
|
+
x_export_xlsx_multiple,
|
|
25
26
|
x_export_query,
|
|
26
27
|
x_export_query_with_template,
|
|
27
28
|
x_export_row_buffer,
|
|
@@ -78,6 +79,7 @@ __all__ = [
|
|
|
78
79
|
"x_export_metadata",
|
|
79
80
|
"x_export_metadata_table",
|
|
80
81
|
"x_export_ods_multiple",
|
|
82
|
+
"x_export_xlsx_multiple",
|
|
81
83
|
"x_export_query",
|
|
82
84
|
"x_export_query_with_template",
|
|
83
85
|
"x_export_row_buffer",
|
|
@@ -19,6 +19,7 @@ from execsql.exporters.html import write_query_to_cgi_html, write_query_to_html
|
|
|
19
19
|
from execsql.exporters.json import write_query_to_json, write_query_to_json_ts
|
|
20
20
|
from execsql.exporters.latex import write_query_to_latex
|
|
21
21
|
from execsql.exporters.ods import write_queries_to_ods, write_query_to_ods
|
|
22
|
+
from execsql.exporters.xlsx import write_queries_to_xlsx, write_query_to_xlsx
|
|
22
23
|
from execsql.exporters.parquet import write_query_to_parquet
|
|
23
24
|
from execsql.exporters.pretty import prettyprint_query, prettyprint_rowset
|
|
24
25
|
from execsql.exporters.raw import write_query_b64, write_query_raw
|
|
@@ -26,6 +27,8 @@ from execsql.exporters.sqlite import write_query_to_sqlite
|
|
|
26
27
|
from execsql.exporters.templates import report_query
|
|
27
28
|
from execsql.exporters.values import write_query_to_values
|
|
28
29
|
from execsql.exporters.xml import write_query_to_xml
|
|
30
|
+
from execsql.exporters.markdown import write_query_to_markdown
|
|
31
|
+
from execsql.exporters.yaml import write_query_to_yaml
|
|
29
32
|
from execsql.importers.base import import_data_table
|
|
30
33
|
from execsql.script import current_script_line
|
|
31
34
|
from execsql.utils.errors import exception_desc
|
|
@@ -85,6 +88,8 @@ def x_export(**kwargs: Any) -> None:
|
|
|
85
88
|
raise ErrInfo("error", other_msg="Cannot export to the HDF5 format within a zipfile.")
|
|
86
89
|
if filefmt == "ods":
|
|
87
90
|
raise ErrInfo("error", other_msg="Cannot export to an ODS workbook within a zipfile.")
|
|
91
|
+
if filefmt == "xlsx":
|
|
92
|
+
raise ErrInfo("error", other_msg="Cannot export to an XLSX workbook within a zipfile.")
|
|
88
93
|
notype = bool(kwargs.get("notype"))
|
|
89
94
|
if zipfilename is not None:
|
|
90
95
|
check_dir(zipfilename)
|
|
@@ -120,6 +125,15 @@ def x_export(**kwargs: Any) -> None:
|
|
|
120
125
|
sheetname=queryname,
|
|
121
126
|
desc=description,
|
|
122
127
|
)
|
|
128
|
+
elif filefmt == "xlsx":
|
|
129
|
+
write_query_to_xlsx(
|
|
130
|
+
select_stmt,
|
|
131
|
+
_state.dbs.current(),
|
|
132
|
+
outfile,
|
|
133
|
+
append,
|
|
134
|
+
sheetname=queryname,
|
|
135
|
+
desc=description,
|
|
136
|
+
)
|
|
123
137
|
elif filefmt == "duckdb":
|
|
124
138
|
write_query_to_duckdb(select_stmt, _state.dbs.current(), outfile, append, tablename=queryname)
|
|
125
139
|
elif filefmt == "sqlite":
|
|
@@ -191,6 +205,24 @@ def x_export(**kwargs: Any) -> None:
|
|
|
191
205
|
)
|
|
192
206
|
elif filefmt == "hdf5":
|
|
193
207
|
write_query_to_hdf5(table, select_stmt, _state.dbs.current(), outfile, append, desc=description)
|
|
208
|
+
elif filefmt == "yaml":
|
|
209
|
+
write_query_to_yaml(
|
|
210
|
+
select_stmt,
|
|
211
|
+
_state.dbs.current(),
|
|
212
|
+
outfile,
|
|
213
|
+
append,
|
|
214
|
+
desc=description,
|
|
215
|
+
zipfile=zipfilename,
|
|
216
|
+
)
|
|
217
|
+
elif filefmt in ("markdown", "md"):
|
|
218
|
+
write_query_to_markdown(
|
|
219
|
+
select_stmt,
|
|
220
|
+
_state.dbs.current(),
|
|
221
|
+
outfile,
|
|
222
|
+
append,
|
|
223
|
+
desc=description,
|
|
224
|
+
zipfile=zipfilename,
|
|
225
|
+
)
|
|
194
226
|
else:
|
|
195
227
|
try:
|
|
196
228
|
hdrs, rows = _state.dbs.current().select_rowsource(select_stmt)
|
|
@@ -237,6 +269,8 @@ def x_export_query(**kwargs: Any) -> None:
|
|
|
237
269
|
raise ErrInfo("error", other_msg="Cannot export to the HDF5 format within a zipfile.")
|
|
238
270
|
if filefmt == "ods":
|
|
239
271
|
raise ErrInfo("error", other_msg="Cannot export to an ODS workbook within a zipfile.")
|
|
272
|
+
if filefmt == "xlsx":
|
|
273
|
+
raise ErrInfo("error", other_msg="Cannot export to an XLSX workbook within a zipfile.")
|
|
240
274
|
notype = bool(kwargs.get("notype"))
|
|
241
275
|
check_dir(outfile)
|
|
242
276
|
if tee and outfile.lower() != "stdout":
|
|
@@ -270,6 +304,16 @@ def x_export_query(**kwargs: Any) -> None:
|
|
|
270
304
|
sheetname=f"Query_{lno}",
|
|
271
305
|
desc=description,
|
|
272
306
|
)
|
|
307
|
+
elif filefmt == "xlsx":
|
|
308
|
+
script_name, lno = current_script_line()
|
|
309
|
+
write_query_to_xlsx(
|
|
310
|
+
select_stmt,
|
|
311
|
+
_state.dbs.current(),
|
|
312
|
+
outfile,
|
|
313
|
+
append,
|
|
314
|
+
sheetname=f"Query_{lno}",
|
|
315
|
+
desc=description,
|
|
316
|
+
)
|
|
273
317
|
elif filefmt == "json":
|
|
274
318
|
write_query_to_json(
|
|
275
319
|
select_stmt,
|
|
@@ -325,6 +369,24 @@ def x_export_query(**kwargs: Any) -> None:
|
|
|
325
369
|
desc=description,
|
|
326
370
|
zipfile=zipfilename,
|
|
327
371
|
)
|
|
372
|
+
elif filefmt == "yaml":
|
|
373
|
+
write_query_to_yaml(
|
|
374
|
+
select_stmt,
|
|
375
|
+
_state.dbs.current(),
|
|
376
|
+
outfile,
|
|
377
|
+
append,
|
|
378
|
+
desc=description,
|
|
379
|
+
zipfile=zipfilename,
|
|
380
|
+
)
|
|
381
|
+
elif filefmt in ("markdown", "md"):
|
|
382
|
+
write_query_to_markdown(
|
|
383
|
+
select_stmt,
|
|
384
|
+
_state.dbs.current(),
|
|
385
|
+
outfile,
|
|
386
|
+
append,
|
|
387
|
+
desc=description,
|
|
388
|
+
zipfile=zipfilename,
|
|
389
|
+
)
|
|
328
390
|
else:
|
|
329
391
|
try:
|
|
330
392
|
hdrs, rows = _state.dbs.current().select_rowsource(select_stmt)
|
|
@@ -403,6 +465,19 @@ def x_export_ods_multiple(**kwargs: Any) -> None:
|
|
|
403
465
|
write_queries_to_ods(table_list, _state.dbs.current(), outfile, append, tee, desc=description)
|
|
404
466
|
|
|
405
467
|
|
|
468
|
+
def x_export_xlsx_multiple(**kwargs: Any) -> None:
|
|
469
|
+
"""Export multiple tables to separate worksheets in a single XLSX workbook."""
|
|
470
|
+
table_list = kwargs["tables"]
|
|
471
|
+
outfile = kwargs["filename"]
|
|
472
|
+
description = kwargs["description"]
|
|
473
|
+
tee = kwargs["tee"]
|
|
474
|
+
tee = bool(tee)
|
|
475
|
+
append = kwargs["append"]
|
|
476
|
+
append = append is not None
|
|
477
|
+
check_dir(outfile)
|
|
478
|
+
write_queries_to_xlsx(table_list, _state.dbs.current(), outfile, append, tee, desc=description)
|
|
479
|
+
|
|
480
|
+
|
|
406
481
|
def x_export_metadata(**kwargs: Any) -> None:
|
|
407
482
|
outfile = kwargs["filename"]
|
|
408
483
|
append = kwargs["append"] is not None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.7.1
|
|
4
4
|
Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
|
|
5
5
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
6
6
|
Project-URL: Issues, https://github.com/geocoug/execsql/issues
|
|
@@ -54,6 +54,7 @@ Requires-Dist: polars; extra == 'all'
|
|
|
54
54
|
Requires-Dist: psycopg2-binary; extra == 'all'
|
|
55
55
|
Requires-Dist: pymysql; extra == 'all'
|
|
56
56
|
Requires-Dist: pyodbc; extra == 'all'
|
|
57
|
+
Requires-Dist: pyyaml; extra == 'all'
|
|
57
58
|
Requires-Dist: tables; extra == 'all'
|
|
58
59
|
Requires-Dist: xlrd; extra == 'all'
|
|
59
60
|
Provides-Extra: all-db
|
|
@@ -76,6 +77,7 @@ Requires-Dist: openpyxl; extra == 'dev'
|
|
|
76
77
|
Requires-Dist: polars; extra == 'dev'
|
|
77
78
|
Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
|
|
78
79
|
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
80
|
+
Requires-Dist: pyyaml; extra == 'dev'
|
|
79
81
|
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
80
82
|
Requires-Dist: tables; extra == 'dev'
|
|
81
83
|
Requires-Dist: tox-uv>=1.13.1; extra == 'dev'
|
|
@@ -91,6 +93,7 @@ Requires-Dist: jinja2; extra == 'formats'
|
|
|
91
93
|
Requires-Dist: odfpy; extra == 'formats'
|
|
92
94
|
Requires-Dist: openpyxl; extra == 'formats'
|
|
93
95
|
Requires-Dist: polars; extra == 'formats'
|
|
96
|
+
Requires-Dist: pyyaml; extra == 'formats'
|
|
94
97
|
Requires-Dist: tables; extra == 'formats'
|
|
95
98
|
Requires-Dist: xlrd; extra == 'formats'
|
|
96
99
|
Provides-Extra: mssql
|
|
@@ -227,7 +230,7 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
|
|
|
227
230
|
# Features
|
|
228
231
|
|
|
229
232
|
- Import data from CSV, TSV, Excel, OpenDocument, Feather, or Parquet files into a database table.
|
|
230
|
-
- Export query results in
|
|
233
|
+
- Export query results in 20+ formats including CSV, TSV, JSON, YAML, XML, HTML, Markdown, LaTeX, XLSX, OpenDocument, Feather, Parquet, HDF5, DuckDB, SQLite, plain text, and Jinja2 templates.
|
|
231
234
|
- Copy data between databases, including across different DBMS types.
|
|
232
235
|
- Conditionally execute SQL and metacommands using `IF`/`ELSE`/`ENDIF` based on data values, DBMS type, or user input.
|
|
233
236
|
- Loop over blocks of SQL and metacommands using `LOOP`/`ENDLOOP`.
|
|
@@ -25,14 +25,15 @@ execsql/db/oracle.py,sha256=AFVHhGlCzBuU7JgrAqeUG6e8TUUkk1Y80XVJQnGOqLM,10547
|
|
|
25
25
|
execsql/db/postgres.py,sha256=oXR7ODzQhR3yO6q-aNa9_il_rO3SpOX9yYGsfIqHwLI,20139
|
|
26
26
|
execsql/db/sqlite.py,sha256=2fD3AfckIGWN1oHcOaqQlQnbig26top1IlW-ejPHttI,10204
|
|
27
27
|
execsql/db/sqlserver.py,sha256=mNwmIIxTzqXU-cOjpNpeFi568HjQHsAk8Xnn-tR6F_E,7563
|
|
28
|
-
execsql/exporters/__init__.py,sha256
|
|
28
|
+
execsql/exporters/__init__.py,sha256=-Cnji-OgodJV8ftcDcOyTof0kQMy9J5kKVC8GVFpc3o,670
|
|
29
29
|
execsql/exporters/base.py,sha256=W9USFyk_2eztjJ51X6CJh7-chE1i3cSx-STOtbHXCNI,6373
|
|
30
|
-
execsql/exporters/delimited.py,sha256=
|
|
30
|
+
execsql/exporters/delimited.py,sha256=zMvurTRVl5W-6N8DuYtn_xILUkYLMlfflwWMfvdeaF0,30304
|
|
31
31
|
execsql/exporters/duckdb.py,sha256=Wc9I5uiV4MzmVQzCX-vgVHQUL7U3ZWdOkFVFWBv5SXM,2911
|
|
32
32
|
execsql/exporters/feather.py,sha256=w2qZAnewzeiRMnmPXECvkgD-6KtyxaiQwjokRT7Awrc,4167
|
|
33
33
|
execsql/exporters/html.py,sha256=ISQBOr7AJ5koKlebXSvWqzEvl1nXriCRGeKmk-bzkrc,9335
|
|
34
34
|
execsql/exporters/json.py,sha256=yljlRBbmvDVSTQUe0EdfdqTTRpD5sHfn7-jQ457ydvc,4139
|
|
35
35
|
execsql/exporters/latex.py,sha256=w_B83_5vKPe8uYxCWGdqvxwJeq0mw5zzKYDiAb7dbN0,4503
|
|
36
|
+
execsql/exporters/markdown.py,sha256=_ZX3dikbtAb6qZxYeWxDZAPF0-cNKTPR7or5kTbD2ZU,4436
|
|
36
37
|
execsql/exporters/ods.py,sha256=jl2qVHUeCLLv8xrkZfG3jgXbaglQ3rggCHziv7tNQOI,18876
|
|
37
38
|
execsql/exporters/parquet.py,sha256=186vUTH1oVAQ0s_qayLzEQVsKKu3ijAkhYEI6tysXkg,1095
|
|
38
39
|
execsql/exporters/pretty.py,sha256=9isA8f6xUz-3-JhMJimibnvtybVrT1cnoAjGnzsPEGI,3423
|
|
@@ -42,7 +43,9 @@ execsql/exporters/sqlite.py,sha256=XA0ALLvy-r6Pz1lpOFkWWbvpSP9Hm1tHHiuo_BvPVDk,2
|
|
|
42
43
|
execsql/exporters/templates.py,sha256=T9nk7vJrlxiPGfOWGc79xqqDxK3TCYu0wXq48U02npw,5564
|
|
43
44
|
execsql/exporters/values.py,sha256=HIyud31aux_dbCphfKHEGeZB9fkIPE5PoGXQz817XIE,2520
|
|
44
45
|
execsql/exporters/xls.py,sha256=nPROgxL8XK2oiBVoqN2L-o0j_jynRIMokwB8NpvOBt0,10623
|
|
46
|
+
execsql/exporters/xlsx.py,sha256=xXTFIKkvJnNOsFdnhSYEkJa4ulTrtq9tIRk6SSchqA0,11299
|
|
45
47
|
execsql/exporters/xml.py,sha256=lqcOM8uKDoCayU42BPSLNH1_2DIHU5D3LtQItREU90c,2564
|
|
48
|
+
execsql/exporters/yaml.py,sha256=0bwLDU3Fy00yMryBOSBSptbjV8Re6Ks-b62DObFNP4o,3062
|
|
46
49
|
execsql/exporters/zip.py,sha256=9-hExltQorONNThiMfxPDYHqHsbTeq9zM9zmtG4oFb8,4410
|
|
47
50
|
execsql/gui/__init__.py,sha256=oCb-cyhLZzVpWJ4WU5HbqEDBrV-lm0ytEwxemrOZyqs,2048
|
|
48
51
|
execsql/gui/base.py,sha256=sfNRkDrf7FhIgMRUOdyZpRLS1Xk9RqNhrV0A1RP6PXU,6068
|
|
@@ -55,15 +58,15 @@ execsql/importers/csv.py,sha256=Mu848WNzuhVO1ade-WurPyxqGOuVNRO8UwRF3-bav_I,4845
|
|
|
55
58
|
execsql/importers/feather.py,sha256=g2B69d2uv9vmnXcmjFyTVsMP40LYEzFYkhk3gD26mGw,1900
|
|
56
59
|
execsql/importers/ods.py,sha256=MJsdsjropzCvxAA3DDZfAL_AnmZ4yij7DnrjGyDJqHQ,2843
|
|
57
60
|
execsql/importers/xls.py,sha256=e0Zfe47ZiCpA1Ae3XDJ1ko3sCiH3-8U6XLKi6NvD0jQ,3683
|
|
58
|
-
execsql/metacommands/__init__.py,sha256=
|
|
61
|
+
execsql/metacommands/__init__.py,sha256=zYRUuCnU-e-HbPdp_zCgQIriR_YgRqHkais45Eo3FNk,11082
|
|
59
62
|
execsql/metacommands/conditions.py,sha256=u-XdeIWj9QMht9hRGhvH0XlB9V09AliAPKDBHRXc02s,24540
|
|
60
63
|
execsql/metacommands/connect.py,sha256=Nsm0D91i3RX-R2rzQQ-Br-gULaI6Uvdn9fqb7DOAVfE,14804
|
|
61
64
|
execsql/metacommands/control.py,sha256=FCIWD-ZivHRZDqMS-2k37iR05HKHsv_7UPh5zJAg4I4,7693
|
|
62
65
|
execsql/metacommands/data.py,sha256=tRQBGTAuW-eJ2tBNWaoZI9OjTyNNyHJISo7gOdL-sm8,11370
|
|
63
66
|
execsql/metacommands/debug.py,sha256=nmfQ2ijUbTQO3drnyV9EzFueGSTfMl-CddP_NlQyI14,8178
|
|
64
|
-
execsql/metacommands/dispatch.py,sha256=
|
|
65
|
-
execsql/metacommands/io.py,sha256=
|
|
66
|
-
execsql/metacommands/io_export.py,sha256=
|
|
67
|
+
execsql/metacommands/dispatch.py,sha256=aZq4QFY3AjqbplCr2iSoD13ayvT34wWns63a15a_JRM,82407
|
|
68
|
+
execsql/metacommands/io.py,sha256=Duh60caM4go9JczbGYNMKKYpcMimwPzF6EQ_tshKxdE,2971
|
|
69
|
+
execsql/metacommands/io_export.py,sha256=7lkCSnPhXy9FVau9_hT1u68NOVdG2DsWmvUh9hM1QWI,18359
|
|
67
70
|
execsql/metacommands/io_fileops.py,sha256=RKqbWPTYiwiqCZYG-lpih0w1JVOY4RBFdWr3BJb_pnY,9669
|
|
68
71
|
execsql/metacommands/io_import.py,sha256=wyxJJdlW07P5ZIhweejhXyyGANAvEhY5uMjKZ200Jyc,12983
|
|
69
72
|
execsql/metacommands/io_write.py,sha256=NpL2aYGfBpbqmPpYsqniYltYfd_SCA1EQz3_4qSdNbo,8279
|
|
@@ -86,24 +89,24 @@ execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
|
|
|
86
89
|
execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
|
|
87
90
|
execsql/utils/strings.py,sha256=5Dvzrk-9SIw2lpxXZQkiJbNyo1sy7iXXAtSULlZ0KG8,8488
|
|
88
91
|
execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
|
|
89
|
-
execsql2-2.
|
|
90
|
-
execsql2-2.
|
|
91
|
-
execsql2-2.
|
|
92
|
-
execsql2-2.
|
|
93
|
-
execsql2-2.
|
|
94
|
-
execsql2-2.
|
|
95
|
-
execsql2-2.
|
|
96
|
-
execsql2-2.
|
|
97
|
-
execsql2-2.
|
|
98
|
-
execsql2-2.
|
|
99
|
-
execsql2-2.
|
|
100
|
-
execsql2-2.
|
|
101
|
-
execsql2-2.
|
|
102
|
-
execsql2-2.
|
|
103
|
-
execsql2-2.
|
|
104
|
-
execsql2-2.
|
|
105
|
-
execsql2-2.
|
|
106
|
-
execsql2-2.
|
|
107
|
-
execsql2-2.
|
|
108
|
-
execsql2-2.
|
|
109
|
-
execsql2-2.
|
|
92
|
+
execsql2-2.7.1.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
|
|
93
|
+
execsql2-2.7.1.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
94
|
+
execsql2-2.7.1.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
|
|
95
|
+
execsql2-2.7.1.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
|
|
96
|
+
execsql2-2.7.1.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
97
|
+
execsql2-2.7.1.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
|
|
98
|
+
execsql2-2.7.1.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
|
|
99
|
+
execsql2-2.7.1.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
|
|
100
|
+
execsql2-2.7.1.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
|
|
101
|
+
execsql2-2.7.1.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
|
|
102
|
+
execsql2-2.7.1.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
|
|
103
|
+
execsql2-2.7.1.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
|
|
104
|
+
execsql2-2.7.1.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
|
|
105
|
+
execsql2-2.7.1.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
|
|
106
|
+
execsql2-2.7.1.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
|
|
107
|
+
execsql2-2.7.1.dist-info/METADATA,sha256=UEW6Lpayq5sSHyOOrXXz0SvdAw0MzuoEbRQQ1Chr2VI,16722
|
|
108
|
+
execsql2-2.7.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
109
|
+
execsql2-2.7.1.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
110
|
+
execsql2-2.7.1.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
111
|
+
execsql2-2.7.1.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
|
|
112
|
+
execsql2-2.7.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|