execsql2 2.1.2__py3-none-any.whl → 2.4.0__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/__init__.py +436 -0
- execsql/cli/dsn.py +86 -0
- execsql/cli/help.py +140 -0
- execsql/{cli.py → cli/run.py} +14 -589
- execsql/config.py +65 -1
- execsql/db/access.py +27 -15
- execsql/db/base.py +328 -215
- execsql/db/dsn.py +10 -5
- execsql/db/duckdb.py +6 -2
- execsql/db/factory.py +21 -0
- execsql/db/firebird.py +27 -19
- execsql/db/mysql.py +12 -7
- execsql/db/oracle.py +15 -11
- execsql/db/postgres.py +31 -16
- execsql/db/sqlite.py +15 -11
- execsql/db/sqlserver.py +16 -5
- execsql/exceptions.py +25 -7
- execsql/exporters/base.py +12 -1
- execsql/exporters/delimited.py +80 -35
- execsql/exporters/duckdb.py +6 -2
- execsql/exporters/feather.py +10 -6
- execsql/exporters/html.py +89 -69
- execsql/exporters/json.py +52 -45
- execsql/exporters/latex.py +37 -27
- execsql/exporters/ods.py +32 -11
- execsql/exporters/parquet.py +5 -2
- execsql/exporters/pretty.py +16 -9
- execsql/exporters/raw.py +22 -16
- execsql/exporters/sqlite.py +6 -2
- execsql/exporters/templates.py +39 -21
- execsql/exporters/values.py +26 -20
- execsql/exporters/xls.py +30 -11
- execsql/exporters/xml.py +31 -13
- execsql/exporters/zip.py +15 -0
- execsql/importers/base.py +6 -4
- execsql/importers/csv.py +8 -6
- execsql/importers/feather.py +6 -4
- execsql/importers/ods.py +6 -4
- execsql/importers/xls.py +6 -4
- execsql/metacommands/__init__.py +208 -1548
- execsql/metacommands/conditions.py +101 -27
- execsql/metacommands/control.py +8 -4
- execsql/metacommands/data.py +6 -6
- execsql/metacommands/debug.py +6 -2
- execsql/metacommands/dispatch.py +2011 -0
- execsql/metacommands/io.py +67 -1310
- execsql/metacommands/io_export.py +442 -0
- execsql/metacommands/io_fileops.py +287 -0
- execsql/metacommands/io_import.py +398 -0
- execsql/metacommands/io_write.py +248 -0
- execsql/metacommands/prompt.py +22 -66
- execsql/metacommands/system.py +7 -2
- execsql/models.py +7 -0
- execsql/parser.py +10 -0
- execsql/py.typed +0 -0
- execsql/script/__init__.py +95 -0
- execsql/script/control.py +162 -0
- execsql/{script.py → script/engine.py} +184 -402
- execsql/script/variables.py +281 -0
- execsql/types.py +49 -20
- 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 +33 -8
- 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.0.data/data/execsql2_extras/README.md +65 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/execsql.conf +1 -1
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/METADATA +13 -6
- execsql2-2.4.0.dist-info/RECORD +108 -0
- execsql2-2.1.2.data/data/execsql2_extras/READ_ME.rst +0 -127
- execsql2-2.1.2.dist-info/RECORD +0 -96
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.1.2.data → execsql2-2.4.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/WHEEL +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.1.2.dist-info → execsql2-2.4.0.dist-info}/licenses/NOTICE +0 -0
execsql/db/sqlite.py
CHANGED
|
@@ -18,11 +18,14 @@ from execsql.exceptions import ErrInfo
|
|
|
18
18
|
from execsql.utils.errors import exception_desc, fatal_error
|
|
19
19
|
import execsql.state as _state
|
|
20
20
|
|
|
21
|
+
__all__ = ["SQLiteDatabase"]
|
|
21
22
|
|
|
22
23
|
DEFAULT_CONNECT_TIMEOUT = 30 # seconds
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class SQLiteDatabase(Database):
|
|
27
|
+
"""SQLite adapter using the Python standard-library sqlite3 module."""
|
|
28
|
+
|
|
26
29
|
def __init__(self, SQLite_fn: str, timeout: float = DEFAULT_CONNECT_TIMEOUT) -> None:
|
|
27
30
|
try:
|
|
28
31
|
import sqlite3 # noqa: F401
|
|
@@ -54,12 +57,12 @@ class SQLiteDatabase(Database):
|
|
|
54
57
|
self.conn = sqlite3.connect(self.db_name, timeout=self.timeout)
|
|
55
58
|
except ErrInfo:
|
|
56
59
|
raise
|
|
57
|
-
except Exception:
|
|
60
|
+
except Exception as e:
|
|
58
61
|
raise ErrInfo(
|
|
59
62
|
type="exception",
|
|
60
63
|
exception_msg=exception_desc(),
|
|
61
64
|
other_msg=f"Can't open SQLite database {self.db_name}",
|
|
62
|
-
)
|
|
65
|
+
) from e
|
|
63
66
|
pragma_cols, pragma_data = self.select_data("pragma encoding;")
|
|
64
67
|
self.encoding = pragma_data[0][0]
|
|
65
68
|
|
|
@@ -82,14 +85,14 @@ class SQLiteDatabase(Database):
|
|
|
82
85
|
curs.execute(sql, (table_name,))
|
|
83
86
|
except ErrInfo:
|
|
84
87
|
raise
|
|
85
|
-
except Exception:
|
|
88
|
+
except Exception as e:
|
|
86
89
|
self.rollback()
|
|
87
90
|
raise ErrInfo(
|
|
88
91
|
type="db",
|
|
89
92
|
command_text=sql,
|
|
90
93
|
exception_msg=exception_desc(),
|
|
91
94
|
other_msg=f'Failed test for existence of SQLite table "{table_name}";',
|
|
92
|
-
)
|
|
95
|
+
) from e
|
|
93
96
|
rows = curs.fetchall()
|
|
94
97
|
return len(rows) > 0
|
|
95
98
|
|
|
@@ -110,14 +113,14 @@ class SQLiteDatabase(Database):
|
|
|
110
113
|
curs.execute(sql)
|
|
111
114
|
except ErrInfo:
|
|
112
115
|
raise
|
|
113
|
-
except Exception:
|
|
116
|
+
except Exception as e:
|
|
114
117
|
self.rollback()
|
|
115
118
|
raise ErrInfo(
|
|
116
119
|
type="db",
|
|
117
120
|
command_text=sql,
|
|
118
121
|
exception_msg=exception_desc(),
|
|
119
122
|
other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
|
|
120
|
-
)
|
|
123
|
+
) from e
|
|
121
124
|
return [d[0] for d in curs.description]
|
|
122
125
|
|
|
123
126
|
def view_exists(self, view_name: str) -> bool:
|
|
@@ -127,14 +130,14 @@ class SQLiteDatabase(Database):
|
|
|
127
130
|
curs.execute(sql, (view_name,))
|
|
128
131
|
except ErrInfo:
|
|
129
132
|
raise
|
|
130
|
-
except Exception:
|
|
133
|
+
except Exception as e:
|
|
131
134
|
self.rollback()
|
|
132
135
|
raise ErrInfo(
|
|
133
136
|
type="db",
|
|
134
137
|
command_text=sql,
|
|
135
138
|
exception_msg=exception_desc(),
|
|
136
139
|
other_msg=f'Failed test for existence of SQLite view "{view_name}";',
|
|
137
|
-
)
|
|
140
|
+
) from e
|
|
138
141
|
rows = curs.fetchall()
|
|
139
142
|
return len(rows) > 0
|
|
140
143
|
|
|
@@ -204,14 +207,14 @@ class SQLiteDatabase(Database):
|
|
|
204
207
|
curs.execute(sql, linedata)
|
|
205
208
|
except ErrInfo:
|
|
206
209
|
raise
|
|
207
|
-
except Exception:
|
|
210
|
+
except Exception as e:
|
|
208
211
|
self.rollback()
|
|
209
212
|
raise ErrInfo(
|
|
210
213
|
type="db",
|
|
211
214
|
command_text=sql,
|
|
212
215
|
exception_msg=exception_desc(),
|
|
213
216
|
other_msg=f"Can't load data into table {sq_name} from line {{{line}}}",
|
|
214
|
-
)
|
|
217
|
+
) from e
|
|
215
218
|
total_rows += 1
|
|
216
219
|
interval = getattr(_state.conf, "import_progress_interval", 0)
|
|
217
220
|
if _state.exec_log and interval > 0 and total_rows % interval == 0:
|
|
@@ -235,5 +238,6 @@ class SQLiteDatabase(Database):
|
|
|
235
238
|
with open(file_name, "rb") as f:
|
|
236
239
|
filedata = f.read()
|
|
237
240
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
238
|
-
|
|
241
|
+
quoted_col = self.quote_identifier(column_name)
|
|
242
|
+
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
239
243
|
self.cursor().execute(sql, (sqlite3.Binary(filedata),))
|
execsql/db/sqlserver.py
CHANGED
|
@@ -7,6 +7,7 @@ Implements :class:`SqlServerDatabase`, which connects to Microsoft SQL
|
|
|
7
7
|
Server via ``pyodbc``. Corresponds to ``-t s`` on the CLI.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
import re
|
|
10
11
|
|
|
11
12
|
from execsql.db.base import Database
|
|
12
13
|
from execsql.exceptions import ErrInfo
|
|
@@ -14,8 +15,12 @@ from execsql.utils.errors import fatal_error
|
|
|
14
15
|
from execsql.utils.auth import clear_stored_password, get_password, password_from_keyring
|
|
15
16
|
import execsql.state as _state
|
|
16
17
|
|
|
18
|
+
__all__ = ["SqlServerDatabase"]
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
class SqlServerDatabase(Database):
|
|
22
|
+
"""Microsoft SQL Server adapter using pyodbc, trying drivers from newest to oldest."""
|
|
23
|
+
|
|
19
24
|
def __init__(
|
|
20
25
|
self,
|
|
21
26
|
server_name: str,
|
|
@@ -96,9 +101,13 @@ class SqlServerDatabase(Database):
|
|
|
96
101
|
try:
|
|
97
102
|
self.conn = pyodbc.connect(connstr)
|
|
98
103
|
except Exception:
|
|
99
|
-
_state.exec_log.log_status_info(
|
|
104
|
+
_state.exec_log.log_status_info(
|
|
105
|
+
f"Could not connect using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
|
|
106
|
+
)
|
|
100
107
|
else:
|
|
101
|
-
_state.exec_log.log_status_info(
|
|
108
|
+
_state.exec_log.log_status_info(
|
|
109
|
+
f"Connected using: {re.sub(r'Pwd=[^;]*', 'Pwd=***', connstr)}",
|
|
110
|
+
)
|
|
102
111
|
return True
|
|
103
112
|
return False
|
|
104
113
|
|
|
@@ -141,7 +150,7 @@ class SqlServerDatabase(Database):
|
|
|
141
150
|
|
|
142
151
|
def schema_exists(self, schema_name: str) -> bool:
|
|
143
152
|
curs = self.cursor()
|
|
144
|
-
curs.execute(
|
|
153
|
+
curs.execute("select * from sys.schemas where name = ?;", (schema_name,))
|
|
145
154
|
rows = curs.fetchall()
|
|
146
155
|
curs.close()
|
|
147
156
|
return len(rows) > 0
|
|
@@ -149,7 +158,8 @@ class SqlServerDatabase(Database):
|
|
|
149
158
|
def role_exists(self, rolename: str) -> bool:
|
|
150
159
|
curs = self.cursor()
|
|
151
160
|
curs.execute(
|
|
152
|
-
|
|
161
|
+
"select name from sys.database_principals where type in ('R', 'S') and name = ?;",
|
|
162
|
+
(rolename,),
|
|
153
163
|
)
|
|
154
164
|
rows = curs.fetchall()
|
|
155
165
|
curs.close()
|
|
@@ -172,5 +182,6 @@ class SqlServerDatabase(Database):
|
|
|
172
182
|
with open(file_name, "rb") as f:
|
|
173
183
|
filedata = f.read()
|
|
174
184
|
sq_name = self.schema_qualified_table_name(schema_name, table_name)
|
|
175
|
-
|
|
185
|
+
quoted_col = self.quote_identifier(column_name)
|
|
186
|
+
sql = f"insert into {sq_name} ({quoted_col}) values ({self.paramsubs(1)});"
|
|
176
187
|
self.cursor().execute(sql, (pyodbc.Binary(filedata),))
|
execsql/exceptions.py
CHANGED
|
@@ -21,6 +21,24 @@ from a single location. Notable exceptions:
|
|
|
21
21
|
- :class:`CondParserError` / :class:`NumericParserError` — parser failures.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
+
__all__ = [
|
|
25
|
+
"ExecSqlError",
|
|
26
|
+
"ConfigError",
|
|
27
|
+
"ExecSqlTimeoutError",
|
|
28
|
+
"ErrInfo",
|
|
29
|
+
"DataTypeError",
|
|
30
|
+
"DbTypeError",
|
|
31
|
+
"ColumnError",
|
|
32
|
+
"DataTableError",
|
|
33
|
+
"DatabaseNotImplementedError",
|
|
34
|
+
"OdsFileError",
|
|
35
|
+
"XlsFileError",
|
|
36
|
+
"XlsxFileError",
|
|
37
|
+
"ConsoleUIError",
|
|
38
|
+
"CondParserError",
|
|
39
|
+
"NumericParserError",
|
|
40
|
+
]
|
|
41
|
+
|
|
24
42
|
|
|
25
43
|
class ExecSqlError(Exception):
|
|
26
44
|
"""Base class for simple single-message execsql exceptions.
|
|
@@ -58,7 +76,7 @@ class ExecSqlTimeoutError(ExecSqlError):
|
|
|
58
76
|
super().__init__(errmsg)
|
|
59
77
|
|
|
60
78
|
|
|
61
|
-
class ErrInfo(
|
|
79
|
+
class ErrInfo(ExecSqlError):
|
|
62
80
|
"""Rich exception and error-data carrier for execsql.
|
|
63
81
|
|
|
64
82
|
``str(e)`` returns the most informative available message (``other_msg``,
|
|
@@ -152,11 +170,11 @@ class ErrInfo(Exception):
|
|
|
152
170
|
return self.eval_err()
|
|
153
171
|
|
|
154
172
|
|
|
155
|
-
class DataTypeError(
|
|
173
|
+
class DataTypeError(ExecSqlError):
|
|
156
174
|
def __init__(self, data_type_name: str, error_msg: str) -> None:
|
|
157
175
|
self.data_type_name = data_type_name or "Unspecified data type"
|
|
158
176
|
self.error_msg = error_msg or "Unspecified error"
|
|
159
|
-
|
|
177
|
+
Exception.__init__(self, str(self))
|
|
160
178
|
|
|
161
179
|
def __repr__(self) -> str:
|
|
162
180
|
return f"DataTypeError({self.data_type_name!r}, {self.error_msg!r})"
|
|
@@ -165,12 +183,12 @@ class DataTypeError(Exception):
|
|
|
165
183
|
return f"{self.data_type_name}: {self.error_msg}"
|
|
166
184
|
|
|
167
185
|
|
|
168
|
-
class DbTypeError(
|
|
186
|
+
class DbTypeError(ExecSqlError):
|
|
169
187
|
def __init__(self, dbms_id: str, data_type: object, error_msg: str) -> None:
|
|
170
188
|
self.dbms_id = dbms_id
|
|
171
189
|
self.data_type = data_type
|
|
172
190
|
self.error_msg = error_msg or "Unspecified error"
|
|
173
|
-
|
|
191
|
+
Exception.__init__(self, str(self))
|
|
174
192
|
|
|
175
193
|
def __repr__(self) -> str:
|
|
176
194
|
return f"DbTypeError({self.dbms_id!r}, {self.data_type!r}, {self.error_msg!r})"
|
|
@@ -190,11 +208,11 @@ class DataTableError(ExecSqlError):
|
|
|
190
208
|
"""Raised for DataTable-level errors."""
|
|
191
209
|
|
|
192
210
|
|
|
193
|
-
class DatabaseNotImplementedError(
|
|
211
|
+
class DatabaseNotImplementedError(ExecSqlError):
|
|
194
212
|
def __init__(self, db_name: str, method: str) -> None:
|
|
195
213
|
self.db_name = db_name
|
|
196
214
|
self.method = method
|
|
197
|
-
|
|
215
|
+
Exception.__init__(self, str(self))
|
|
198
216
|
|
|
199
217
|
def __repr__(self) -> str:
|
|
200
218
|
return f"DatabaseNotImplementedError({self.db_name!r}, {self.method!r})"
|
execsql/exporters/base.py
CHANGED
|
@@ -22,6 +22,8 @@ from execsql.script import current_script_line
|
|
|
22
22
|
from execsql.utils.errors import file_size_date
|
|
23
23
|
from execsql.utils.gui import ConsoleUIError
|
|
24
24
|
|
|
25
|
+
__all__ = ["WriteSpec", "ExportRecord", "ExportMetadata"]
|
|
26
|
+
|
|
25
27
|
|
|
26
28
|
class ExportRecord:
|
|
27
29
|
"""Records the details of a single EXPORT operation for metadata tracking.
|
|
@@ -88,15 +90,18 @@ class ExportMetadata:
|
|
|
88
90
|
self.recordlist: list[ExportRecord] = []
|
|
89
91
|
|
|
90
92
|
def add(self, exp_record: ExportRecord) -> None:
|
|
93
|
+
"""Append an export record to the collection."""
|
|
91
94
|
self.recordlist.append(exp_record)
|
|
92
95
|
|
|
93
96
|
def get(self):
|
|
97
|
+
"""Return column headers and all not-yet-exported records, marking them exported."""
|
|
94
98
|
recs = [er.record for er in self.recordlist if not er.exported]
|
|
95
99
|
for er in self.recordlist:
|
|
96
100
|
er.exported = True
|
|
97
101
|
return self.colhdrs, recs
|
|
98
102
|
|
|
99
103
|
def get_all(self):
|
|
104
|
+
"""Return column headers and every record regardless of prior export state."""
|
|
100
105
|
recs = [er.record for er in self.recordlist]
|
|
101
106
|
for er in self.recordlist:
|
|
102
107
|
er.exported = True
|
|
@@ -130,6 +135,7 @@ class WriteSpec:
|
|
|
130
135
|
self.written = False
|
|
131
136
|
|
|
132
137
|
def write(self) -> None:
|
|
138
|
+
"""Execute the deferred write, expanding substitution variables first."""
|
|
133
139
|
# Writes the message per the specifications given to '__init__()'. Substitution
|
|
134
140
|
# variables are processed.
|
|
135
141
|
# Inputs: no inputs.
|
|
@@ -143,7 +149,12 @@ class WriteSpec:
|
|
|
143
149
|
if self.outfile:
|
|
144
150
|
from execsql.utils.fileio import EncodedFile
|
|
145
151
|
|
|
146
|
-
EncodedFile(self.outfile, conf.output_encoding)
|
|
152
|
+
ef = EncodedFile(self.outfile, conf.output_encoding)
|
|
153
|
+
fh = ef.open("a")
|
|
154
|
+
try:
|
|
155
|
+
fh.write(msg)
|
|
156
|
+
finally:
|
|
157
|
+
fh.close()
|
|
147
158
|
if (not self.outfile) or self.tee:
|
|
148
159
|
try:
|
|
149
160
|
_state.output.write(msg.encode(conf.output_encoding))
|
execsql/exporters/delimited.py
CHANGED
|
@@ -29,9 +29,14 @@ from execsql.utils.errors import exception_desc
|
|
|
29
29
|
from execsql.utils.fileio import filewriter_close
|
|
30
30
|
from execsql.utils.strings import clean_words, fold_words
|
|
31
31
|
|
|
32
|
+
__all__ = ["LineDelimiter", "CsvFile", "CsvWriter", "DelimitedWriter", "write_delimited_file"]
|
|
33
|
+
|
|
32
34
|
|
|
33
35
|
class LineDelimiter:
|
|
36
|
+
"""Encapsulates delimiter, quote character, and escape rules for a single line format."""
|
|
37
|
+
|
|
34
38
|
def __init__(self, delim: str | None, quote: str | None, escchar: str | None) -> None:
|
|
39
|
+
"""Initialise the line-format constants for a given delimiter/quote pair."""
|
|
35
40
|
self.delimiter = delim
|
|
36
41
|
self.joinchar = delim if delim else ""
|
|
37
42
|
self.quotechar = quote
|
|
@@ -44,6 +49,7 @@ class LineDelimiter:
|
|
|
44
49
|
self.quotedquote = None
|
|
45
50
|
|
|
46
51
|
def delimited(self, datarow: Any, add_newline: bool = True) -> str:
|
|
52
|
+
"""Format a sequence of values as a single delimited text line."""
|
|
47
53
|
conf = _state.conf
|
|
48
54
|
if self.quotechar:
|
|
49
55
|
d_row = []
|
|
@@ -79,22 +85,30 @@ class LineDelimiter:
|
|
|
79
85
|
|
|
80
86
|
|
|
81
87
|
class DelimitedWriter:
|
|
88
|
+
"""Low-level writer that formats data rows as delimited text and sends them to an open file object."""
|
|
89
|
+
|
|
82
90
|
def __init__(self, outfile: Any, delim: str | None, quote: str | None, escchar: str | None) -> None:
|
|
91
|
+
"""Wrap an open file object with a :class:`LineDelimiter` for row-by-row writing."""
|
|
83
92
|
self.outfile = outfile
|
|
84
93
|
self.line_delimiter = LineDelimiter(delim, quote, escchar)
|
|
85
94
|
|
|
86
95
|
def write(self, text_str: str) -> None:
|
|
96
|
+
"""Write a raw string directly to the underlying file object."""
|
|
87
97
|
self.outfile.write(text_str)
|
|
88
98
|
|
|
89
99
|
def writerow(self, datarow: Any) -> None:
|
|
100
|
+
"""Format one data row as delimited text and write it to the file."""
|
|
90
101
|
self.outfile.write(self.line_delimiter.delimited(datarow))
|
|
91
102
|
|
|
92
103
|
def writerows(self, datarows: Any) -> None:
|
|
104
|
+
"""Write each row in an iterable of data rows to the file."""
|
|
93
105
|
for row in datarows:
|
|
94
106
|
self.writerow(row)
|
|
95
107
|
|
|
96
108
|
|
|
97
109
|
class CsvWriter:
|
|
110
|
+
"""Opens a named file (or stdout) and exposes a row-oriented delimited-text writing API."""
|
|
111
|
+
|
|
98
112
|
def __init__(
|
|
99
113
|
self,
|
|
100
114
|
filename: str,
|
|
@@ -113,21 +127,32 @@ class CsvWriter:
|
|
|
113
127
|
self.dwriter = DelimitedWriter(self.output, delim, quote, escchar)
|
|
114
128
|
|
|
115
129
|
def write(self, text_str: str) -> None:
|
|
130
|
+
"""Write a raw string to the output file."""
|
|
116
131
|
self.dwriter.write(text_str)
|
|
117
132
|
|
|
118
133
|
def writerow(self, datarow: Any) -> None:
|
|
134
|
+
"""Format and write a single data row."""
|
|
119
135
|
self.dwriter.writerow(datarow)
|
|
120
136
|
|
|
121
137
|
def writerows(self, datarows: Any) -> None:
|
|
138
|
+
"""Format and write each row in an iterable."""
|
|
122
139
|
self.dwriter.writerows(datarows)
|
|
123
140
|
|
|
124
141
|
def close(self) -> None:
|
|
142
|
+
"""Close the underlying output file."""
|
|
125
143
|
self.output.close()
|
|
126
144
|
self.output = None
|
|
127
145
|
|
|
128
146
|
|
|
129
147
|
class CsvFile(EncodedFile):
|
|
148
|
+
"""Full delimited-file reader/writer with automatic format diagnosis.
|
|
149
|
+
|
|
150
|
+
Supports custom delimiters, quoting, encoding, junk-header skipping,
|
|
151
|
+
column-type inference, and ZIP output via :class:`CsvWriter`.
|
|
152
|
+
"""
|
|
153
|
+
|
|
130
154
|
def __init__(self, csvfname: str, file_encoding: str, junk_header_lines: int = 0) -> None:
|
|
155
|
+
"""Open a CSV file path for later reading or writing."""
|
|
131
156
|
super().__init__(csvfname, file_encoding)
|
|
132
157
|
self.csvfname = csvfname
|
|
133
158
|
self.junk_header_lines = junk_header_lines
|
|
@@ -143,6 +168,7 @@ class CsvFile(EncodedFile):
|
|
|
143
168
|
return f"CsvFile({self.csvfname!r}, {self.encoding!r})"
|
|
144
169
|
|
|
145
170
|
def openclean(self, mode: str) -> Any:
|
|
171
|
+
"""Return an opened file object with the configured number of junk header lines skipped."""
|
|
146
172
|
# Returns an opened file object with junk headers stripped.
|
|
147
173
|
f = self.open(mode)
|
|
148
174
|
for _ in range(self.junk_header_lines):
|
|
@@ -150,6 +176,7 @@ class CsvFile(EncodedFile):
|
|
|
150
176
|
return f
|
|
151
177
|
|
|
152
178
|
def lineformat(self, delimiter: str | None, quotechar: str | None, escapechar: str | None) -> None:
|
|
179
|
+
"""Explicitly set the delimiter, quote character, and escape character."""
|
|
153
180
|
# Specifies the format of a line.
|
|
154
181
|
self.delimiter = delimiter
|
|
155
182
|
self.quotechar = quotechar
|
|
@@ -413,15 +440,21 @@ class CsvFile(EncodedFile):
|
|
|
413
440
|
)
|
|
414
441
|
|
|
415
442
|
def evaluate_line_format(self) -> None:
|
|
443
|
+
"""Scan the file to auto-detect the delimiter, quote character, and escape character."""
|
|
416
444
|
# Scans the file to determine the delimiter, quote character, and escapechar.
|
|
417
445
|
if not self.lineformat_set:
|
|
418
|
-
|
|
446
|
+
f = self.openclean("rt")
|
|
447
|
+
try:
|
|
448
|
+
self.delimiter, self.quotechar, self.escapechar = self.diagnose_delim(f)
|
|
449
|
+
finally:
|
|
450
|
+
f.close()
|
|
419
451
|
self.lineformat_set = True
|
|
420
452
|
|
|
421
453
|
def _record_format_error(self, pos_no: int, errmsg: str) -> None:
|
|
422
454
|
self.parse_errors.append(f"{errmsg} in position {pos_no}")
|
|
423
455
|
|
|
424
456
|
def read_and_parse_line(self, f: Any) -> list:
|
|
457
|
+
"""Read and parse one line from an open file, returning a list of field values."""
|
|
425
458
|
# Returns a list of line elements, parsed according to the established delimiter and quotechar.
|
|
426
459
|
elements = []
|
|
427
460
|
eat_multiple_delims = self.delimiter == " "
|
|
@@ -567,30 +600,34 @@ class CsvFile(EncodedFile):
|
|
|
567
600
|
return elements
|
|
568
601
|
|
|
569
602
|
def reader(self) -> Any:
|
|
603
|
+
"""Yield parsed rows from the file as lists of field values."""
|
|
570
604
|
conf = _state.conf
|
|
571
605
|
self.evaluate_line_format()
|
|
572
606
|
f = self.openclean("rt")
|
|
573
607
|
line_no = 0
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
608
|
+
try:
|
|
609
|
+
while True:
|
|
610
|
+
line_no += 1
|
|
611
|
+
try:
|
|
612
|
+
elements = self.read_and_parse_line(f)
|
|
613
|
+
except ErrInfo as e:
|
|
614
|
+
raise ErrInfo("error", other_msg=f"{e.other} on line {line_no}.") from e
|
|
615
|
+
except:
|
|
616
|
+
raise
|
|
617
|
+
if len(elements) > 0:
|
|
618
|
+
if conf.del_empty_cols and len(self.blank_cols) > 0:
|
|
619
|
+
blanks = copy.copy(self.blank_cols)
|
|
620
|
+
while len(blanks) > 0:
|
|
621
|
+
b = blanks.pop()
|
|
622
|
+
del elements[b]
|
|
623
|
+
yield elements
|
|
624
|
+
else:
|
|
625
|
+
break
|
|
626
|
+
finally:
|
|
627
|
+
f.close()
|
|
592
628
|
|
|
593
629
|
def writer(self, append: bool = False) -> CsvWriter:
|
|
630
|
+
"""Return a :class:`CsvWriter` configured with this file's format settings."""
|
|
594
631
|
return CsvWriter(self.filename, self.encoding, self.delimiter, self.quotechar, self.escapechar, append)
|
|
595
632
|
|
|
596
633
|
def _colhdrs(self, inf: Any) -> list[str]:
|
|
@@ -600,12 +637,12 @@ class CsvFile(EncodedFile):
|
|
|
600
637
|
except ErrInfo as e:
|
|
601
638
|
e.other = f"Can't read column header line from {self.filename}. {e.other or ''}"
|
|
602
639
|
raise
|
|
603
|
-
except Exception:
|
|
640
|
+
except Exception as e:
|
|
604
641
|
raise ErrInfo(
|
|
605
642
|
type="exception",
|
|
606
643
|
exception_msg=exception_desc(),
|
|
607
644
|
other_msg=f"Can't read column header line from {self.filename}",
|
|
608
|
-
)
|
|
645
|
+
) from e
|
|
609
646
|
if any(x is None or len(x) == 0 for x in colnames):
|
|
610
647
|
if conf.del_empty_cols:
|
|
611
648
|
self.blank_cols = [
|
|
@@ -634,17 +671,20 @@ class CsvFile(EncodedFile):
|
|
|
634
671
|
return colnames
|
|
635
672
|
|
|
636
673
|
def column_headers(self) -> list[str]:
|
|
674
|
+
"""Return the first row of the file as a list of column header strings."""
|
|
637
675
|
if not self.lineformat_set:
|
|
638
676
|
self.evaluate_line_format()
|
|
639
677
|
inf = self.reader()
|
|
640
678
|
return self._colhdrs(inf)
|
|
641
679
|
|
|
642
680
|
def data_table_def(self) -> Any:
|
|
681
|
+
"""Return a :class:`DataTable` describing the file's columns and types."""
|
|
643
682
|
if self.table_data is None:
|
|
644
683
|
self.evaluate_column_types()
|
|
645
684
|
return self.table_data
|
|
646
685
|
|
|
647
686
|
def evaluate_column_types(self) -> None:
|
|
687
|
+
"""Scan the file and populate ``table_data`` with inferred column types."""
|
|
648
688
|
if not self.lineformat_set:
|
|
649
689
|
self.evaluate_line_format()
|
|
650
690
|
inf = self.reader()
|
|
@@ -652,6 +692,7 @@ class CsvFile(EncodedFile):
|
|
|
652
692
|
self.table_data = DataTable(colnames, inf)
|
|
653
693
|
|
|
654
694
|
def create_table(self, database_type: Any, schemaname: str | None, tablename: str, pretty: bool = False) -> str:
|
|
695
|
+
"""Generate a CREATE TABLE SQL statement for this file's inferred schema."""
|
|
655
696
|
return self.table_data.create_table(database_type, schemaname, tablename, pretty)
|
|
656
697
|
|
|
657
698
|
|
|
@@ -664,6 +705,7 @@ def write_delimited_file(
|
|
|
664
705
|
append: bool = False,
|
|
665
706
|
zipfile: str | None = None,
|
|
666
707
|
) -> None:
|
|
708
|
+
"""Write a query result set to a CSV, TSV, or other delimited text file."""
|
|
667
709
|
delim = None
|
|
668
710
|
quote = None
|
|
669
711
|
escchar = None
|
|
@@ -700,18 +742,21 @@ def write_delimited_file(
|
|
|
700
742
|
filewriter_close(outfile)
|
|
701
743
|
ofile = EncodedFile(outfile, file_encoding).open(mode=fmode)
|
|
702
744
|
fdesc = outfile
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
for rec in rowsource:
|
|
707
|
-
try:
|
|
708
|
-
datarow = line_delimiter.delimited(rec)
|
|
745
|
+
try:
|
|
746
|
+
if not (filefmt.lower() == "plain" or (append and zipfile is None)):
|
|
747
|
+
datarow = line_delimiter.delimited(column_headers)
|
|
709
748
|
ofile.write(datarow)
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
749
|
+
for rec in rowsource:
|
|
750
|
+
try:
|
|
751
|
+
datarow = line_delimiter.delimited(rec)
|
|
752
|
+
ofile.write(datarow)
|
|
753
|
+
except ErrInfo:
|
|
754
|
+
raise
|
|
755
|
+
except Exception as e:
|
|
756
|
+
raise ErrInfo(
|
|
757
|
+
"exception",
|
|
758
|
+
exception_msg=exception_desc(),
|
|
759
|
+
other_msg=f"Can't write output to file {fdesc}.",
|
|
760
|
+
) from e
|
|
761
|
+
finally:
|
|
762
|
+
ofile.close()
|
execsql/exporters/duckdb.py
CHANGED
|
@@ -15,6 +15,8 @@ from typing import Any
|
|
|
15
15
|
from execsql.exceptions import ErrInfo
|
|
16
16
|
from execsql.types import dbt_duckdb
|
|
17
17
|
|
|
18
|
+
__all__ = ["export_duckdb", "write_query_to_duckdb"]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
def export_duckdb(
|
|
20
22
|
outfile: str,
|
|
@@ -23,6 +25,7 @@ def export_duckdb(
|
|
|
23
25
|
append: bool,
|
|
24
26
|
tablename: str,
|
|
25
27
|
) -> None:
|
|
28
|
+
"""Write pre-fetched rows to a named table in a DuckDB database file."""
|
|
26
29
|
try:
|
|
27
30
|
import duckdb
|
|
28
31
|
except Exception:
|
|
@@ -79,12 +82,13 @@ def write_query_to_duckdb(
|
|
|
79
82
|
append: bool,
|
|
80
83
|
tablename: str,
|
|
81
84
|
) -> None:
|
|
85
|
+
"""Execute a SELECT and write the result set to a named table in a DuckDB database."""
|
|
82
86
|
from execsql.utils.errors import exception_desc
|
|
83
87
|
|
|
84
88
|
try:
|
|
85
89
|
hdrs, rows = db.select_rowsource(select_stmt)
|
|
86
90
|
except ErrInfo:
|
|
87
91
|
raise
|
|
88
|
-
except Exception:
|
|
89
|
-
raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
|
|
92
|
+
except Exception as e:
|
|
93
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
90
94
|
export_duckdb(outfile, hdrs, rows, append, tablename)
|
execsql/exporters/feather.py
CHANGED
|
@@ -18,16 +18,19 @@ from execsql.types import DT_Boolean, DT_Date, DT_Timestamp, DT_TimestampTZ
|
|
|
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_feather", "write_query_to_hdf5"]
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
def write_query_to_feather(outfile: str, headers: list[str], rows: Any) -> None:
|
|
25
|
+
"""Write a row source as an Apache Arrow Feather v2 file using polars."""
|
|
23
26
|
try:
|
|
24
27
|
import polars as pl
|
|
25
|
-
except ImportError:
|
|
28
|
+
except ImportError as e:
|
|
26
29
|
raise ErrInfo(
|
|
27
30
|
"exception",
|
|
28
31
|
exception_msg=exception_desc(),
|
|
29
32
|
other_msg="The polars Python package must be installed to export data to the feather format.",
|
|
30
|
-
)
|
|
33
|
+
) from e
|
|
31
34
|
rows_list = list(rows)
|
|
32
35
|
if rows_list:
|
|
33
36
|
df = pl.DataFrame(rows_list, schema=headers, orient="row")
|
|
@@ -45,20 +48,21 @@ def write_query_to_hdf5(
|
|
|
45
48
|
append: bool = False,
|
|
46
49
|
desc: str | None = None,
|
|
47
50
|
) -> None:
|
|
51
|
+
"""Execute a SELECT and write the result set to an HDF5 file using the tables library."""
|
|
48
52
|
try:
|
|
49
53
|
import tables
|
|
50
|
-
except ImportError:
|
|
54
|
+
except ImportError as e:
|
|
51
55
|
raise ErrInfo(
|
|
52
56
|
"exception",
|
|
53
57
|
exception_msg=exception_desc(),
|
|
54
58
|
other_msg="The tables Python library must be installed to export data to the HDF5 format.",
|
|
55
|
-
)
|
|
59
|
+
) from e
|
|
56
60
|
try:
|
|
57
61
|
hdrs, rows = db.select_rowsource(select_stmt)
|
|
58
62
|
except ErrInfo:
|
|
59
63
|
raise
|
|
60
|
-
except Exception:
|
|
61
|
-
raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
|
|
64
|
+
except Exception as e:
|
|
65
|
+
raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
|
|
62
66
|
|
|
63
67
|
def h5type(datatype, size):
|
|
64
68
|
if datatype in (_state.DT_Varchar, _state.DT_Text):
|