execsql2 2.12.7__py3-none-any.whl → 2.13.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/html.py +10 -2
- execsql/exporters/raw.py +31 -19
- execsql/exporters/zip.py +21 -3
- execsql/importers/json.py +142 -0
- execsql/metacommands/__init__.py +2 -0
- execsql/metacommands/dispatch.py +12 -0
- execsql/metacommands/io.py +2 -0
- execsql/metacommands/io_import.py +36 -0
- execsql/metacommands/system.py +4 -3
- execsql/utils/fileio.py +8 -2
- execsql/utils/mail.py +19 -3
- {execsql2-2.12.7.dist-info → execsql2-2.13.1.dist-info}/METADATA +4 -4
- {execsql2-2.12.7.dist-info → execsql2-2.13.1.dist-info}/RECORD +32 -31
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.12.7.dist-info → execsql2-2.13.1.dist-info}/WHEEL +0 -0
- {execsql2-2.12.7.dist-info → execsql2-2.13.1.dist-info}/entry_points.txt +0 -0
- {execsql2-2.12.7.dist-info → execsql2-2.13.1.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.12.7.dist-info → execsql2-2.13.1.dist-info}/licenses/NOTICE +0 -0
execsql/exporters/html.py
CHANGED
|
@@ -154,8 +154,16 @@ def export_html(
|
|
|
154
154
|
finally:
|
|
155
155
|
t.close()
|
|
156
156
|
f.close()
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
try:
|
|
158
|
+
os.unlink(outfile)
|
|
159
|
+
os.rename(tempfname, outfile)
|
|
160
|
+
except OSError:
|
|
161
|
+
# Clean up temp file if rename fails.
|
|
162
|
+
try:
|
|
163
|
+
os.unlink(tempfname)
|
|
164
|
+
except OSError:
|
|
165
|
+
pass
|
|
166
|
+
raise
|
|
159
167
|
|
|
160
168
|
|
|
161
169
|
def export_cgi_html(
|
execsql/exporters/raw.py
CHANGED
|
@@ -29,21 +29,30 @@ def write_query_raw(
|
|
|
29
29
|
if zipfile is None:
|
|
30
30
|
filewriter_close(outfile)
|
|
31
31
|
mode = "wb" if not append else "ab"
|
|
32
|
-
|
|
32
|
+
with open(outfile, mode) as of:
|
|
33
|
+
for row in rowsource:
|
|
34
|
+
for col in row:
|
|
35
|
+
if isinstance(col, bytearray):
|
|
36
|
+
of.write(col)
|
|
37
|
+
else:
|
|
38
|
+
if isinstance(col, str):
|
|
39
|
+
of.write(bytes(col, db_encoding))
|
|
40
|
+
else:
|
|
41
|
+
of.write(bytes(str(col), db_encoding))
|
|
33
42
|
else:
|
|
34
43
|
of = ZipWriter(zipfile, outfile, append)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
else:
|
|
41
|
-
if isinstance(col, str):
|
|
42
|
-
of.write(bytes(col, db_encoding))
|
|
44
|
+
try:
|
|
45
|
+
for row in rowsource:
|
|
46
|
+
for col in row:
|
|
47
|
+
if isinstance(col, bytearray):
|
|
48
|
+
of.write(col)
|
|
43
49
|
else:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
if isinstance(col, str):
|
|
51
|
+
of.write(bytes(col, db_encoding))
|
|
52
|
+
else:
|
|
53
|
+
of.write(bytes(str(col), db_encoding))
|
|
54
|
+
finally:
|
|
55
|
+
of.close()
|
|
47
56
|
|
|
48
57
|
|
|
49
58
|
def write_query_b64(outfile: str, rowsource: Any, append: bool = False, zipfile: str | None = None) -> None:
|
|
@@ -51,12 +60,15 @@ def write_query_b64(outfile: str, rowsource: Any, append: bool = False, zipfile:
|
|
|
51
60
|
if zipfile is None:
|
|
52
61
|
filewriter_close(outfile)
|
|
53
62
|
mode = "wb" if not append else "ab"
|
|
54
|
-
|
|
63
|
+
with open(outfile, mode) as of:
|
|
64
|
+
for row in rowsource:
|
|
65
|
+
for col in row:
|
|
66
|
+
of.write(base64.standard_b64decode(col))
|
|
55
67
|
else:
|
|
56
68
|
of = ZipWriter(zipfile, outfile, append)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
try:
|
|
70
|
+
for row in rowsource:
|
|
71
|
+
for col in row:
|
|
72
|
+
of.write(base64.standard_b64decode(col))
|
|
73
|
+
finally:
|
|
74
|
+
of.close()
|
execsql/exporters/zip.py
CHANGED
|
@@ -32,8 +32,18 @@ class WriteableZipfile:
|
|
|
32
32
|
self.zf = zipfile.ZipFile(zipfile_name, mode=zmode, compression=comp, compresslevel=9)
|
|
33
33
|
self.current_handle = None
|
|
34
34
|
|
|
35
|
-
def
|
|
35
|
+
def __enter__(self) -> WriteableZipfile:
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
36
39
|
self.close()
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
def __del__(self) -> None:
|
|
43
|
+
try:
|
|
44
|
+
self.close()
|
|
45
|
+
except Exception:
|
|
46
|
+
pass # Best-effort cleanup at interpreter shutdown.
|
|
37
47
|
|
|
38
48
|
def member_file(self, member_filename: str) -> None:
|
|
39
49
|
"""Create a new member entry in the archive and open it for writing."""
|
|
@@ -97,11 +107,19 @@ class ZipWriter:
|
|
|
97
107
|
self.zwriter = WriteableZipfile(self.zip_fname, append)
|
|
98
108
|
self.member = self.zwriter.member_file(member_fname)
|
|
99
109
|
|
|
110
|
+
def __enter__(self) -> ZipWriter:
|
|
111
|
+
return self
|
|
112
|
+
|
|
113
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
114
|
+
self.close()
|
|
115
|
+
return None
|
|
116
|
+
|
|
100
117
|
def write(self, str_data: str) -> None:
|
|
101
118
|
"""Write a string to the current zip member."""
|
|
102
119
|
self.zwriter.write(str_data)
|
|
103
120
|
|
|
104
121
|
def close(self) -> None:
|
|
105
122
|
"""Close the zip member and finalise the archive."""
|
|
106
|
-
self.zwriter
|
|
107
|
-
|
|
123
|
+
if self.zwriter is not None:
|
|
124
|
+
self.zwriter.close()
|
|
125
|
+
self.zwriter = None
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
JSON import for execsql.
|
|
5
|
+
|
|
6
|
+
Provides :func:`import_json`, used by ``IMPORT … FORMAT json``.
|
|
7
|
+
Supports JSON arrays of objects (``[{…}, …]``) and newline-delimited
|
|
8
|
+
JSON (NDJSON, one object per line). Nested objects are flattened with
|
|
9
|
+
dot-separated keys; nested arrays and non-object values are serialized
|
|
10
|
+
as JSON strings so every column maps to a scalar database value.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from execsql.db.base import Database
|
|
18
|
+
from execsql.exceptions import ErrInfo
|
|
19
|
+
from execsql.importers.base import import_data_table
|
|
20
|
+
|
|
21
|
+
__all__ = ["import_json"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _flatten(obj: Any, prefix: str = "", sep: str = ".") -> dict[str, Any]:
|
|
25
|
+
"""Recursively flatten a nested dict.
|
|
26
|
+
|
|
27
|
+
Nested dicts produce dot-separated keys. All other compound values
|
|
28
|
+
(lists, nested lists-of-dicts) are serialized as JSON strings so the
|
|
29
|
+
result is always ``{str: scalar}``.
|
|
30
|
+
"""
|
|
31
|
+
items: dict[str, Any] = {}
|
|
32
|
+
if isinstance(obj, dict):
|
|
33
|
+
for key, value in obj.items():
|
|
34
|
+
new_key = f"{prefix}{sep}{key}" if prefix else key
|
|
35
|
+
if isinstance(value, dict):
|
|
36
|
+
items.update(_flatten(value, new_key, sep))
|
|
37
|
+
elif isinstance(value, list):
|
|
38
|
+
# Serialize arrays as JSON strings — tables are flat.
|
|
39
|
+
items[new_key] = json.dumps(value, default=str)
|
|
40
|
+
else:
|
|
41
|
+
items[new_key] = value
|
|
42
|
+
return items
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_json_file(filename: str, encoding: str) -> list[dict[str, Any]]:
|
|
46
|
+
"""Read a JSON file and return a list of flat dicts.
|
|
47
|
+
|
|
48
|
+
Accepts either a JSON array of objects or newline-delimited JSON
|
|
49
|
+
(NDJSON).
|
|
50
|
+
"""
|
|
51
|
+
text = Path(filename).read_text(encoding=encoding)
|
|
52
|
+
stripped = text.strip()
|
|
53
|
+
|
|
54
|
+
if stripped.startswith("["):
|
|
55
|
+
# Standard JSON array.
|
|
56
|
+
raw = json.loads(stripped)
|
|
57
|
+
if not isinstance(raw, list):
|
|
58
|
+
raise ErrInfo(type="error", other_msg="JSON file root is not an array of objects.")
|
|
59
|
+
records = raw
|
|
60
|
+
elif stripped.startswith("{"):
|
|
61
|
+
# Try NDJSON (one object per line) or a single object.
|
|
62
|
+
records = []
|
|
63
|
+
for lineno, line in enumerate(stripped.splitlines(), 1):
|
|
64
|
+
line = line.strip()
|
|
65
|
+
if not line:
|
|
66
|
+
continue
|
|
67
|
+
try:
|
|
68
|
+
obj = json.loads(line)
|
|
69
|
+
except json.JSONDecodeError as exc:
|
|
70
|
+
raise ErrInfo(
|
|
71
|
+
type="error",
|
|
72
|
+
other_msg=f"Invalid JSON on line {lineno}: {exc}",
|
|
73
|
+
) from exc
|
|
74
|
+
if not isinstance(obj, dict):
|
|
75
|
+
raise ErrInfo(
|
|
76
|
+
type="error",
|
|
77
|
+
other_msg=f"Line {lineno} is not a JSON object.",
|
|
78
|
+
)
|
|
79
|
+
records.append(obj)
|
|
80
|
+
else:
|
|
81
|
+
raise ErrInfo(
|
|
82
|
+
type="error",
|
|
83
|
+
other_msg="JSON import expects a file starting with '[' (array) or '{' (object/NDJSON).",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if not records:
|
|
87
|
+
raise ErrInfo(type="error", other_msg="JSON file contains no records.")
|
|
88
|
+
|
|
89
|
+
# Validate that all records are dicts.
|
|
90
|
+
for i, rec in enumerate(records):
|
|
91
|
+
if not isinstance(rec, dict):
|
|
92
|
+
raise ErrInfo(
|
|
93
|
+
type="error",
|
|
94
|
+
other_msg=f"Record {i} in JSON file is not an object (got {type(rec).__name__}).",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return [_flatten(rec) for rec in records]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def import_json(
|
|
101
|
+
db: Database,
|
|
102
|
+
schemaname: str | None,
|
|
103
|
+
tablename: str,
|
|
104
|
+
filename: str,
|
|
105
|
+
is_new: Any,
|
|
106
|
+
encoding: str | None = None,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Import a JSON file into a database table.
|
|
109
|
+
|
|
110
|
+
Objects are flattened so that nested keys become dot-separated column
|
|
111
|
+
names (e.g. ``address.city``). Arrays within objects are stored as
|
|
112
|
+
JSON strings.
|
|
113
|
+
"""
|
|
114
|
+
from execsql.utils.errors import exception_desc
|
|
115
|
+
|
|
116
|
+
import execsql.state as _state
|
|
117
|
+
|
|
118
|
+
enc = encoding if encoding else _state.conf.import_encoding
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
flat_records = _parse_json_file(filename, enc)
|
|
122
|
+
except ErrInfo:
|
|
123
|
+
raise
|
|
124
|
+
except Exception as e:
|
|
125
|
+
raise ErrInfo(
|
|
126
|
+
"exception",
|
|
127
|
+
exception_msg=exception_desc(),
|
|
128
|
+
other_msg=f"Can't parse JSON file {filename}",
|
|
129
|
+
) from e
|
|
130
|
+
|
|
131
|
+
# Build a union of all keys across records (preserving first-seen order).
|
|
132
|
+
seen: dict[str, None] = {}
|
|
133
|
+
for rec in flat_records:
|
|
134
|
+
for key in rec:
|
|
135
|
+
if key not in seen:
|
|
136
|
+
seen[key] = None
|
|
137
|
+
hdrs = list(seen)
|
|
138
|
+
|
|
139
|
+
# Build row data aligned to hdrs — missing keys become None.
|
|
140
|
+
data = [[rec.get(h) for h in hdrs] for rec in flat_records]
|
|
141
|
+
|
|
142
|
+
import_data_table(db, schemaname, tablename, is_new, hdrs, data)
|
execsql/metacommands/__init__.py
CHANGED
|
@@ -118,6 +118,7 @@ from execsql.metacommands.io import (
|
|
|
118
118
|
x_import_xls_pattern,
|
|
119
119
|
x_import_parquet,
|
|
120
120
|
x_import_feather,
|
|
121
|
+
x_import_json,
|
|
121
122
|
x_import_row_buffer,
|
|
122
123
|
x_show_progress,
|
|
123
124
|
x_export_row_buffer,
|
|
@@ -325,6 +326,7 @@ __all__ = [
|
|
|
325
326
|
"x_import_xls_pattern",
|
|
326
327
|
"x_import_parquet",
|
|
327
328
|
"x_import_feather",
|
|
329
|
+
"x_import_json",
|
|
328
330
|
"x_import_row_buffer",
|
|
329
331
|
"x_show_progress",
|
|
330
332
|
"x_export_row_buffer",
|
execsql/metacommands/dispatch.py
CHANGED
|
@@ -118,6 +118,7 @@ from execsql.metacommands.io import (
|
|
|
118
118
|
x_import,
|
|
119
119
|
x_import_feather,
|
|
120
120
|
x_import_file,
|
|
121
|
+
x_import_json,
|
|
121
122
|
x_import_ods,
|
|
122
123
|
x_import_ods_pattern,
|
|
123
124
|
x_import_parquet,
|
|
@@ -540,6 +541,17 @@ def build_dispatch_table() -> MetaCommandList:
|
|
|
540
541
|
x_import_feather,
|
|
541
542
|
)
|
|
542
543
|
|
|
544
|
+
# ------------------------------------------------------------------
|
|
545
|
+
# IMPORT JSON
|
|
546
|
+
# ------------------------------------------------------------------
|
|
547
|
+
mcl.add(
|
|
548
|
+
ins_table_rxs(
|
|
549
|
+
r"^\s*IMPORT\s+TO\s+(?:(?P<new>NEW|REPLACEMENT)\s+)?",
|
|
550
|
+
ins_fn_rxs(r"\s+FROM\s+JSON\s+", r"\s*$"),
|
|
551
|
+
),
|
|
552
|
+
x_import_json,
|
|
553
|
+
)
|
|
554
|
+
|
|
543
555
|
# ------------------------------------------------------------------
|
|
544
556
|
# PROMPT ACTION
|
|
545
557
|
# ------------------------------------------------------------------
|
execsql/metacommands/io.py
CHANGED
|
@@ -34,6 +34,7 @@ from execsql.metacommands.io_import import ( # noqa: F401
|
|
|
34
34
|
x_import,
|
|
35
35
|
x_import_feather,
|
|
36
36
|
x_import_file,
|
|
37
|
+
x_import_json,
|
|
37
38
|
x_import_ods,
|
|
38
39
|
x_import_ods_pattern,
|
|
39
40
|
x_import_parquet,
|
|
@@ -88,6 +89,7 @@ __all__ = [
|
|
|
88
89
|
"x_import",
|
|
89
90
|
"x_import_feather",
|
|
90
91
|
"x_import_file",
|
|
92
|
+
"x_import_json",
|
|
91
93
|
"x_import_ods",
|
|
92
94
|
"x_import_ods_pattern",
|
|
93
95
|
"x_import_parquet",
|
|
@@ -14,6 +14,7 @@ import execsql.state as _state
|
|
|
14
14
|
from execsql.exceptions import ErrInfo
|
|
15
15
|
from execsql.importers.csv import importfile, importtable
|
|
16
16
|
from execsql.importers.feather import import_feather, import_parquet
|
|
17
|
+
from execsql.importers.json import import_json
|
|
17
18
|
from execsql.importers.ods import OdsFile, importods
|
|
18
19
|
from execsql.exporters.xls import XlsFile, XlsxFile
|
|
19
20
|
from execsql.importers.xls import importxls
|
|
@@ -388,6 +389,41 @@ def x_import_feather(**kwargs: Any) -> None:
|
|
|
388
389
|
return None
|
|
389
390
|
|
|
390
391
|
|
|
392
|
+
def x_import_json(**kwargs: Any) -> None:
|
|
393
|
+
newstr = kwargs["new"]
|
|
394
|
+
if newstr:
|
|
395
|
+
is_new = 1 + ["new", "replacement"].index(newstr.lower())
|
|
396
|
+
else:
|
|
397
|
+
is_new = 0
|
|
398
|
+
schemaname = kwargs["schema"]
|
|
399
|
+
tablename = kwargs["table"]
|
|
400
|
+
filename = kwargs["filename"]
|
|
401
|
+
if len(filename) > 1 and filename[0] == "~" and filename[1] == os.sep:
|
|
402
|
+
filename = str(Path.home() / filename[2:])
|
|
403
|
+
if not Path(filename).exists():
|
|
404
|
+
raise ErrInfo(
|
|
405
|
+
type="cmd",
|
|
406
|
+
command_text=kwargs["metacommandline"],
|
|
407
|
+
other_msg=f"Input file {filename} does not exist",
|
|
408
|
+
)
|
|
409
|
+
enc = kwargs.get("encoding")
|
|
410
|
+
from execsql.metacommands.conditions import file_size_date
|
|
411
|
+
|
|
412
|
+
sz, dt = file_size_date(filename)
|
|
413
|
+
_state.exec_log.log_status_info(f"IMPORTing from JSON file {filename} ({sz}, {dt})")
|
|
414
|
+
try:
|
|
415
|
+
import_json(_state.dbs.current(), schemaname, tablename, filename, is_new, encoding=enc)
|
|
416
|
+
except ErrInfo:
|
|
417
|
+
raise
|
|
418
|
+
except Exception as e:
|
|
419
|
+
raise ErrInfo(
|
|
420
|
+
"exception",
|
|
421
|
+
exception_msg=exception_desc(),
|
|
422
|
+
other_msg=f"Can't import data from JSON file {filename}",
|
|
423
|
+
) from e
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
|
|
391
427
|
def x_import_row_buffer(**kwargs: Any) -> None:
|
|
392
428
|
rows = kwargs["rows"]
|
|
393
429
|
_state.conf.import_row_buffer = int(rows)
|
execsql/metacommands/system.py
CHANGED
|
@@ -51,7 +51,8 @@ def x_system_cmd(**kwargs: Any) -> None:
|
|
|
51
51
|
returncode = subprocess.call(cmdargs)
|
|
52
52
|
_state.subvars.add_substitution("$SYSTEM_CMD_EXIT_STATUS", str(returncode))
|
|
53
53
|
else:
|
|
54
|
-
subprocess.Popen(cmdargs)
|
|
54
|
+
proc = subprocess.Popen(cmdargs)
|
|
55
|
+
_state.subvars.add_substitution("$SYSTEM_CMD_PID", str(proc.pid))
|
|
55
56
|
return None
|
|
56
57
|
|
|
57
58
|
|
|
@@ -62,8 +63,8 @@ def x_email(**kwargs: Any) -> None:
|
|
|
62
63
|
msg = kwargs["msg"]
|
|
63
64
|
msg_file = kwargs["msg_file"]
|
|
64
65
|
att_file = kwargs["att_file"]
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
with Mailer() as m:
|
|
67
|
+
m.sendmail(from_addr, to_addr, subject, msg, msg_file, att_file)
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
def x_timer(**kwargs: Any) -> None:
|
execsql/utils/fileio.py
CHANGED
|
@@ -119,7 +119,10 @@ class FileWriter(multiprocessing.Process):
|
|
|
119
119
|
self.close_after_write = False
|
|
120
120
|
|
|
121
121
|
def __del__(self) -> None:
|
|
122
|
-
|
|
122
|
+
try:
|
|
123
|
+
self.close()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass # Best-effort cleanup at interpreter shutdown.
|
|
123
126
|
|
|
124
127
|
def write_queue(self) -> None:
|
|
125
128
|
while len(self.output_queue) > 0:
|
|
@@ -215,7 +218,10 @@ class FileWriter(multiprocessing.Process):
|
|
|
215
218
|
)
|
|
216
219
|
|
|
217
220
|
def __del__(self) -> None:
|
|
218
|
-
|
|
221
|
+
try:
|
|
222
|
+
self.close_all()
|
|
223
|
+
except Exception:
|
|
224
|
+
pass # Best-effort cleanup at interpreter shutdown.
|
|
219
225
|
|
|
220
226
|
def close_all(self) -> None:
|
|
221
227
|
for fc in getattr(self, "files", {}).values():
|
execsql/utils/mail.py
CHANGED
|
@@ -28,9 +28,24 @@ class Mailer:
|
|
|
28
28
|
def __repr__(self) -> str:
|
|
29
29
|
return "Mailer()"
|
|
30
30
|
|
|
31
|
-
def
|
|
31
|
+
def __enter__(self) -> Mailer:
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
35
|
+
self.close()
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def close(self) -> None:
|
|
32
39
|
if hasattr(self, "smtpconn"):
|
|
33
|
-
|
|
40
|
+
try:
|
|
41
|
+
self.smtpconn.quit()
|
|
42
|
+
except Exception:
|
|
43
|
+
pass # Best-effort; connection may already be closed.
|
|
44
|
+
finally:
|
|
45
|
+
del self.smtpconn
|
|
46
|
+
|
|
47
|
+
def __del__(self) -> None:
|
|
48
|
+
self.close()
|
|
34
49
|
|
|
35
50
|
def __init__(self) -> None:
|
|
36
51
|
conf = _state.conf
|
|
@@ -134,5 +149,6 @@ class MailSpec:
|
|
|
134
149
|
content_filename = _state.subvars.substitute_all(content_filename)
|
|
135
150
|
attach_filename = _state.commandliststack[-1].localvars.substitute_all(self.attach_filename)
|
|
136
151
|
attach_filename = _state.subvars.substitute_all(attach_filename)
|
|
137
|
-
Mailer()
|
|
152
|
+
with Mailer() as m:
|
|
153
|
+
m.sendmail(send_from, send_to, subject, msg_content, content_filename, attach_filename)
|
|
138
154
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.13.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: Homepage, https://execsql2.readthedocs.io
|
|
6
6
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
@@ -51,7 +51,7 @@ Requires-Dist: keyring; extra == 'all'
|
|
|
51
51
|
Requires-Dist: odfpy; extra == 'all'
|
|
52
52
|
Requires-Dist: openpyxl; extra == 'all'
|
|
53
53
|
Requires-Dist: oracledb; extra == 'all'
|
|
54
|
-
Requires-Dist: pg-upsert>=1.
|
|
54
|
+
Requires-Dist: pg-upsert>=1.20.0; extra == 'all'
|
|
55
55
|
Requires-Dist: polars; extra == 'all'
|
|
56
56
|
Requires-Dist: psycopg2-binary; extra == 'all'
|
|
57
57
|
Requires-Dist: pymysql; extra == 'all'
|
|
@@ -109,7 +109,7 @@ Requires-Dist: oracledb; extra == 'oracle'
|
|
|
109
109
|
Provides-Extra: postgres
|
|
110
110
|
Requires-Dist: psycopg2-binary; extra == 'postgres'
|
|
111
111
|
Provides-Extra: upsert
|
|
112
|
-
Requires-Dist: pg-upsert>=1.
|
|
112
|
+
Requires-Dist: pg-upsert>=1.20.0; extra == 'upsert'
|
|
113
113
|
Description-Content-Type: text/markdown
|
|
114
114
|
|
|
115
115
|
> [!NOTE]
|
|
@@ -238,7 +238,7 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
|
|
|
238
238
|
|
|
239
239
|
# Features
|
|
240
240
|
|
|
241
|
-
- Import data from CSV, TSV, Excel, OpenDocument, Feather, or Parquet files into a database table.
|
|
241
|
+
- Import data from CSV, TSV, JSON, Excel, OpenDocument, Feather, or Parquet files into a database table.
|
|
242
242
|
- 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.
|
|
243
243
|
- Copy data between databases, including across different DBMS types.
|
|
244
244
|
- Conditionally execute SQL and metacommands using `IF`/`ELSE`/`ENDIF` based on data values, DBMS type, or user input.
|
|
@@ -33,7 +33,7 @@ execsql/exporters/base.py,sha256=W9USFyk_2eztjJ51X6CJh7-chE1i3cSx-STOtbHXCNI,637
|
|
|
33
33
|
execsql/exporters/delimited.py,sha256=URvEQo1IRF_tfdVHL3uBwEonihC-XfDm0f1argQPf4M,32088
|
|
34
34
|
execsql/exporters/duckdb.py,sha256=Wc9I5uiV4MzmVQzCX-vgVHQUL7U3ZWdOkFVFWBv5SXM,2911
|
|
35
35
|
execsql/exporters/feather.py,sha256=w2qZAnewzeiRMnmPXECvkgD-6KtyxaiQwjokRT7Awrc,4167
|
|
36
|
-
execsql/exporters/html.py,sha256=
|
|
36
|
+
execsql/exporters/html.py,sha256=2FhC1pe60w7PYkFfesY3YrbULcBED_7QdXceRDuwzH8,9583
|
|
37
37
|
execsql/exporters/json.py,sha256=yljlRBbmvDVSTQUe0EdfdqTTRpD5sHfn7-jQ457ydvc,4139
|
|
38
38
|
execsql/exporters/latex.py,sha256=w_B83_5vKPe8uYxCWGdqvxwJeq0mw5zzKYDiAb7dbN0,4503
|
|
39
39
|
execsql/exporters/markdown.py,sha256=_ZX3dikbtAb6qZxYeWxDZAPF0-cNKTPR7or5kTbD2ZU,4436
|
|
@@ -41,7 +41,7 @@ execsql/exporters/ods.py,sha256=jl2qVHUeCLLv8xrkZfG3jgXbaglQ3rggCHziv7tNQOI,1887
|
|
|
41
41
|
execsql/exporters/parquet.py,sha256=186vUTH1oVAQ0s_qayLzEQVsKKu3ijAkhYEI6tysXkg,1095
|
|
42
42
|
execsql/exporters/pretty.py,sha256=9isA8f6xUz-3-JhMJimibnvtybVrT1cnoAjGnzsPEGI,3423
|
|
43
43
|
execsql/exporters/protocol.py,sha256=BxATgz0xKHbB2FpZBeNg7wZfIiCohhD1awlr3JCec0c,4372
|
|
44
|
-
execsql/exporters/raw.py,sha256=
|
|
44
|
+
execsql/exporters/raw.py,sha256=hqO5XEv_Ab9KiphPvZ9sZEdxWTg-kyn8PH9V3qFksyo,2488
|
|
45
45
|
execsql/exporters/sqlite.py,sha256=XA0ALLvy-r6Pz1lpOFkWWbvpSP9Hm1tHHiuo_BvPVDk,2686
|
|
46
46
|
execsql/exporters/templates.py,sha256=T9nk7vJrlxiPGfOWGc79xqqDxK3TCYu0wXq48U02npw,5564
|
|
47
47
|
execsql/exporters/values.py,sha256=HIyud31aux_dbCphfKHEGeZB9fkIPE5PoGXQz817XIE,2520
|
|
@@ -49,7 +49,7 @@ execsql/exporters/xls.py,sha256=nPROgxL8XK2oiBVoqN2L-o0j_jynRIMokwB8NpvOBt0,1062
|
|
|
49
49
|
execsql/exporters/xlsx.py,sha256=Gm8ns_KeqSMu2DONSSJ1DcwPBEjYwpbU7frmX0g5L7c,11487
|
|
50
50
|
execsql/exporters/xml.py,sha256=lqcOM8uKDoCayU42BPSLNH1_2DIHU5D3LtQItREU90c,2564
|
|
51
51
|
execsql/exporters/yaml.py,sha256=1Vuc6uMDuLTkCuXCfXWKz4gLkkAVdEXkLs4gEB_67Xo,3110
|
|
52
|
-
execsql/exporters/zip.py,sha256=
|
|
52
|
+
execsql/exporters/zip.py,sha256=kr0X6VLE_ULGVQtMzfqZZSUmc1Qupxg9HbMOAvnTQvI,4890
|
|
53
53
|
execsql/gui/__init__.py,sha256=oCb-cyhLZzVpWJ4WU5HbqEDBrV-lm0ytEwxemrOZyqs,2048
|
|
54
54
|
execsql/gui/base.py,sha256=sfNRkDrf7FhIgMRUOdyZpRLS1Xk9RqNhrV0A1RP6PXU,6068
|
|
55
55
|
execsql/gui/console.py,sha256=TuGzm7XFxa8iWrofGLwx5DuVIlr3wqqyP_pSnAJ1S3s,14271
|
|
@@ -59,23 +59,24 @@ execsql/importers/__init__.py,sha256=dDsxSVeQYXBvm9yGqN3QswyGbLWTwt08pvUuRJgZhl4
|
|
|
59
59
|
execsql/importers/base.py,sha256=FGVz3ntN6xHL99rQixlQj3tAf570K_oU83UtbYE1lJg,4124
|
|
60
60
|
execsql/importers/csv.py,sha256=Mu848WNzuhVO1ade-WurPyxqGOuVNRO8UwRF3-bav_I,4845
|
|
61
61
|
execsql/importers/feather.py,sha256=g2B69d2uv9vmnXcmjFyTVsMP40LYEzFYkhk3gD26mGw,1900
|
|
62
|
+
execsql/importers/json.py,sha256=Z7QJZQ9fyqaxFxjfqcfZoaoW2GSZt-DMqW5LZiEfyNs,4684
|
|
62
63
|
execsql/importers/ods.py,sha256=MJsdsjropzCvxAA3DDZfAL_AnmZ4yij7DnrjGyDJqHQ,2843
|
|
63
64
|
execsql/importers/xls.py,sha256=e0Zfe47ZiCpA1Ae3XDJ1ko3sCiH3-8U6XLKi6NvD0jQ,3683
|
|
64
|
-
execsql/metacommands/__init__.py,sha256=
|
|
65
|
+
execsql/metacommands/__init__.py,sha256=3Kz-VasFS9B-C-UdHOjr3RMXjheMeYHe6qYBwp5e7wE,11434
|
|
65
66
|
execsql/metacommands/conditions.py,sha256=Fzrk83-pWbFOoKahYdQW7CZjQeh3zByDUbfgpTM_bjQ,29259
|
|
66
67
|
execsql/metacommands/connect.py,sha256=Nsm0D91i3RX-R2rzQQ-Br-gULaI6Uvdn9fqb7DOAVfE,14804
|
|
67
68
|
execsql/metacommands/control.py,sha256=PAZFK1ck5SDSm5QdFV1ctif3KpEiyYWIXdDceRWgQ6k,8513
|
|
68
69
|
execsql/metacommands/data.py,sha256=tRQBGTAuW-eJ2tBNWaoZI9OjTyNNyHJISo7gOdL-sm8,11370
|
|
69
70
|
execsql/metacommands/debug.py,sha256=pnT24dfvfOx8xFu86mO5czfVCGKbcvgBLyXnqaMWO4w,8184
|
|
70
|
-
execsql/metacommands/dispatch.py,sha256=
|
|
71
|
-
execsql/metacommands/io.py,sha256=
|
|
71
|
+
execsql/metacommands/dispatch.py,sha256=LYmTuSfC0nFKgAPWRK8n8Qf8sbrRtRmbh6ntosOjQEo,85587
|
|
72
|
+
execsql/metacommands/io.py,sha256=vlGBje5sgnqeilooMdhJDgSRIhysHy5_7LrKtik9Xjs,3011
|
|
72
73
|
execsql/metacommands/io_export.py,sha256=7lkCSnPhXy9FVau9_hT1u68NOVdG2DsWmvUh9hM1QWI,18359
|
|
73
74
|
execsql/metacommands/io_fileops.py,sha256=RrcJTh_cgj7bJ-bezjo0yNl-fN3CoWV-aZ71z1KHYZs,9803
|
|
74
|
-
execsql/metacommands/io_import.py,sha256=
|
|
75
|
+
execsql/metacommands/io_import.py,sha256=fzShl_m74DWt2_SmxlxWQe00H53hDIrOYZtvvqKBhSM,14231
|
|
75
76
|
execsql/metacommands/io_write.py,sha256=NpL2aYGfBpbqmPpYsqniYltYfd_SCA1EQz3_4qSdNbo,8279
|
|
76
77
|
execsql/metacommands/prompt.py,sha256=xd3mAkdbn4AE4hUPuSfN5DgZGUZmk-Db23iL-JJPwXs,36918
|
|
77
78
|
execsql/metacommands/script_ext.py,sha256=TUgAldB2LSJAwZrCvDDi804hQ1d9BDQD2GDqHNPVOcM,2280
|
|
78
|
-
execsql/metacommands/system.py,sha256=
|
|
79
|
+
execsql/metacommands/system.py,sha256=AeyxbMLZVOi2nThbHiZ2F_UAIrZ1r4kjI3G80TtnSD4,7385
|
|
79
80
|
execsql/metacommands/upsert.py,sha256=P2935aQHDZPiVwnXi0fGQ7Guxrm-Sy_YunyuSqVSegI,15880
|
|
80
81
|
execsql/script/__init__.py,sha256=HbVQmQEVn4gBtzwy5_nlbDGuRnbWd4dI4nG-q1KyBxs,3498
|
|
81
82
|
execsql/script/control.py,sha256=s-1eZdGARM6H1FwZ6VDdO_f50j7bvvRtTHesfUm9tbc,6144
|
|
@@ -86,31 +87,31 @@ execsql/utils/auth.py,sha256=onXzNkNZQZxGC5w7eey06sjvAIAX_Lf9g7nUJtcsel0,7009
|
|
|
86
87
|
execsql/utils/crypto.py,sha256=2OnBWwn9bCBGc1ZkyRv16TvhottoCNYtXqgbE3mG3Sg,2960
|
|
87
88
|
execsql/utils/datetime.py,sha256=V_itd5vVvUPjT86P_z_hh4mlerMDGhDzI5MwPMDBaI4,7715
|
|
88
89
|
execsql/utils/errors.py,sha256=YKhYD27-3timuZavc2vIrRIfHa71vzih-KVPsAKgvkU,8163
|
|
89
|
-
execsql/utils/fileio.py,sha256=
|
|
90
|
+
execsql/utils/fileio.py,sha256=RGaMUe-e0xIw32OeprNJCezh0Jr9gQimQNqOXBEaJ2M,24338
|
|
90
91
|
execsql/utils/gui.py,sha256=UFtwrXPNqNvZCJFCbumZ1aG2d9B-vyaJXIG83fDHteo,18409
|
|
91
|
-
execsql/utils/mail.py,sha256=
|
|
92
|
+
execsql/utils/mail.py,sha256=Sd7vWj-dz3w0XDSFU9PM8gmy41pojk-Vsgbfven2DMk,5786
|
|
92
93
|
execsql/utils/numeric.py,sha256=xh02ANSRk3nUpQ-rtm66ILoMqoi7HtzCoRMIOT9U8QI,1570
|
|
93
94
|
execsql/utils/regex.py,sha256=diEzTZqU_HHwVMadPAvN1Vgzhl7I03eVaEFGCXyGGL8,3770
|
|
94
95
|
execsql/utils/strings.py,sha256=5Dvzrk-9SIw2lpxXZQkiJbNyo1sy7iXXAtSULlZ0KG8,8488
|
|
95
96
|
execsql/utils/timer.py,sha256=eDYf5VzCNFk7oo90InJucUm3XcBdhYMogjZMqeg9xzc,1899
|
|
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.
|
|
110
|
-
execsql2-2.
|
|
111
|
-
execsql2-2.
|
|
112
|
-
execsql2-2.
|
|
113
|
-
execsql2-2.
|
|
114
|
-
execsql2-2.
|
|
115
|
-
execsql2-2.
|
|
116
|
-
execsql2-2.
|
|
97
|
+
execsql2-2.13.1.data/data/execsql2_extras/README.md,sha256=sxwVyU0ZahCfANv56LahkyuM505kFjrMhe-1SvWE69E,4845
|
|
98
|
+
execsql2-2.13.1.data/data/execsql2_extras/config_settings.sqlite,sha256=aY5cxR7Q7J6zJ4bC9lu5mHUrhy211Cq3MNKPQVCt02E,20480
|
|
99
|
+
execsql2-2.13.1.data/data/execsql2_extras/example_config_prompt.sql,sha256=SY3Jxn1qcVm4kPW9xmmTfknHfvURXmeEYTbRjYrjGSw,7487
|
|
100
|
+
execsql2-2.13.1.data/data/execsql2_extras/execsql.conf,sha256=_45iJ-KWZnB8uMW_gEg067MM5pmGJ-dVl7VbAZMunAE,9530
|
|
101
|
+
execsql2-2.13.1.data/data/execsql2_extras/make_config_db.sql,sha256=WwyC6dK-Eh5CAVppiBCDHqiI1_wEI9U95Ytpr4lsZkg,8726
|
|
102
|
+
execsql2-2.13.1.data/data/execsql2_extras/md_compare.sql,sha256=B8Wd7LZ0vnMY2qvA139JIEBkPObgRH2i5xj6PejTQt8,24092
|
|
103
|
+
execsql2-2.13.1.data/data/execsql2_extras/md_glossary.sql,sha256=DJRHcU5NbFpxTTX-IwH3yRlsboj1q6BBGrUAHKn4Cuo,10796
|
|
104
|
+
execsql2-2.13.1.data/data/execsql2_extras/md_upsert.sql,sha256=v_7GbWh_N1mBTmw3gvTrkagOVp2q0KmXvM8hE-DlFxY,112524
|
|
105
|
+
execsql2-2.13.1.data/data/execsql2_extras/pg_compare.sql,sha256=9dWa8hnfy5dVJI-z2iGpd9JzQmI4j2ziMlEdpnr66ro,24352
|
|
106
|
+
execsql2-2.13.1.data/data/execsql2_extras/pg_glossary.sql,sha256=pKjIIDsROAgJq2H-1qNEcRMAWManivcZ_AEVHzUUlic,9908
|
|
107
|
+
execsql2-2.13.1.data/data/execsql2_extras/pg_upsert.sql,sha256=k7AFiGTLBy3nf-qO5QIaZrEYTAKvdxxU3JDLx9jqkzs,108315
|
|
108
|
+
execsql2-2.13.1.data/data/execsql2_extras/script_template.sql,sha256=1Estacb_vm1FgK41k_G9nuduP1yiA-fQ1Kn4Z4mv5Ao,11153
|
|
109
|
+
execsql2-2.13.1.data/data/execsql2_extras/ss_compare.sql,sha256=TsVxWm3cEpR5-EiMYXNhtaY0arSNeKZhsJdHdLA7xeI,24833
|
|
110
|
+
execsql2-2.13.1.data/data/execsql2_extras/ss_glossary.sql,sha256=cLM7nN8JOIu9ZVP9oY9qdSK3hrnWJiDcX6nZmQQbQWI,13065
|
|
111
|
+
execsql2-2.13.1.data/data/execsql2_extras/ss_upsert.sql,sha256=BCqmBykXBF-BpCgOFeG1qhf2XfScKsxPD17wd1hYfHw,120647
|
|
112
|
+
execsql2-2.13.1.dist-info/METADATA,sha256=l1roY9QFNY72_7eiyYogsqDXyxAcjeE_oo18sL9axHo,17566
|
|
113
|
+
execsql2-2.13.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
114
|
+
execsql2-2.13.1.dist-info/entry_points.txt,sha256=sUOxkM-dN1eBGGpSpDLsAaE0yNXYQKWZAfxPOlMkQyk,90
|
|
115
|
+
execsql2-2.13.1.dist-info/licenses/LICENSE.txt,sha256=LBdhuxejF8_bLCHZ2kWfmDXpDGUu914Gbd6_3JjCRe0,676
|
|
116
|
+
execsql2-2.13.1.dist-info/licenses/NOTICE,sha256=sqVrM73Ys9zfvWC_P797lHfTnoPW_ETeBSrUTFaob0A,339
|
|
117
|
+
execsql2-2.13.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
{execsql2-2.12.7.data → execsql2-2.13.1.data}/data/execsql2_extras/example_config_prompt.sql
RENAMED
|
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
|