execsql2 2.24.0__py3-none-any.whl → 2.25.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.
Files changed (72) hide show
  1. execsql/api.py +169 -160
  2. execsql/cli/__init__.py +2 -0
  3. execsql/cli/commands/lint.py +2 -10
  4. execsql/cli/commands/run.py +35 -27
  5. execsql/cli/dsn.py +11 -4
  6. execsql/cli/lint.py +271 -28
  7. execsql/cli/options.py +1 -1
  8. execsql/cli/run.py +56 -19
  9. execsql/config.py +5 -19
  10. execsql/data/execsql.conf.template +14 -4
  11. execsql/db/access.py +29 -8
  12. execsql/db/base.py +40 -8
  13. execsql/db/dsn.py +13 -4
  14. execsql/db/duckdb.py +3 -1
  15. execsql/db/factory.py +11 -2
  16. execsql/db/firebird.py +21 -7
  17. execsql/db/mysql.py +37 -16
  18. execsql/db/oracle.py +49 -16
  19. execsql/db/postgres.py +30 -6
  20. execsql/db/sqlite.py +7 -9
  21. execsql/db/sqlserver.py +25 -8
  22. execsql/exporters/delimited.py +87 -28
  23. execsql/exporters/xlsx.py +24 -9
  24. execsql/exporters/zip.py +1 -2
  25. execsql/format.py +29 -2
  26. execsql/gui/desktop.py +1 -3
  27. execsql/gui/tui.py +2 -2
  28. execsql/importers/base.py +35 -1
  29. execsql/importers/csv.py +3 -1
  30. execsql/manifest.py +33 -22
  31. execsql/metacommands/conditions.py +6 -7
  32. execsql/metacommands/data.py +4 -1
  33. execsql/metacommands/debug.py +2 -4
  34. execsql/metacommands/io_fileops.py +62 -37
  35. execsql/metacommands/io_import.py +0 -1
  36. execsql/metacommands/prompt.py +2 -0
  37. execsql/metacommands/upsert.py +5 -2
  38. execsql/models.py +3 -2
  39. execsql/parser.py +14 -5
  40. execsql/plugins.py +24 -0
  41. execsql/script/engine.py +40 -11
  42. execsql/script/executor.py +9 -5
  43. execsql/script/parser.py +167 -118
  44. execsql/state.py +23 -9
  45. execsql/state.pyi +8 -0
  46. execsql/types.py +22 -13
  47. execsql/utils/auth.py +169 -24
  48. execsql/utils/datetime.py +55 -17
  49. execsql/utils/errors.py +71 -9
  50. execsql/utils/fileio.py +404 -117
  51. execsql/utils/gui.py +3 -0
  52. execsql/utils/mail.py +37 -12
  53. {execsql2-2.24.0.dist-info → execsql2-2.25.0.dist-info}/METADATA +18 -12
  54. {execsql2-2.24.0.dist-info → execsql2-2.25.0.dist-info}/RECORD +72 -72
  55. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/README.md +0 -0
  56. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  57. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  58. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  59. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  60. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  61. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  62. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  63. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  64. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  65. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/script_template.sql +0 -0
  66. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  67. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  68. {execsql2-2.24.0.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  69. {execsql2-2.24.0.dist-info → execsql2-2.25.0.dist-info}/WHEEL +0 -0
  70. {execsql2-2.24.0.dist-info → execsql2-2.25.0.dist-info}/entry_points.txt +0 -0
  71. {execsql2-2.24.0.dist-info → execsql2-2.25.0.dist-info}/licenses/LICENSE.txt +0 -0
  72. {execsql2-2.24.0.dist-info → execsql2-2.25.0.dist-info}/licenses/NOTICE +0 -0
execsql/utils/errors.py CHANGED
@@ -8,8 +8,10 @@ throughout the codebase:
8
8
 
9
9
  - :func:`exception_info` — extracts type, value, and source location
10
10
  from the current exception.
11
- - :func:`exception_desc` — formats exception information as a
12
- human-readable string.
11
+ - :func:`exception_desc` — ``Type: message`` for the exception being
12
+ handled, as shown on screen.
13
+ - :func:`error_origin` / :func:`logged_error` — where in execsql an error
14
+ was raised, added to the message written to the log.
13
15
  - :func:`write_warning` — writes a non-fatal warning message.
14
16
  - :func:`exit_now` — terminates execution after optional halt hooks.
15
17
  - :func:`fatal_error` — logs a fatal error and calls :func:`exit_now`.
@@ -27,7 +29,9 @@ from execsql.exceptions import ErrInfo
27
29
 
28
30
  __all__ = [
29
31
  "exception_info",
32
+ "error_origin",
30
33
  "exception_desc",
34
+ "logged_error",
31
35
  "exit_now",
32
36
  "fatal_error",
33
37
  "stamp_errinfo",
@@ -67,8 +71,35 @@ def exception_info() -> tuple:
67
71
 
68
72
 
69
73
  def exception_desc() -> str:
70
- exc_type, exc_strval, exc_filename, exc_lineno, exc_linetext = exception_info()
71
- return f"{exc_type}: {exc_strval} in {exc_filename} on line {exc_lineno} of execsql."
74
+ """``Type: message`` for the exception being handled, as shown to the person running the script.
75
+
76
+ Where in execsql it was raised is for the log: see :func:`error_origin`.
77
+ """
78
+ exc_type, exc_strval, _filename, _lineno, _linetext = exception_info()
79
+ return f"{exc_type}: {exc_strval}"
80
+
81
+
82
+ def error_origin(exc: BaseException) -> str | None:
83
+ """Where in the execsql package the error behind *exc* was raised, as ``execsql/db/base.py:254``.
84
+
85
+ Follows *exc* to the exception it was raised from, and returns the last
86
+ frame of that traceback inside the package, or ``None`` when there is none.
87
+ """
88
+ package = Path(__file__).resolve().parents[1]
89
+ cause = exc.__cause__ or exc.__context__ or exc
90
+ origin = None
91
+ for frame in traceback.extract_tb(cause.__traceback__):
92
+ path = Path(frame.filename).resolve()
93
+ if path.is_relative_to(package):
94
+ origin = f"{path.relative_to(package.parent).as_posix()}:{frame.lineno}"
95
+ return origin
96
+
97
+
98
+ def logged_error(errinfo: ErrInfo) -> str:
99
+ """The error message for the log: what the screen shows, plus :func:`error_origin` when known."""
100
+ origin = error_origin(errinfo)
101
+ message = errinfo.errmsg()
102
+ return f"{message}\n Raised at: {origin}" if origin else message
72
103
 
73
104
 
74
105
  def stamp_errinfo(errinfo: ErrInfo) -> ErrInfo:
@@ -124,18 +155,38 @@ def _run_deferred_script(spec: Any) -> None:
124
155
  )
125
156
 
126
157
 
158
+ #: How long a halt waits for an output file that another process has
159
+ #: locked, before its pending output is counted as lost.
160
+ HALT_FLUSH_SECONDS = 10.0
161
+
162
+
163
+ def _flush_output_at_halt() -> ErrInfo | None:
164
+ """Write out what the run's files are still waiting for; the loss, if any, as an error."""
165
+ from execsql.utils.fileio import filewriter_close_all_after_write
166
+
167
+ try:
168
+ filewriter_close_all_after_write(max_wait=HALT_FLUSH_SECONDS)
169
+ except ErrInfo as lost:
170
+ # The loss belongs to no particular statement.
171
+ lost.script_file = ""
172
+ return lost
173
+ except Exception:
174
+ pass # A failed flush must not hide the error that halted the run.
175
+ return None
176
+
177
+
127
178
  def exit_now(exit_status: int, errinfo: ErrInfo | None, logmsg: str | None = None) -> None:
128
179
  # An error in input typed at a BREAKPOINT prompt ends
129
180
  # that input, not the run: the prompt reports it and reads the next one.
130
181
  # HALT and a canceled prompt carry no error and still end the run.
131
182
  if errinfo is not None and _state.prompt_input is not None:
132
183
  raise errinfo
133
- em = None
184
+ _state.halt_error = errinfo
134
185
  if errinfo is not None:
135
186
  stamp_errinfo(errinfo)
136
187
  if _state.subvars is not None:
137
188
  _state.subvars.add_substitution("$ERROR_MESSAGE", errinfo.errmsg())
138
- em = errinfo.write()
189
+ errinfo.write()
139
190
  if _state.err_halt_writespec is not None:
140
191
  try:
141
192
  _state.err_halt_writespec.write()
@@ -182,14 +233,23 @@ def exit_now(exit_status: int, errinfo: ErrInfo | None, logmsg: str | None = Non
182
233
  cancelexec = _state.cancel_halt_exec
183
234
  _state.cancel_halt_exec = None
184
235
  _run_deferred_script(cancelexec)
236
+ # After the ON ERROR_HALT / ON CANCEL_HALT actions, which may write files
237
+ # themselves; before the writer is stopped, which discards what waits.
238
+ lost_output = _flush_output_at_halt()
239
+ if lost_output is not None:
240
+ _state.halt_output_error = lost_output
241
+ lost_output.write()
242
+ exit_status = exit_status or 1
185
243
  if exit_status > 0 and _state.exec_log:
186
244
  if logmsg:
187
245
  _state.exec_log.log_exit_error(logmsg)
188
- else:
189
- if em:
190
- _state.exec_log.log_exit_error(em)
246
+ elif errinfo is not None:
247
+ _state.exec_log.log_exit_error(logged_error(errinfo))
248
+ if lost_output is not None:
249
+ _state.exec_log.log_status_error(lost_output.errmsg())
191
250
  if _state.exec_log is not None:
192
251
  _state.exec_log.log_status_info(f"{_state.cmds_run} commands run")
252
+ _state.exec_log.log_exit()
193
253
  _state.exec_log.close()
194
254
  from execsql.utils.fileio import filewriter_end
195
255
 
@@ -198,6 +258,8 @@ def exit_now(exit_status: int, errinfo: ErrInfo | None, logmsg: str | None = Non
198
258
  from execsql import manifest as _manifest
199
259
 
200
260
  if (manifest := _manifest.current()) is not None:
261
+ if lost_output is not None:
262
+ manifest.add_error(lost_output)
201
263
  manifest.finish(exit_status, errinfo)
202
264
  sys.exit(exit_status)
203
265