execsql2 2.1.2__py3-none-any.whl → 2.2.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.
Files changed (75) hide show
  1. execsql/cli/__init__.py +436 -0
  2. execsql/cli/dsn.py +86 -0
  3. execsql/cli/help.py +140 -0
  4. execsql/{cli.py → cli/run.py} +14 -589
  5. execsql/config.py +13 -1
  6. execsql/db/access.py +16 -12
  7. execsql/db/base.py +158 -90
  8. execsql/db/dsn.py +6 -5
  9. execsql/db/duckdb.py +2 -2
  10. execsql/db/firebird.py +23 -19
  11. execsql/db/mysql.py +8 -7
  12. execsql/db/oracle.py +11 -11
  13. execsql/db/postgres.py +28 -16
  14. execsql/db/sqlite.py +12 -11
  15. execsql/db/sqlserver.py +5 -3
  16. execsql/exceptions.py +7 -7
  17. execsql/exporters/base.py +6 -1
  18. execsql/exporters/delimited.py +44 -35
  19. execsql/exporters/duckdb.py +2 -2
  20. execsql/exporters/feather.py +6 -6
  21. execsql/exporters/html.py +83 -69
  22. execsql/exporters/json.py +50 -42
  23. execsql/exporters/latex.py +33 -27
  24. execsql/exporters/ods.py +4 -4
  25. execsql/exporters/parquet.py +2 -2
  26. execsql/exporters/pretty.py +11 -9
  27. execsql/exporters/raw.py +17 -13
  28. execsql/exporters/sqlite.py +2 -2
  29. execsql/exporters/templates.py +23 -15
  30. execsql/exporters/values.py +22 -20
  31. execsql/exporters/xls.py +4 -4
  32. execsql/exporters/xml.py +28 -13
  33. execsql/importers/base.py +4 -4
  34. execsql/importers/csv.py +6 -6
  35. execsql/importers/feather.py +4 -4
  36. execsql/importers/ods.py +4 -4
  37. execsql/importers/xls.py +4 -4
  38. execsql/metacommands/__init__.py +518 -67
  39. execsql/metacommands/conditions.py +101 -27
  40. execsql/metacommands/control.py +8 -4
  41. execsql/metacommands/data.py +6 -6
  42. execsql/metacommands/debug.py +6 -2
  43. execsql/metacommands/io.py +67 -1310
  44. execsql/metacommands/io_export.py +442 -0
  45. execsql/metacommands/io_fileops.py +287 -0
  46. execsql/metacommands/io_import.py +398 -0
  47. execsql/metacommands/io_write.py +248 -0
  48. execsql/metacommands/prompt.py +22 -66
  49. execsql/metacommands/system.py +7 -2
  50. execsql/py.typed +0 -0
  51. execsql/script.py +49 -5
  52. execsql/types.py +20 -20
  53. execsql/utils/fileio.py +15 -8
  54. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/METADATA +6 -6
  55. execsql2-2.2.1.dist-info/RECORD +104 -0
  56. execsql2-2.1.2.dist-info/RECORD +0 -96
  57. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/READ_ME.rst +0 -0
  58. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  59. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  60. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/execsql.conf +0 -0
  61. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/make_config_db.sql +0 -0
  62. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_compare.sql +0 -0
  63. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_glossary.sql +0 -0
  64. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/md_upsert.sql +0 -0
  65. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_compare.sql +0 -0
  66. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  67. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  68. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/script_template.sql +0 -0
  69. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_compare.sql +0 -0
  70. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  71. {execsql2-2.1.2.data → execsql2-2.2.1.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  72. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/WHEEL +0 -0
  73. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/entry_points.txt +0 -0
  74. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/licenses/LICENSE.txt +0 -0
  75. {execsql2-2.1.2.dist-info → execsql2-2.2.1.dist-info}/licenses/NOTICE +0 -0
@@ -40,24 +40,26 @@ def export_values(
40
40
  f = ef.open("wt")
41
41
  else:
42
42
  f = ZipWriter(zipfile, outfile, append)
43
- if desc is not None:
44
- f.write(f"-- {desc}\n")
45
- f.write(f"INSERT INTO !!target_table!!\n ({', '.join(hdrs)})\n")
46
- f.write("VALUES\n")
47
- firstrow = True
48
- for r in rows:
49
- if firstrow:
50
- firstrow = False
51
- else:
52
- f.write(",\n")
53
- quoted_row = [
54
- f"'{v.replace(chr(39), chr(39) * 2)}'" if isinstance(v, str) else str(v) if v is not None else "NULL"
55
- for v in r
56
- ]
57
- f.write(f" ({', '.join(quoted_row)})")
58
- f.write("\n ;\n")
59
- if outfile.lower() != "stdout":
60
- f.close()
43
+ try:
44
+ if desc is not None:
45
+ f.write(f"-- {desc}\n")
46
+ f.write(f"INSERT INTO !!target_table!!\n ({', '.join(hdrs)})\n")
47
+ f.write("VALUES\n")
48
+ firstrow = True
49
+ for r in rows:
50
+ if firstrow:
51
+ firstrow = False
52
+ else:
53
+ f.write(",\n")
54
+ quoted_row = [
55
+ f"'{v.replace(chr(39), chr(39) * 2)}'" if isinstance(v, str) else str(v) if v is not None else "NULL"
56
+ for v in r
57
+ ]
58
+ f.write(f" ({', '.join(quoted_row)})")
59
+ f.write("\n ;\n")
60
+ finally:
61
+ if outfile.lower() != "stdout":
62
+ f.close()
61
63
 
62
64
 
63
65
  def write_query_to_values(
@@ -72,6 +74,6 @@ def write_query_to_values(
72
74
  hdrs, rows = db.select_rowsource(select_stmt)
73
75
  except ErrInfo:
74
76
  raise
75
- except Exception:
76
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
77
+ except Exception as e:
78
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
77
79
  export_values(outfile, hdrs, rows, append, desc, zipfile=zipfile)
execsql/exporters/xls.py CHANGED
@@ -76,8 +76,8 @@ class XlsFile:
76
76
  def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) -> list:
77
77
  try:
78
78
  sheet = self.sheet_named(sheetname)
79
- except Exception:
80
- raise XlsFileError(f"There is no Excel worksheet named {sheetname} in {self.filename}.")
79
+ except Exception as e:
80
+ raise XlsFileError(f"There is no Excel worksheet named {sheetname} in {self.filename}.") from e
81
81
 
82
82
  # Don't rely on sheet.ncols and sheet.nrows, because Excel will count columns
83
83
  # and rows that have ever been filled, even if they are now empty. Base the column count
@@ -211,8 +211,8 @@ class XlsxFile:
211
211
  def sheet_data(self, sheetname: Any, junk_header_rows: int = 0) -> list:
212
212
  try:
213
213
  sheet = self.sheet_named(sheetname)
214
- except Exception:
215
- raise XlsxFileError(f"There is no Excel worksheet named {sheetname} in {self.filename}.")
214
+ except Exception as e:
215
+ raise XlsxFileError(f"There is no Excel worksheet named {sheetname} in {self.filename}.") from e
216
216
  # Don't rely on sheet.max_column and sheet.max_row, because Excel will count columns
217
217
  # and rows that have ever been filled, even if they are now empty. Base the column count
218
218
  # on the number of contiguous non-empty cells in the first row, and process the data up to nrows until
execsql/exporters/xml.py CHANGED
@@ -8,7 +8,9 @@ to a well-formed XML file with one element per row and column values as
8
8
  child elements or attributes.
9
9
  """
10
10
 
11
+ import re
11
12
  from typing import Any
13
+ from xml.sax.saxutils import escape as xml_escape
12
14
 
13
15
  import execsql.state as _state
14
16
  from execsql.exporters.zip import ZipWriter
@@ -31,8 +33,8 @@ def write_query_to_xml(
31
33
  hdrs, rows = db.select_rowsource(select_stmt)
32
34
  except ErrInfo:
33
35
  raise
34
- except Exception:
35
- raise ErrInfo("db", select_stmt, exception_msg=exception_desc())
36
+ except Exception as e:
37
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
36
38
  if zipfile is None:
37
39
  filewriter_close(outfile)
38
40
  from execsql.utils.fileio import EncodedFile
@@ -47,14 +49,27 @@ def write_query_to_xml(
47
49
  else:
48
50
  f = ZipWriter(zipfile, outfile, append)
49
51
  f.write(f"<?xml version='1.0' encoding='{conf.output_encoding}'?>\n")
50
- if desc is not None:
51
- f.write(f"<!--{desc}-->\n")
52
- f.write(f"<{tablename}>\n")
53
- str_hdrs = [str(h) for h in hdrs]
54
- for row in rows:
55
- f.write(" <row>\n")
56
- for i, col in enumerate(str_hdrs):
57
- f.write(f" <{col}>{row[i]}</{col}>\n")
58
- f.write(" </row>\n")
59
- f.write(f"</{tablename}>\n")
60
- f.close()
52
+
53
+ def _safe_xml_name(name: str) -> str:
54
+ """Sanitize a string for use as an XML element name."""
55
+ # Replace characters that are invalid in XML names with underscores.
56
+ s = re.sub(r"[^\w.\-]", "_", str(name))
57
+ # XML names must start with a letter or underscore, not a digit or dot.
58
+ if s and not (s[0].isalpha() or s[0] == "_"):
59
+ s = "_" + s
60
+ return s or "_"
61
+
62
+ try:
63
+ if desc is not None:
64
+ f.write(f"<!--{desc.replace('--', '- -')}-->\n")
65
+ safe_tablename = _safe_xml_name(tablename)
66
+ f.write(f"<{safe_tablename}>\n")
67
+ str_hdrs = [_safe_xml_name(h) for h in hdrs]
68
+ for row in rows:
69
+ f.write(" <row>\n")
70
+ for i, col in enumerate(str_hdrs):
71
+ f.write(f" <{col}>{xml_escape(str(row[i]) if row[i] is not None else '')}</{col}>\n")
72
+ f.write(" </row>\n")
73
+ f.write(f"</{safe_tablename}>\n")
74
+ finally:
75
+ f.close()
execsql/importers/base.py CHANGED
@@ -85,13 +85,13 @@ def import_data_table(
85
85
  # ...except for Firebird.
86
86
  if db.type == dbt_firebird:
87
87
  db.conn.commit()
88
- except Exception:
88
+ except Exception as e:
89
89
  raise ErrInfo(
90
90
  type="db",
91
91
  command_text=sql,
92
92
  exception_msg=exception_info(),
93
93
  other_msg=f"Could not create new table ({tablename}) for IMPORT metacommand",
94
- )
94
+ ) from e
95
95
  table_cols = db.table_columns(tablename, schemaname)
96
96
  if conf.import_common_cols_only:
97
97
  import_cols = [col for col in hdrs if col.lower() in [tc.lower() for tc in table_cols]]
@@ -108,5 +108,5 @@ def import_data_table(
108
108
  db.commit()
109
109
  except ErrInfo:
110
110
  raise
111
- except Exception:
112
- raise ErrInfo("db", "Call to populate_table when importing data", exception_msg=exception_info())
111
+ except Exception as e:
112
+ raise ErrInfo("db", "Call to populate_table when importing data", exception_msg=exception_info()) from e
execsql/importers/csv.py CHANGED
@@ -60,13 +60,13 @@ def importtable(
60
60
  # ...except for Firebird. Execute the commit directly via the connection so it is always done.
61
61
  if db.type == dbt_firebird:
62
62
  db.conn.commit()
63
- except Exception:
63
+ except Exception as e:
64
64
  raise ErrInfo(
65
65
  type="db",
66
66
  command_text=sql,
67
67
  exception_msg=exception_info(),
68
68
  other_msg=f"Could not create new table ({tablename}) for IMPORT metacommand",
69
- )
69
+ ) from e
70
70
  else:
71
71
  if schemaname is not None:
72
72
  if not db.table_exists(tablename, schemaname):
@@ -85,13 +85,13 @@ def importtable(
85
85
  db.commit()
86
86
  except ErrInfo:
87
87
  raise
88
- except Exception:
88
+ except Exception as e:
89
89
  fq_tablename = db.schema_qualified_table_name(schemaname, tablename)
90
90
  raise ErrInfo(
91
91
  "exception",
92
92
  exception_msg=exception_info(),
93
93
  other_msg=f"Can't import tabular file ({filename}) to table ({fq_tablename})",
94
- )
94
+ ) from e
95
95
  inf.close()
96
96
 
97
97
 
@@ -121,10 +121,10 @@ def importfile(
121
121
  db.commit()
122
122
  except ErrInfo:
123
123
  raise
124
- except Exception:
124
+ except Exception as e:
125
125
  fq_tablename = db.schema_qualified_table_name(schemaname, tablename)
126
126
  raise ErrInfo(
127
127
  "exception",
128
128
  exception_msg=exception_info(),
129
129
  other_msg=f"Can't import file ({filename}) to table ({fq_tablename})",
130
- )
130
+ ) from e
@@ -26,12 +26,12 @@ def import_feather(
26
26
 
27
27
  try:
28
28
  import polars as pl
29
- except Exception:
29
+ except Exception as e:
30
30
  raise ErrInfo(
31
31
  "exception",
32
32
  exception_msg=exception_info(),
33
33
  other_msg="The polars Python library must be installed to import data from the Feather format.",
34
- )
34
+ ) from e
35
35
  df = pl.read_ipc(filename)
36
36
  hdrs = df.columns
37
37
  data = [list(row) for row in df.rows()]
@@ -49,12 +49,12 @@ def import_parquet(
49
49
 
50
50
  try:
51
51
  import polars as pl
52
- except Exception:
52
+ except Exception as e:
53
53
  raise ErrInfo(
54
54
  "exception",
55
55
  exception_msg=exception_info(),
56
56
  other_msg="The polars Python library must be installed to import data from the Parquet format.",
57
- )
57
+ ) from e
58
58
  df = pl.read_parquet(filename)
59
59
  hdrs = df.columns
60
60
  data = [list(row) for row in df.rows()]
execsql/importers/ods.py CHANGED
@@ -31,12 +31,12 @@ def ods_data(
31
31
  wbk = OdsFile()
32
32
  try:
33
33
  wbk.open(filename)
34
- except Exception:
35
- raise ErrInfo(type="cmd", other_msg=f"{filename} is not a valid OpenDocument spreadsheet.")
34
+ except Exception as e:
35
+ raise ErrInfo(type="cmd", other_msg=f"{filename} is not a valid OpenDocument spreadsheet.") from e
36
36
  try:
37
37
  alldata = wbk.sheet_data(sheetname, junk_header_rows)
38
- except Exception:
39
- raise ErrInfo(type="cmd", other_msg=f"{sheetname} is not a worksheet in {filename}.")
38
+ except Exception as e:
39
+ raise ErrInfo(type="cmd", other_msg=f"{sheetname} is not a worksheet in {filename}.") from e
40
40
  colhdrs = alldata[0]
41
41
  if any(x is None or len(x.strip()) == 0 for x in colhdrs):
42
42
  if conf.del_empty_cols:
execsql/importers/xls.py CHANGED
@@ -45,12 +45,12 @@ def xls_data(
45
45
  raise ErrInfo(type="cmd", other_msg=f"{filename} is not a recognizable Excel spreadsheet name.")
46
46
  try:
47
47
  wbk.open(filename, encoding, read_only=True)
48
- except Exception:
49
- raise ErrInfo(type="cmd", other_msg=f"{filename} is not a valid Excel spreadsheet.")
48
+ except Exception as e:
49
+ raise ErrInfo(type="cmd", other_msg=f"{filename} is not a valid Excel spreadsheet.") from e
50
50
  try:
51
51
  alldata = wbk.sheet_data(sheetname, junk_header_rows)
52
- except Exception:
53
- raise ErrInfo(type="cmd", other_msg=f"Error reading worksheet {sheetname} from {filename}.")
52
+ except Exception as e:
53
+ raise ErrInfo(type="cmd", other_msg=f"Error reading worksheet {sheetname} from {filename}.") from e
54
54
  if len(alldata) == 0:
55
55
  raise ErrInfo(type="cmd", other_msg=f"There are no data on worksheet {sheetname} of file {filename}.")
56
56
  if ext3 == "lsx":