execsql2 2.2.1__py3-none-any.whl → 2.4.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/cli/run.py +11 -5
- execsql/config.py +52 -0
- execsql/db/access.py +11 -3
- execsql/db/base.py +180 -135
- execsql/db/dsn.py +4 -0
- execsql/db/duckdb.py +4 -0
- execsql/db/factory.py +31 -5
- execsql/db/firebird.py +4 -0
- execsql/db/mysql.py +18 -1
- execsql/db/oracle.py +4 -0
- execsql/db/postgres.py +3 -0
- execsql/db/sqlite.py +3 -0
- execsql/db/sqlserver.py +11 -2
- execsql/exceptions.py +18 -0
- execsql/exporters/base.py +6 -0
- execsql/exporters/delimited.py +36 -0
- execsql/exporters/duckdb.py +4 -0
- execsql/exporters/feather.py +4 -0
- execsql/exporters/html.py +6 -0
- execsql/exporters/json.py +5 -6
- execsql/exporters/latex.py +4 -0
- execsql/exporters/ods.py +28 -7
- execsql/exporters/parquet.py +3 -0
- execsql/exporters/pretty.py +5 -0
- execsql/exporters/raw.py +5 -3
- execsql/exporters/sqlite.py +4 -0
- execsql/exporters/templates.py +16 -6
- execsql/exporters/values.py +4 -0
- execsql/exporters/xls.py +26 -7
- execsql/exporters/xml.py +3 -0
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +5 -3
- execsql/importers/csv.py +7 -5
- execsql/importers/feather.py +6 -4
- execsql/importers/ods.py +2 -0
- execsql/importers/xls.py +2 -0
- execsql/metacommands/__init__.py +177 -1968
- execsql/metacommands/dispatch.py +2011 -0
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +144 -406
- execsql/script/variables.py +281 -0
- execsql/types.py +29 -0
- execsql/utils/auth.py +2 -0
- execsql/utils/crypto.py +4 -6
- execsql/utils/datetime.py +1 -0
- execsql/utils/errors.py +11 -0
- execsql/utils/fileio.py +18 -0
- execsql/utils/gui.py +46 -0
- execsql/utils/mail.py +7 -17
- execsql/utils/numeric.py +2 -0
- execsql/utils/regex.py +9 -0
- execsql/utils/strings.py +16 -0
- execsql/utils/timer.py +2 -0
- execsql2-2.4.1.data/data/execsql2_extras/README.md +65 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/METADATA +8 -1
- execsql2-2.4.1.dist-info/RECORD +108 -0
- execsql2-2.2.1.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.2.1.dist-info/RECORD +0 -104
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.2.1.data → execsql2-2.4.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/WHEEL +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.2.1.dist-info → execsql2-2.4.1.dist-info}/licenses/NOTICE +0 -0
execsql/exporters/pretty.py
CHANGED
|
@@ -16,6 +16,8 @@ from execsql.exceptions import ErrInfo
|
|
|
16
16
|
from execsql.utils.errors import exception_desc
|
|
17
17
|
from execsql.utils.fileio import filewriter_close
|
|
18
18
|
|
|
19
|
+
__all__ = ["prettyprint_query", "prettyprint_rowset"]
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
def prettyprint_rowset(
|
|
21
23
|
colhdrs: list[str],
|
|
@@ -26,6 +28,8 @@ def prettyprint_rowset(
|
|
|
26
28
|
desc: str | None = None,
|
|
27
29
|
zipfile: str | None = None,
|
|
28
30
|
) -> None:
|
|
31
|
+
"""Format a pre-fetched result set as a fixed-width human-readable text table and write it."""
|
|
32
|
+
|
|
29
33
|
# Adapted from the pp() function by Aaron Watters,
|
|
30
34
|
# posted to gadfly-rdbms@egroups.com 1999-01-18.
|
|
31
35
|
def as_ucode(s):
|
|
@@ -97,6 +101,7 @@ def prettyprint_query(
|
|
|
97
101
|
desc: str | None = None,
|
|
98
102
|
zipfile: str | None = None,
|
|
99
103
|
) -> None:
|
|
104
|
+
"""Execute a SELECT and write the result set as a column-aligned text table."""
|
|
100
105
|
_state.status.sql_error = False
|
|
101
106
|
names, rows = db.select_data(select_stmt)
|
|
102
107
|
prettyprint_rowset(names, rows, outfile, append, and_val, desc, zipfile=zipfile)
|
execsql/exporters/raw.py
CHANGED
|
@@ -9,11 +9,14 @@ used by the ``EXPORT … FORMAT raw`` and ``FORMAT b64`` metacommand
|
|
|
9
9
|
variants.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import base64
|
|
12
13
|
from typing import Any
|
|
13
14
|
|
|
14
15
|
from execsql.exporters.zip import ZipWriter
|
|
15
16
|
from execsql.utils.fileio import filewriter_close
|
|
16
17
|
|
|
18
|
+
__all__ = ["write_query_raw", "write_query_b64"]
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
def write_query_raw(
|
|
19
22
|
outfile: str,
|
|
@@ -22,6 +25,7 @@ def write_query_raw(
|
|
|
22
25
|
append: bool = False,
|
|
23
26
|
zipfile: str | None = None,
|
|
24
27
|
) -> None:
|
|
28
|
+
"""Write raw binary column data from a row source directly to a file or ZIP archive."""
|
|
25
29
|
if zipfile is None:
|
|
26
30
|
filewriter_close(outfile)
|
|
27
31
|
mode = "wb" if not append else "ab"
|
|
@@ -43,9 +47,7 @@ def write_query_raw(
|
|
|
43
47
|
|
|
44
48
|
|
|
45
49
|
def write_query_b64(outfile: str, rowsource: Any, append: bool = False, zipfile: str | None = None) -> None:
|
|
46
|
-
|
|
47
|
-
import base64
|
|
48
|
-
|
|
50
|
+
"""Decode base64 column data from a row source and write the raw bytes to a file or ZIP archive."""
|
|
49
51
|
if zipfile is None:
|
|
50
52
|
filewriter_close(outfile)
|
|
51
53
|
mode = "wb" if not append else "ab"
|
execsql/exporters/sqlite.py
CHANGED
|
@@ -14,6 +14,8 @@ from typing import Any
|
|
|
14
14
|
from execsql.exceptions import ErrInfo
|
|
15
15
|
from execsql.types import dbt_sqlite
|
|
16
16
|
|
|
17
|
+
__all__ = ["export_sqlite", "write_query_to_sqlite"]
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
def export_sqlite(
|
|
19
21
|
outfile: str,
|
|
@@ -22,6 +24,7 @@ def export_sqlite(
|
|
|
22
24
|
append: bool,
|
|
23
25
|
tablename: str,
|
|
24
26
|
) -> None:
|
|
27
|
+
"""Write pre-fetched rows to a table in an SQLite database file, creating it if necessary."""
|
|
25
28
|
import sqlite3
|
|
26
29
|
|
|
27
30
|
from execsql.models import DataTable
|
|
@@ -71,6 +74,7 @@ def write_query_to_sqlite(
|
|
|
71
74
|
append: bool,
|
|
72
75
|
tablename: str,
|
|
73
76
|
) -> None:
|
|
77
|
+
"""Execute a SELECT and write the result set to a named table in an SQLite database."""
|
|
74
78
|
from execsql.utils.errors import exception_desc
|
|
75
79
|
|
|
76
80
|
try:
|
execsql/exporters/templates.py
CHANGED
|
@@ -9,6 +9,7 @@ substitution) and :func:`report_query`, which drives the
|
|
|
9
9
|
variants. The Jinja2 template processor is loaded lazily when selected.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
|
+
import string
|
|
12
13
|
from typing import Any
|
|
13
14
|
|
|
14
15
|
import execsql.state as _state
|
|
@@ -16,14 +17,16 @@ from execsql.exceptions import ErrInfo
|
|
|
16
17
|
from execsql.utils.errors import fatal_error
|
|
17
18
|
from execsql.utils.fileio import filewriter_close
|
|
18
19
|
|
|
20
|
+
__all__ = ["StrTemplateReport", "JinjaTemplateReport", "report_query"]
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
class StrTemplateReport:
|
|
24
|
+
"""Generates a report by applying Python's :class:`string.Template` to each row of a data table."""
|
|
25
|
+
|
|
21
26
|
# Exporting/reporting using Python's default string.Template, iterated over all
|
|
22
27
|
# rows of a data table.
|
|
23
28
|
def __init__(self, template_file: str) -> None:
|
|
24
|
-
|
|
25
|
-
import string
|
|
26
|
-
|
|
29
|
+
"""Load and compile the template from the given file path."""
|
|
27
30
|
conf = _state.conf
|
|
28
31
|
self.infname = template_file
|
|
29
32
|
from execsql.utils.fileio import EncodedFile
|
|
@@ -46,6 +49,7 @@ class StrTemplateReport:
|
|
|
46
49
|
append: bool = False,
|
|
47
50
|
zipfile: str | None = None,
|
|
48
51
|
) -> None:
|
|
52
|
+
"""Render the template for each row in ``data_dict_rows`` and write the output."""
|
|
49
53
|
conf = _state.conf
|
|
50
54
|
from execsql.utils.fileio import EncodedFile
|
|
51
55
|
from execsql.exporters.zip import ZipWriter
|
|
@@ -70,12 +74,16 @@ class StrTemplateReport:
|
|
|
70
74
|
|
|
71
75
|
|
|
72
76
|
class JinjaTemplateReport:
|
|
77
|
+
"""Generates a report by rendering a Jinja2 template against the full data table."""
|
|
78
|
+
|
|
73
79
|
# Exporting/reporting using the Jinja2 templating library.
|
|
74
80
|
def __init__(self, template_file: str) -> None:
|
|
75
|
-
|
|
81
|
+
"""Load and compile the Jinja2 template from the given file path."""
|
|
76
82
|
try:
|
|
77
83
|
import jinja2
|
|
78
84
|
from jinja2.sandbox import SandboxedEnvironment
|
|
85
|
+
|
|
86
|
+
self._jinja2 = jinja2
|
|
79
87
|
except ImportError:
|
|
80
88
|
fatal_error(
|
|
81
89
|
"The jinja2 library is required to produce reports with the Jinja2 templating system. See http://jinja.pocoo.org/",
|
|
@@ -102,6 +110,7 @@ class JinjaTemplateReport:
|
|
|
102
110
|
append: bool = False,
|
|
103
111
|
zipfile: str | None = None,
|
|
104
112
|
) -> None:
|
|
113
|
+
"""Render the Jinja2 template with ``headers`` and ``datatable`` context and write the output."""
|
|
105
114
|
conf = _state.conf
|
|
106
115
|
from execsql.utils.fileio import EncodedFile
|
|
107
116
|
from execsql.exporters.zip import ZipWriter
|
|
@@ -119,9 +128,9 @@ class JinjaTemplateReport:
|
|
|
119
128
|
ofile = ZipWriter(zipfile, output_dest, append)
|
|
120
129
|
try:
|
|
121
130
|
ofile.write(self.template.render(headers=headers, datatable=data_dict_rows))
|
|
122
|
-
except
|
|
131
|
+
except self._jinja2.TemplateSyntaxError as e:
|
|
123
132
|
raise ErrInfo("error", other_msg=e.message + f" on template line {e.lineno}") from e
|
|
124
|
-
except
|
|
133
|
+
except self._jinja2.TemplateError as e:
|
|
125
134
|
raise ErrInfo("error", other_msg=f"Jinja2 template error ({e.message})") from e
|
|
126
135
|
finally:
|
|
127
136
|
if output_dest != "stdout":
|
|
@@ -136,6 +145,7 @@ def report_query(
|
|
|
136
145
|
append: bool = False,
|
|
137
146
|
zipfile: str | None = None,
|
|
138
147
|
) -> None:
|
|
148
|
+
"""Execute a SELECT and render the result set through a str-template or Jinja2 template file."""
|
|
139
149
|
# Write (export) a template-based report.
|
|
140
150
|
conf = _state.conf
|
|
141
151
|
_state.status.sql_error = False
|
execsql/exporters/values.py
CHANGED
|
@@ -16,6 +16,8 @@ from execsql.exceptions import ErrInfo
|
|
|
16
16
|
from execsql.utils.errors import exception_desc
|
|
17
17
|
from execsql.utils.fileio import filewriter_close
|
|
18
18
|
|
|
19
|
+
__all__ = ["export_values", "write_query_to_values"]
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
def export_values(
|
|
21
23
|
outfile: str,
|
|
@@ -25,6 +27,7 @@ def export_values(
|
|
|
25
27
|
desc: str | None = None,
|
|
26
28
|
zipfile: str | None = None,
|
|
27
29
|
) -> None:
|
|
30
|
+
"""Write pre-fetched rows as SQL INSERT … VALUES statements to a file or ZIP archive."""
|
|
28
31
|
conf = _state.conf
|
|
29
32
|
if outfile.lower() == "stdout":
|
|
30
33
|
f = _state.output
|
|
@@ -70,6 +73,7 @@ def write_query_to_values(
|
|
|
70
73
|
desc: str | None = None,
|
|
71
74
|
zipfile: str | None = None,
|
|
72
75
|
) -> None:
|
|
76
|
+
"""Execute a SELECT and write the result set as SQL INSERT … VALUES statements."""
|
|
73
77
|
try:
|
|
74
78
|
hdrs, rows = db.select_rowsource(select_stmt)
|
|
75
79
|
except ErrInfo:
|
execsql/exporters/xls.py
CHANGED
|
@@ -16,8 +16,12 @@ from typing import Any
|
|
|
16
16
|
from execsql.exceptions import XlsFileError, XlsxFileError
|
|
17
17
|
from execsql.utils.errors import fatal_error
|
|
18
18
|
|
|
19
|
+
__all__ = ["XlsFile", "XlsxFile"]
|
|
20
|
+
|
|
19
21
|
|
|
20
22
|
class XlsFile:
|
|
23
|
+
"""Read-only wrapper around ``xlrd`` for importing legacy ``.xls`` spreadsheets."""
|
|
24
|
+
|
|
21
25
|
def __repr__(self) -> str:
|
|
22
26
|
return "XlsFile()"
|
|
23
27
|
|
|
@@ -29,9 +33,11 @@ class XlsFile:
|
|
|
29
33
|
self.log_msgs.append(msg)
|
|
30
34
|
|
|
31
35
|
def __init__(self) -> None:
|
|
36
|
+
"""Import xlrd and initialise file state; raises a fatal error if xlrd is absent."""
|
|
32
37
|
try:
|
|
33
|
-
global xlrd
|
|
34
38
|
import xlrd
|
|
39
|
+
|
|
40
|
+
self._xlrd = xlrd
|
|
35
41
|
except ImportError:
|
|
36
42
|
fatal_error("The xlrd library is needed to read Excel (.xls) spreadsheets.")
|
|
37
43
|
self.filename = None
|
|
@@ -41,20 +47,23 @@ class XlsFile:
|
|
|
41
47
|
self.errlog = self.XlsLog()
|
|
42
48
|
|
|
43
49
|
def open(self, filename: str, encoding: str | None = None, read_only: bool = False) -> None:
|
|
50
|
+
"""Open an existing ``.xls`` file for reading; raises XlsFileError if absent."""
|
|
44
51
|
self.filename = filename
|
|
45
52
|
self.encoding = encoding
|
|
46
53
|
self.read_only = read_only
|
|
47
54
|
if Path(filename).is_file():
|
|
48
55
|
# The 'read_only' argument is not used, but is present for compatibility with XlsxFile.open().
|
|
49
|
-
self.wbk =
|
|
56
|
+
self.wbk = self._xlrd.open_workbook(filename, logfile=self.errlog, encoding_override=self.encoding)
|
|
50
57
|
self.datemode = self.wbk.datemode
|
|
51
58
|
else:
|
|
52
59
|
raise XlsFileError(f"There is no Excel file {self.filename}.")
|
|
53
60
|
|
|
54
61
|
def sheetnames(self) -> Any:
|
|
62
|
+
"""Return the list of sheet objects in the open workbook."""
|
|
55
63
|
return self.wbk.sheets()
|
|
56
64
|
|
|
57
65
|
def sheet_named(self, sheetname: Any) -> Any:
|
|
66
|
+
"""Return the sheet matching a name or 1-based integer, raising XlsFileError if absent."""
|
|
58
67
|
# Return the sheet with the matching name. If the name is actually an integer,
|
|
59
68
|
# return that sheet number.
|
|
60
69
|
if isinstance(sheetname, int):
|
|
@@ -74,6 +83,7 @@ class XlsFile:
|
|
|
74
83
|
return sheet
|
|
75
84
|
|
|
76
85
|
def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) -> list:
|
|
86
|
+
"""Return all row data from the named sheet, optionally skipping leading junk rows."""
|
|
77
87
|
try:
|
|
78
88
|
sheet = self.sheet_named(sheetname)
|
|
79
89
|
except Exception as e:
|
|
@@ -109,7 +119,7 @@ class XlsFile:
|
|
|
109
119
|
datarow.append(c.value)
|
|
110
120
|
elif c.ctype == 3:
|
|
111
121
|
# date
|
|
112
|
-
dt =
|
|
122
|
+
dt = self._xlrd.xldate_as_tuple(c.value, self.datemode)
|
|
113
123
|
# Convert to time or datetime
|
|
114
124
|
if not any(dt[:3]):
|
|
115
125
|
# No date values
|
|
@@ -121,7 +131,7 @@ class XlsFile:
|
|
|
121
131
|
datarow.append(bool(c.value))
|
|
122
132
|
elif c.ctype == 5:
|
|
123
133
|
# Error code
|
|
124
|
-
datarow.append(
|
|
134
|
+
datarow.append(self._xlrd.error_text_from_code(c.value))
|
|
125
135
|
elif c.ctype == 6:
|
|
126
136
|
# blank
|
|
127
137
|
datarow.append(None)
|
|
@@ -145,6 +155,8 @@ class XlsFile:
|
|
|
145
155
|
|
|
146
156
|
|
|
147
157
|
class XlsxFile:
|
|
158
|
+
"""Read/write wrapper around ``openpyxl`` for ``.xlsx`` spreadsheets."""
|
|
159
|
+
|
|
148
160
|
def __repr__(self) -> str:
|
|
149
161
|
return "XlsxFile()"
|
|
150
162
|
|
|
@@ -156,9 +168,11 @@ class XlsxFile:
|
|
|
156
168
|
self.log_msgs.append(msg)
|
|
157
169
|
|
|
158
170
|
def __init__(self) -> None:
|
|
171
|
+
"""Import openpyxl and initialise file state; raises a fatal error if openpyxl is absent."""
|
|
159
172
|
try:
|
|
160
|
-
global openpyxl
|
|
161
173
|
import openpyxl
|
|
174
|
+
|
|
175
|
+
self._openpyxl = openpyxl
|
|
162
176
|
except ImportError:
|
|
163
177
|
fatal_error("The openpyxl library is needed to read Excel (.xlsx) spreadsheets.")
|
|
164
178
|
self.filename = None
|
|
@@ -168,18 +182,20 @@ class XlsxFile:
|
|
|
168
182
|
self.errlog = self.XlsxLog()
|
|
169
183
|
|
|
170
184
|
def open(self, filename: str, encoding: str | None = None, read_only: bool = False) -> None:
|
|
185
|
+
"""Open an existing ``.xlsx`` file for reading; raises XlsxFileError if absent."""
|
|
171
186
|
self.filename = filename
|
|
172
187
|
self.encoding = encoding
|
|
173
188
|
self.read_only = read_only
|
|
174
189
|
if Path(filename).is_file():
|
|
175
190
|
if read_only:
|
|
176
|
-
self.wbk =
|
|
191
|
+
self.wbk = self._openpyxl.load_workbook(filename, read_only=True)
|
|
177
192
|
else:
|
|
178
|
-
self.wbk =
|
|
193
|
+
self.wbk = self._openpyxl.load_workbook(filename)
|
|
179
194
|
else:
|
|
180
195
|
raise XlsxFileError(f"There is no Excel file {self.filename}.")
|
|
181
196
|
|
|
182
197
|
def close(self) -> None:
|
|
198
|
+
"""Close the open workbook and reset all state attributes."""
|
|
183
199
|
if self.wbk is not None:
|
|
184
200
|
self.wbk.close()
|
|
185
201
|
self.wbk = None
|
|
@@ -187,9 +203,11 @@ class XlsxFile:
|
|
|
187
203
|
self.encoding = None
|
|
188
204
|
|
|
189
205
|
def sheetnames(self) -> list[str]:
|
|
206
|
+
"""Return the list of worksheet names in the open workbook."""
|
|
190
207
|
return self.wbk.sheetnames
|
|
191
208
|
|
|
192
209
|
def sheet_named(self, sheetname: Any) -> Any:
|
|
210
|
+
"""Return the sheet matching a name or 1-based integer index."""
|
|
193
211
|
# Return the sheet with the matching name. If the name is actually an integer,
|
|
194
212
|
# return that sheet number.
|
|
195
213
|
if isinstance(sheetname, int):
|
|
@@ -209,6 +227,7 @@ class XlsxFile:
|
|
|
209
227
|
return sheet
|
|
210
228
|
|
|
211
229
|
def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) -> list:
|
|
230
|
+
"""Return all row data from the named sheet, optionally skipping leading junk rows."""
|
|
212
231
|
try:
|
|
213
232
|
sheet = self.sheet_named(sheetname)
|
|
214
233
|
except Exception as e:
|
execsql/exporters/xml.py
CHANGED
|
@@ -18,6 +18,8 @@ from execsql.exceptions import ErrInfo
|
|
|
18
18
|
from execsql.utils.errors import exception_desc
|
|
19
19
|
from execsql.utils.fileio import filewriter_close
|
|
20
20
|
|
|
21
|
+
__all__ = ["write_query_to_xml"]
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
def write_query_to_xml(
|
|
23
25
|
select_stmt: str,
|
|
@@ -28,6 +30,7 @@ def write_query_to_xml(
|
|
|
28
30
|
desc: str | None = None,
|
|
29
31
|
zipfile: str | None = None,
|
|
30
32
|
) -> None:
|
|
33
|
+
"""Execute a SELECT and write the result set as a well-formed XML document."""
|
|
31
34
|
conf = _state.conf
|
|
32
35
|
try:
|
|
33
36
|
hdrs, rows = db.select_rowsource(select_stmt)
|
execsql/exporters/zip.py
CHANGED
|
@@ -15,9 +15,14 @@ import zipfile
|
|
|
15
15
|
|
|
16
16
|
import execsql.state as _state
|
|
17
17
|
|
|
18
|
+
__all__ = ["WriteableZipfile", "ZipWriter"]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
class WriteableZipfile:
|
|
22
|
+
"""Thin ZipFile wrapper that accepts chunked string writes via an internal buffer."""
|
|
23
|
+
|
|
20
24
|
def __init__(self, zipfile_name: str, append: bool = False) -> None:
|
|
25
|
+
"""Open (or create) a ZIP archive and allocate a write buffer."""
|
|
21
26
|
conf = _state.conf
|
|
22
27
|
self.bufsize = conf.zip_buffer_mb * 1024 * 1000
|
|
23
28
|
self.buf = memoryview(bytearray(self.bufsize))
|
|
@@ -31,6 +36,7 @@ class WriteableZipfile:
|
|
|
31
36
|
self.close()
|
|
32
37
|
|
|
33
38
|
def member_file(self, member_filename: str) -> None:
|
|
39
|
+
"""Create a new member entry in the archive and open it for writing."""
|
|
34
40
|
# Creates a ZipInfo object (file) within the zipfile and opens it for writing.
|
|
35
41
|
self.current_zinfo = zipfile.ZipInfo(
|
|
36
42
|
filename=member_filename,
|
|
@@ -49,6 +55,7 @@ class WriteableZipfile:
|
|
|
49
55
|
self.current_handle = self.zf.open(self.current_zinfo, mode="w")
|
|
50
56
|
|
|
51
57
|
def zip_buffer(self) -> None:
|
|
58
|
+
"""Flush any buffered bytes to the currently open zip member file."""
|
|
52
59
|
# Writes the buffer contents, if any, to the zip member file.
|
|
53
60
|
if self.buflen > 0 and self.current_handle is not None:
|
|
54
61
|
with self.zf._lock:
|
|
@@ -57,6 +64,7 @@ class WriteableZipfile:
|
|
|
57
64
|
self.buflen = 0
|
|
58
65
|
|
|
59
66
|
def write(self, str_data: str) -> None:
|
|
67
|
+
"""Buffer a UTF-8-encoded string for writing to the currently open member."""
|
|
60
68
|
# Writes the given text to the currently open member.
|
|
61
69
|
# Convert from string to bytes.
|
|
62
70
|
data = str_data.encode("utf-8")
|
|
@@ -67,26 +75,33 @@ class WriteableZipfile:
|
|
|
67
75
|
self.buflen = self.buflen + datalen
|
|
68
76
|
|
|
69
77
|
def close_member(self) -> None:
|
|
78
|
+
"""Flush the buffer and close the currently open member file handle."""
|
|
70
79
|
if self.current_handle is not None:
|
|
71
80
|
self.zip_buffer()
|
|
72
81
|
self.current_handle.close()
|
|
73
82
|
self.current_handle = None
|
|
74
83
|
|
|
75
84
|
def close(self) -> None:
|
|
85
|
+
"""Close the open member (flushing the buffer) and finalise the ZIP archive."""
|
|
76
86
|
self.close_member()
|
|
77
87
|
self.zf.close()
|
|
78
88
|
|
|
79
89
|
|
|
80
90
|
class ZipWriter:
|
|
91
|
+
"""High-level write-only interface used by EXPORT metacommands to stream output into a ZIP archive."""
|
|
92
|
+
|
|
81
93
|
def __init__(self, zip_fname: str, member_fname: str, append: bool = False) -> None:
|
|
94
|
+
"""Open the archive at ``zip_fname`` and begin a new member file named ``member_fname``."""
|
|
82
95
|
self.zip_fname = zip_fname
|
|
83
96
|
self.member_fname = member_fname
|
|
84
97
|
self.zwriter = WriteableZipfile(self.zip_fname, append)
|
|
85
98
|
self.member = self.zwriter.member_file(member_fname)
|
|
86
99
|
|
|
87
100
|
def write(self, str_data: str) -> None:
|
|
101
|
+
"""Write a string to the current zip member."""
|
|
88
102
|
self.zwriter.write(str_data)
|
|
89
103
|
|
|
90
104
|
def close(self) -> None:
|
|
105
|
+
"""Close the zip member and finalise the archive."""
|
|
91
106
|
self.zwriter.close()
|
|
92
107
|
self.zwriter = None
|
execsql/importers/base.py
CHANGED
|
@@ -17,6 +17,8 @@ from execsql.db.base import Database
|
|
|
17
17
|
import execsql.state as _state
|
|
18
18
|
from execsql.types import dbt_firebird
|
|
19
19
|
|
|
20
|
+
__all__ = ["import_data_table"]
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
def import_data_table(
|
|
22
24
|
db: Database,
|
|
@@ -26,7 +28,7 @@ def import_data_table(
|
|
|
26
28
|
hdrs: list[str],
|
|
27
29
|
data: list[Any],
|
|
28
30
|
) -> None:
|
|
29
|
-
from execsql.utils.errors import
|
|
31
|
+
from execsql.utils.errors import exception_desc
|
|
30
32
|
|
|
31
33
|
conf = _state.conf
|
|
32
34
|
if any(x is None or len(x.strip()) == 0 for x in hdrs):
|
|
@@ -89,7 +91,7 @@ def import_data_table(
|
|
|
89
91
|
raise ErrInfo(
|
|
90
92
|
type="db",
|
|
91
93
|
command_text=sql,
|
|
92
|
-
exception_msg=
|
|
94
|
+
exception_msg=exception_desc(),
|
|
93
95
|
other_msg=f"Could not create new table ({tablename}) for IMPORT metacommand",
|
|
94
96
|
) from e
|
|
95
97
|
table_cols = db.table_columns(tablename, schemaname)
|
|
@@ -109,4 +111,4 @@ def import_data_table(
|
|
|
109
111
|
except ErrInfo:
|
|
110
112
|
raise
|
|
111
113
|
except Exception as e:
|
|
112
|
-
raise ErrInfo("db", "Call to populate_table when importing data", exception_msg=
|
|
114
|
+
raise ErrInfo("db", "Call to populate_table when importing data", exception_msg=exception_desc()) from e
|
execsql/importers/csv.py
CHANGED
|
@@ -17,6 +17,8 @@ from execsql.db.base import Database
|
|
|
17
17
|
import execsql.state as _state
|
|
18
18
|
from execsql.types import dbt_firebird
|
|
19
19
|
|
|
20
|
+
__all__ = ["importfile", "importtable"]
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
def importtable(
|
|
22
24
|
db: Database,
|
|
@@ -30,7 +32,7 @@ def importtable(
|
|
|
30
32
|
encoding: str | None = None,
|
|
31
33
|
junk_header_lines: int = 0,
|
|
32
34
|
) -> None:
|
|
33
|
-
from execsql.utils.errors import
|
|
35
|
+
from execsql.utils.errors import exception_desc
|
|
34
36
|
|
|
35
37
|
conf = _state.conf
|
|
36
38
|
if not Path(filename).is_file():
|
|
@@ -64,7 +66,7 @@ def importtable(
|
|
|
64
66
|
raise ErrInfo(
|
|
65
67
|
type="db",
|
|
66
68
|
command_text=sql,
|
|
67
|
-
exception_msg=
|
|
69
|
+
exception_msg=exception_desc(),
|
|
68
70
|
other_msg=f"Could not create new table ({tablename}) for IMPORT metacommand",
|
|
69
71
|
) from e
|
|
70
72
|
else:
|
|
@@ -89,7 +91,7 @@ def importtable(
|
|
|
89
91
|
fq_tablename = db.schema_qualified_table_name(schemaname, tablename)
|
|
90
92
|
raise ErrInfo(
|
|
91
93
|
"exception",
|
|
92
|
-
exception_msg=
|
|
94
|
+
exception_msg=exception_desc(),
|
|
93
95
|
other_msg=f"Can't import tabular file ({filename}) to table ({fq_tablename})",
|
|
94
96
|
) from e
|
|
95
97
|
inf.close()
|
|
@@ -102,7 +104,7 @@ def importfile(
|
|
|
102
104
|
columname: str,
|
|
103
105
|
filename: str,
|
|
104
106
|
) -> None:
|
|
105
|
-
from execsql.utils.errors import
|
|
107
|
+
from execsql.utils.errors import exception_desc
|
|
106
108
|
|
|
107
109
|
if schemaname is not None:
|
|
108
110
|
if not db.table_exists(tablename, schemaname):
|
|
@@ -125,6 +127,6 @@ def importfile(
|
|
|
125
127
|
fq_tablename = db.schema_qualified_table_name(schemaname, tablename)
|
|
126
128
|
raise ErrInfo(
|
|
127
129
|
"exception",
|
|
128
|
-
exception_msg=
|
|
130
|
+
exception_msg=exception_desc(),
|
|
129
131
|
other_msg=f"Can't import file ({filename}) to table ({fq_tablename})",
|
|
130
132
|
) from e
|
execsql/importers/feather.py
CHANGED
|
@@ -14,6 +14,8 @@ from execsql.exceptions import ErrInfo
|
|
|
14
14
|
from execsql.db.base import Database
|
|
15
15
|
from execsql.importers.base import import_data_table
|
|
16
16
|
|
|
17
|
+
__all__ = ["import_feather", "import_parquet"]
|
|
18
|
+
|
|
17
19
|
|
|
18
20
|
def import_feather(
|
|
19
21
|
db: Database,
|
|
@@ -22,14 +24,14 @@ def import_feather(
|
|
|
22
24
|
filename: str,
|
|
23
25
|
is_new: Any,
|
|
24
26
|
) -> None:
|
|
25
|
-
from execsql.utils.errors import
|
|
27
|
+
from execsql.utils.errors import exception_desc
|
|
26
28
|
|
|
27
29
|
try:
|
|
28
30
|
import polars as pl
|
|
29
31
|
except Exception as e:
|
|
30
32
|
raise ErrInfo(
|
|
31
33
|
"exception",
|
|
32
|
-
exception_msg=
|
|
34
|
+
exception_msg=exception_desc(),
|
|
33
35
|
other_msg="The polars Python library must be installed to import data from the Feather format.",
|
|
34
36
|
) from e
|
|
35
37
|
df = pl.read_ipc(filename)
|
|
@@ -45,14 +47,14 @@ def import_parquet(
|
|
|
45
47
|
filename: str,
|
|
46
48
|
is_new: Any,
|
|
47
49
|
) -> None:
|
|
48
|
-
from execsql.utils.errors import
|
|
50
|
+
from execsql.utils.errors import exception_desc
|
|
49
51
|
|
|
50
52
|
try:
|
|
51
53
|
import polars as pl
|
|
52
54
|
except Exception as e:
|
|
53
55
|
raise ErrInfo(
|
|
54
56
|
"exception",
|
|
55
|
-
exception_msg=
|
|
57
|
+
exception_msg=exception_desc(),
|
|
56
58
|
other_msg="The polars Python library must be installed to import data from the Parquet format.",
|
|
57
59
|
) from e
|
|
58
60
|
df = pl.read_parquet(filename)
|
execsql/importers/ods.py
CHANGED