execsql2 2.22.8__tar.gz → 2.22.9__tar.gz

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 (379) hide show
  1. {execsql2-2.22.8 → execsql2-2.22.9}/CHANGELOG.md +9 -0
  2. {execsql2-2.22.8 → execsql2-2.22.9}/PKG-INFO +2 -2
  3. {execsql2-2.22.8 → execsql2-2.22.9}/README.md +1 -1
  4. {execsql2-2.22.8 → execsql2-2.22.9}/docs/about/divergence.md +1 -0
  5. {execsql2-2.22.8 → execsql2-2.22.9}/docs/guides/formatter.md +1 -1
  6. {execsql2-2.22.8 → execsql2-2.22.9}/docs/reference/configuration.md +1 -1
  7. {execsql2-2.22.8 → execsql2-2.22.9}/pyproject.toml +6 -2
  8. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/data/execsql.conf.template +1 -0
  9. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/mail.py +9 -5
  10. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_mail.py +190 -23
  11. {execsql2-2.22.8 → execsql2-2.22.9}/uv.lock +1 -1
  12. {execsql2-2.22.8 → execsql2-2.22.9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  13. {execsql2-2.22.8 → execsql2-2.22.9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  14. {execsql2-2.22.8 → execsql2-2.22.9}/.github/dependabot.yml +0 -0
  15. {execsql2-2.22.8 → execsql2-2.22.9}/.github/workflows/ci-cd.yml +0 -0
  16. {execsql2-2.22.8 → execsql2-2.22.9}/.gitignore +0 -0
  17. {execsql2-2.22.8 → execsql2-2.22.9}/.pre-commit-config.yaml +0 -0
  18. {execsql2-2.22.8 → execsql2-2.22.9}/.pre-commit-hooks.yaml +0 -0
  19. {execsql2-2.22.8 → execsql2-2.22.9}/.python-version +0 -0
  20. {execsql2-2.22.8 → execsql2-2.22.9}/.readthedocs.yaml +0 -0
  21. {execsql2-2.22.8 → execsql2-2.22.9}/CONTRIBUTING.md +0 -0
  22. {execsql2-2.22.8 → execsql2-2.22.9}/LICENSE.txt +0 -0
  23. {execsql2-2.22.8 → execsql2-2.22.9}/NOTICE +0 -0
  24. {execsql2-2.22.8 → execsql2-2.22.9}/SECURITY.md +0 -0
  25. {execsql2-2.22.8 → execsql2-2.22.9}/docs/about/contributors.md +0 -0
  26. {execsql2-2.22.8 → execsql2-2.22.9}/docs/about/copyright.md +0 -0
  27. {execsql2-2.22.8 → execsql2-2.22.9}/docs/api/cli.md +0 -0
  28. {execsql2-2.22.8 → execsql2-2.22.9}/docs/api/db.md +0 -0
  29. {execsql2-2.22.8 → execsql2-2.22.9}/docs/api/exporters.md +0 -0
  30. {execsql2-2.22.8 → execsql2-2.22.9}/docs/api/importers.md +0 -0
  31. {execsql2-2.22.8 → execsql2-2.22.9}/docs/api/index.md +0 -0
  32. {execsql2-2.22.8 → execsql2-2.22.9}/docs/api/metacommands.md +0 -0
  33. {execsql2-2.22.8 → execsql2-2.22.9}/docs/dev/adding_db_adapters.md +0 -0
  34. {execsql2-2.22.8 → execsql2-2.22.9}/docs/dev/adding_exporters.md +0 -0
  35. {execsql2-2.22.8 → execsql2-2.22.9}/docs/dev/adding_importers.md +0 -0
  36. {execsql2-2.22.8 → execsql2-2.22.9}/docs/dev/adding_metacommands.md +0 -0
  37. {execsql2-2.22.8 → execsql2-2.22.9}/docs/dev/architecture.md +0 -0
  38. {execsql2-2.22.8 → execsql2-2.22.9}/docs/dev/releasing.md +0 -0
  39. {execsql2-2.22.8 → execsql2-2.22.9}/docs/getting-started/installation.md +0 -0
  40. {execsql2-2.22.8 → execsql2-2.22.9}/docs/getting-started/requirements.md +0 -0
  41. {execsql2-2.22.8 → execsql2-2.22.9}/docs/getting-started/syntax.md +0 -0
  42. {execsql2-2.22.8 → execsql2-2.22.9}/docs/guides/debugging.md +0 -0
  43. {execsql2-2.22.8 → execsql2-2.22.9}/docs/guides/documentation.md +0 -0
  44. {execsql2-2.22.8 → execsql2-2.22.9}/docs/guides/encoding.md +0 -0
  45. {execsql2-2.22.8 → execsql2-2.22.9}/docs/guides/examples.md +0 -0
  46. {execsql2-2.22.8 → execsql2-2.22.9}/docs/guides/logging.md +0 -0
  47. {execsql2-2.22.8 → execsql2-2.22.9}/docs/guides/sql_syntax.md +0 -0
  48. {execsql2-2.22.8 → execsql2-2.22.9}/docs/guides/usage.md +0 -0
  49. {execsql2-2.22.8 → execsql2-2.22.9}/docs/guides/using_scripts.md +0 -0
  50. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/Compare_planets.png +0 -0
  51. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/actions.png +0 -0
  52. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/actions2.png +0 -0
  53. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/checkboxes.png +0 -0
  54. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/connect.b64 +0 -0
  55. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/connect.png +0 -0
  56. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/create_conf.png +0 -0
  57. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/data_error1_screenshot.jpg +0 -0
  58. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/entry_form.png +0 -0
  59. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/execsql_console.png +0 -0
  60. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/execsql_logo_01.png +0 -0
  61. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/fatals.png +0 -0
  62. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/logo_small.png +0 -0
  63. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/pause_terminal.png +0 -0
  64. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/pause_terminal_sm.b64 +0 -0
  65. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/pause_terminal_sm.png +0 -0
  66. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/prompt_compare.png +0 -0
  67. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/set_build_commands.jpg +0 -0
  68. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/unit_conversions.b64 +0 -0
  69. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/unit_conversions_029.png +0 -0
  70. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/unmatched.png +0 -0
  71. {execsql2-2.22.8 → execsql2-2.22.9}/docs/images/vim_execsql_highlight.png +0 -0
  72. {execsql2-2.22.8 → execsql2-2.22.9}/docs/index.md +0 -0
  73. {execsql2-2.22.8 → execsql2-2.22.9}/docs/reference/metacommands.md +0 -0
  74. {execsql2-2.22.8 → execsql2-2.22.9}/docs/reference/security.md +0 -0
  75. {execsql2-2.22.8 → execsql2-2.22.9}/docs/reference/substitution_vars.md +0 -0
  76. {execsql2-2.22.8 → execsql2-2.22.9}/extras/plugin-template/README.md +0 -0
  77. {execsql2-2.22.8 → execsql2-2.22.9}/extras/plugin-template/pyproject.toml +0 -0
  78. {execsql2-2.22.8 → execsql2-2.22.9}/extras/plugin-template/src/execsql_plugin_YOURNAME/__init__.py +0 -0
  79. {execsql2-2.22.8 → execsql2-2.22.9}/extras/plugin-template/tests/test_plugin.py.example +0 -0
  80. {execsql2-2.22.8 → execsql2-2.22.9}/extras/vscode-execsql/README.md +0 -0
  81. {execsql2-2.22.8 → execsql2-2.22.9}/extras/vscode-execsql/package.json +0 -0
  82. {execsql2-2.22.8 → execsql2-2.22.9}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  83. {execsql2-2.22.8 → execsql2-2.22.9}/justfile +0 -0
  84. {execsql2-2.22.8 → execsql2-2.22.9}/scripts/benchmarks/README.md +0 -0
  85. {execsql2-2.22.8 → execsql2-2.22.9}/scripts/benchmarks/results/.gitignore +0 -0
  86. {execsql2-2.22.8 → execsql2-2.22.9}/scripts/benchmarks/run_benchmarks.py +0 -0
  87. {execsql2-2.22.8 → execsql2-2.22.9}/scripts/generate_vscode_grammar.py +0 -0
  88. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/__init__.py +0 -0
  89. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/__main__.py +0 -0
  90. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/api.py +0 -0
  91. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/cli/__init__.py +0 -0
  92. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/cli/dsn.py +0 -0
  93. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/cli/help.py +0 -0
  94. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/cli/lint.py +0 -0
  95. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/cli/run.py +0 -0
  96. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/config.py +0 -0
  97. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/data/__init__.py +0 -0
  98. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/__init__.py +0 -0
  99. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/access.py +0 -0
  100. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/base.py +0 -0
  101. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/dsn.py +0 -0
  102. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/duckdb.py +0 -0
  103. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/factory.py +0 -0
  104. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/firebird.py +0 -0
  105. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/mysql.py +0 -0
  106. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/oracle.py +0 -0
  107. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/postgres.py +0 -0
  108. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/sqlite.py +0 -0
  109. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/db/sqlserver.py +0 -0
  110. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/debug/__init__.py +0 -0
  111. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/debug/repl.py +0 -0
  112. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exceptions.py +0 -0
  113. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/__init__.py +0 -0
  114. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/base.py +0 -0
  115. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/delimited.py +0 -0
  116. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/duckdb.py +0 -0
  117. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/feather.py +0 -0
  118. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/html.py +0 -0
  119. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/json.py +0 -0
  120. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/latex.py +0 -0
  121. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/markdown.py +0 -0
  122. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/ods.py +0 -0
  123. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/parquet.py +0 -0
  124. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/pretty.py +0 -0
  125. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/protocol.py +0 -0
  126. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/raw.py +0 -0
  127. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/sqlite.py +0 -0
  128. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/templates.py +0 -0
  129. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/values.py +0 -0
  130. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/xls.py +0 -0
  131. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/xlsx.py +0 -0
  132. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/xml.py +0 -0
  133. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/yaml.py +0 -0
  134. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/exporters/zip.py +0 -0
  135. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/format.py +0 -0
  136. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/gui/__init__.py +0 -0
  137. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/gui/base.py +0 -0
  138. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/gui/console.py +0 -0
  139. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/gui/desktop.py +0 -0
  140. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/gui/tui.py +0 -0
  141. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/importers/__init__.py +0 -0
  142. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/importers/base.py +0 -0
  143. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/importers/csv.py +0 -0
  144. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/importers/feather.py +0 -0
  145. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/importers/json.py +0 -0
  146. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/importers/ods.py +0 -0
  147. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/importers/xls.py +0 -0
  148. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/__init__.py +0 -0
  149. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/conditions.py +0 -0
  150. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/connect.py +0 -0
  151. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/control.py +0 -0
  152. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/data.py +0 -0
  153. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/debug.py +0 -0
  154. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/dispatch.py +0 -0
  155. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/io.py +0 -0
  156. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/io_export.py +0 -0
  157. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/io_fileops.py +0 -0
  158. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/io_import.py +0 -0
  159. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/io_write.py +0 -0
  160. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/prompt.py +0 -0
  161. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/script_ext.py +0 -0
  162. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/system.py +0 -0
  163. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/metacommands/upsert.py +0 -0
  164. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/models.py +0 -0
  165. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/parser.py +0 -0
  166. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/plugins.py +0 -0
  167. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/py.typed +0 -0
  168. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/script/__init__.py +0 -0
  169. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/script/ast.py +0 -0
  170. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/script/control.py +0 -0
  171. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/script/engine.py +0 -0
  172. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/script/executor.py +0 -0
  173. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/script/parser.py +0 -0
  174. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/script/variables.py +0 -0
  175. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/state.py +0 -0
  176. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/state.pyi +0 -0
  177. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/types.py +0 -0
  178. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/__init__.py +0 -0
  179. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/auth.py +0 -0
  180. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/crypto.py +0 -0
  181. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/datetime.py +0 -0
  182. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/errors.py +0 -0
  183. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/fileio.py +0 -0
  184. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/gui.py +0 -0
  185. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/numeric.py +0 -0
  186. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/regex.py +0 -0
  187. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/strings.py +0 -0
  188. {execsql2-2.22.8 → execsql2-2.22.9}/src/execsql/utils/timer.py +0 -0
  189. {execsql2-2.22.8 → execsql2-2.22.9}/templates/README.md +0 -0
  190. {execsql2-2.22.8 → execsql2-2.22.9}/templates/config_settings.sqlite +0 -0
  191. {execsql2-2.22.8 → execsql2-2.22.9}/templates/example_config_prompt.sql +0 -0
  192. {execsql2-2.22.8 → execsql2-2.22.9}/templates/make_config_db.sql +0 -0
  193. {execsql2-2.22.8 → execsql2-2.22.9}/templates/md_compare.sql +0 -0
  194. {execsql2-2.22.8 → execsql2-2.22.9}/templates/md_glossary.sql +0 -0
  195. {execsql2-2.22.8 → execsql2-2.22.9}/templates/md_upsert.sql +0 -0
  196. {execsql2-2.22.8 → execsql2-2.22.9}/templates/pg_compare.sql +0 -0
  197. {execsql2-2.22.8 → execsql2-2.22.9}/templates/pg_glossary.sql +0 -0
  198. {execsql2-2.22.8 → execsql2-2.22.9}/templates/pg_upsert.sql +0 -0
  199. {execsql2-2.22.8 → execsql2-2.22.9}/templates/script_template.sql +0 -0
  200. {execsql2-2.22.8 → execsql2-2.22.9}/templates/ss_compare.sql +0 -0
  201. {execsql2-2.22.8 → execsql2-2.22.9}/templates/ss_glossary.sql +0 -0
  202. {execsql2-2.22.8 → execsql2-2.22.9}/templates/ss_upsert.sql +0 -0
  203. {execsql2-2.22.8 → execsql2-2.22.9}/tests/__init__.py +0 -0
  204. {execsql2-2.22.8 → execsql2-2.22.9}/tests/cli/__init__.py +0 -0
  205. {execsql2-2.22.8 → execsql2-2.22.9}/tests/cli/test_cli.py +0 -0
  206. {execsql2-2.22.8 → execsql2-2.22.9}/tests/cli/test_cli_e2e.py +0 -0
  207. {execsql2-2.22.8 → execsql2-2.22.9}/tests/cli/test_cli_run.py +0 -0
  208. {execsql2-2.22.8 → execsql2-2.22.9}/tests/cli/test_lint.py +0 -0
  209. {execsql2-2.22.8 → execsql2-2.22.9}/tests/cli/test_ping.py +0 -0
  210. {execsql2-2.22.8 → execsql2-2.22.9}/tests/cli/test_profile.py +0 -0
  211. {execsql2-2.22.8 → execsql2-2.22.9}/tests/conftest.py +0 -0
  212. {execsql2-2.22.8 → execsql2-2.22.9}/tests/data/formatter_shapes/comment_anchoring.sql +0 -0
  213. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/__init__.py +0 -0
  214. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_access_windows.py +0 -0
  215. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_base.py +0 -0
  216. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_db_adapters_mocked.py +0 -0
  217. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_db_adapters_mocked_extra.py +0 -0
  218. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_dsn.py +0 -0
  219. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_duckdb.py +0 -0
  220. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_factory.py +0 -0
  221. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_mysql_case_folding.py +0 -0
  222. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_mysql_inprocess.py +0 -0
  223. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_postgres.py +0 -0
  224. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_postgres_inprocess.py +0 -0
  225. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_sqlite.py +0 -0
  226. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_sqlite_extra.py +0 -0
  227. {execsql2-2.22.8 → execsql2-2.22.9}/tests/db/test_sqlserver_inprocess.py +0 -0
  228. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/__init__.py +0 -0
  229. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_base.py +0 -0
  230. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_db.py +0 -0
  231. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_delimited.py +0 -0
  232. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_duckdb_exporter.py +0 -0
  233. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_exporters.py +0 -0
  234. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_feather.py +0 -0
  235. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_html_extended.py +0 -0
  236. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_html_latex.py +0 -0
  237. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_json.py +0 -0
  238. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_json_extended.py +0 -0
  239. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_latex_extended.py +0 -0
  240. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_markdown.py +0 -0
  241. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_ods.py +0 -0
  242. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_ods_export.py +0 -0
  243. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_parquet.py +0 -0
  244. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_pretty_extended.py +0 -0
  245. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_raw_extended.py +0 -0
  246. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_sqlite_exporter.py +0 -0
  247. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_templates.py +0 -0
  248. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_templates_extended.py +0 -0
  249. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_values_extended.py +0 -0
  250. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_xls_xlsx.py +0 -0
  251. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_xlsx.py +0 -0
  252. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_xml.py +0 -0
  253. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_yaml.py +0 -0
  254. {execsql2-2.22.8 → execsql2-2.22.9}/tests/exporters/test_zip.py +0 -0
  255. {execsql2-2.22.8 → execsql2-2.22.9}/tests/gui/__init__.py +0 -0
  256. {execsql2-2.22.8 → execsql2-2.22.9}/tests/gui/test_backends.py +0 -0
  257. {execsql2-2.22.8 → execsql2-2.22.9}/tests/gui/test_backends_extended.py +0 -0
  258. {execsql2-2.22.8 → execsql2-2.22.9}/tests/gui/test_compare_stats.py +0 -0
  259. {execsql2-2.22.8 → execsql2-2.22.9}/tests/gui/test_compute_row_diffs.py +0 -0
  260. {execsql2-2.22.8 → execsql2-2.22.9}/tests/gui/test_desktop_dialogs.py +0 -0
  261. {execsql2-2.22.8 → execsql2-2.22.9}/tests/gui/test_format_table.py +0 -0
  262. {execsql2-2.22.8 → execsql2-2.22.9}/tests/gui/test_tui_pilot.py +0 -0
  263. {execsql2-2.22.8 → execsql2-2.22.9}/tests/gui/test_tui_pilot_complex.py +0 -0
  264. {execsql2-2.22.8 → execsql2-2.22.9}/tests/gui/test_utils_gui_extended.py +0 -0
  265. {execsql2-2.22.8 → execsql2-2.22.9}/tests/importers/__init__.py +0 -0
  266. {execsql2-2.22.8 → execsql2-2.22.9}/tests/importers/test_base_extended.py +0 -0
  267. {execsql2-2.22.8 → execsql2-2.22.9}/tests/importers/test_csv_edge_cases.py +0 -0
  268. {execsql2-2.22.8 → execsql2-2.22.9}/tests/importers/test_csv_importer.py +0 -0
  269. {execsql2-2.22.8 → execsql2-2.22.9}/tests/importers/test_feather_importer.py +0 -0
  270. {execsql2-2.22.8 → execsql2-2.22.9}/tests/importers/test_json_importer.py +0 -0
  271. {execsql2-2.22.8 → execsql2-2.22.9}/tests/importers/test_ods_importer.py +0 -0
  272. {execsql2-2.22.8 → execsql2-2.22.9}/tests/importers/test_xls_importer.py +0 -0
  273. {execsql2-2.22.8 → execsql2-2.22.9}/tests/integration/__init__.py +0 -0
  274. {execsql2-2.22.8 → execsql2-2.22.9}/tests/integration/conftest.py +0 -0
  275. {execsql2-2.22.8 → execsql2-2.22.9}/tests/integration/test_dsn.py +0 -0
  276. {execsql2-2.22.8 → execsql2-2.22.9}/tests/integration/test_duckdb.py +0 -0
  277. {execsql2-2.22.8 → execsql2-2.22.9}/tests/integration/test_mysql.py +0 -0
  278. {execsql2-2.22.8 → execsql2-2.22.9}/tests/integration/test_postgres.py +0 -0
  279. {execsql2-2.22.8 → execsql2-2.22.9}/tests/integration/test_sqlite.py +0 -0
  280. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/__init__.py +0 -0
  281. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_assert.py +0 -0
  282. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_breakpoint.py +0 -0
  283. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_conditions_extra.py +0 -0
  284. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_connect.py +0 -0
  285. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_io_export.py +0 -0
  286. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_io_import.py +0 -0
  287. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_metacommands.py +0 -0
  288. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_metacommands_connect.py +0 -0
  289. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_metacommands_data.py +0 -0
  290. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_metacommands_extended.py +0 -0
  291. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  292. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_metacommands_io.py +0 -0
  293. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  294. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  295. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_metacommands_system.py +0 -0
  296. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  297. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_pg_upsert.py +0 -0
  298. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_prompt.py +0 -0
  299. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_row_count.py +0 -0
  300. {execsql2-2.22.8 → execsql2-2.22.9}/tests/metacommands/test_show_scripts.py +0 -0
  301. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/__init__.py +0 -0
  302. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/audit_lint_bad.sql +0 -0
  303. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/audit_smoke/sample.json +0 -0
  304. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/audit_smoke/sample.jsonl +0 -0
  305. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/audit_smoke.sql +0 -0
  306. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/config_runtime.sql +0 -0
  307. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/control_flow.sql +0 -0
  308. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/counters_and_locals.sql +0 -0
  309. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/error_handling.sql +0 -0
  310. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/import_options/has_comments.csv +0 -0
  311. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/import_options.sql +0 -0
  312. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/includes/helper.sql +0 -0
  313. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/includes.sql +0 -0
  314. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/io_formats.sql +0 -0
  315. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/io_roundtrip.sql +0 -0
  316. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/multi_db.sql +0 -0
  317. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/parquet_feather_roundtrip.sql +0 -0
  318. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/parse_only/parse_tree.sql +0 -0
  319. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/predicates.sql +0 -0
  320. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/scripts.sql +0 -0
  321. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/smoke.sql +0 -0
  322. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/subroutine_loops.sql +0 -0
  323. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/subvars_advanced.sql +0 -0
  324. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/template_export/greeting.tmpl +0 -0
  325. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/template_export.sql +0 -0
  326. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/timer.sql +0 -0
  327. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/transactions.sql +0 -0
  328. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/write_create_table.sql +0 -0
  329. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/fixtures/xlsx_ods_roundtrip.sql +0 -0
  330. {execsql2-2.22.8 → execsql2-2.22.9}/tests/scripts/test_sql_scripts.py +0 -0
  331. {execsql2-2.22.8 → execsql2-2.22.9}/tests/security/__init__.py +0 -0
  332. {execsql2-2.22.8 → execsql2-2.22.9}/tests/security/test_env_detection.py +0 -0
  333. {execsql2-2.22.8 → execsql2-2.22.9}/tests/security/test_expansion_bomb.py +0 -0
  334. {execsql2-2.22.8 → execsql2-2.22.9}/tests/security/test_import_regex_hardening.py +0 -0
  335. {execsql2-2.22.8 → execsql2-2.22.9}/tests/security/test_log_redaction.py +0 -0
  336. {execsql2-2.22.8 → execsql2-2.22.9}/tests/security/test_odbc_injection.py +0 -0
  337. {execsql2-2.22.8 → execsql2-2.22.9}/tests/security/test_path_containment.py +0 -0
  338. {execsql2-2.22.8 → execsql2-2.22.9}/tests/security/test_substitution_injection.py +0 -0
  339. {execsql2-2.22.8 → execsql2-2.22.9}/tests/security/test_zip_bomb.py +0 -0
  340. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_api.py +0 -0
  341. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_api_extended.py +0 -0
  342. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_ast.py +0 -0
  343. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_ast_parser.py +0 -0
  344. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_bundled_templates.py +0 -0
  345. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_config.py +0 -0
  346. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_config_data.py +0 -0
  347. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_config_extended.py +0 -0
  348. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_debug_repl.py +0 -0
  349. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_engine.py +0 -0
  350. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_error_messages.py +0 -0
  351. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_exceptions.py +0 -0
  352. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_executor.py +0 -0
  353. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_executor_inprocess.py +0 -0
  354. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_format.py +0 -0
  355. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_models.py +0 -0
  356. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_package.py +0 -0
  357. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_parser.py +0 -0
  358. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_parser_params.py +0 -0
  359. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_plugins.py +0 -0
  360. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_registry.py +0 -0
  361. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_script.py +0 -0
  362. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_state.py +0 -0
  363. {execsql2-2.22.8 → execsql2-2.22.9}/tests/test_types.py +0 -0
  364. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/__init__.py +0 -0
  365. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_auth.py +0 -0
  366. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_auth_extra.py +0 -0
  367. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_crypto.py +0 -0
  368. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_datetime.py +0 -0
  369. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_encodedfile_context.py +0 -0
  370. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_errors.py +0 -0
  371. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_errors_extra.py +0 -0
  372. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_fileio.py +0 -0
  373. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_fileio_extra.py +0 -0
  374. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_numeric.py +0 -0
  375. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_regex.py +0 -0
  376. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_strings.py +0 -0
  377. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_timer.py +0 -0
  378. {execsql2-2.22.8 → execsql2-2.22.9}/tests/utils/test_timer_extra.py +0 -0
  379. {execsql2-2.22.8 → execsql2-2.22.9}/zensical.toml +0 -0
@@ -13,6 +13,15 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.22.9] - 2026-09-24
17
+
18
+ ### Fixed
19
+
20
+ - `EMAIL`, and the `ON ERROR_HALT EMAIL` and `ON CANCEL_HALT EMAIL` hooks, failed with `AttributeError: 'SMTP' object has no attribute 'ehlo_or_hello_if_needed'` instead of sending. Every send was affected, on every server and TLS setting.
21
+ - An `[email]` `username` with no `password` or `enc_password` now reports a configuration error instead of failing with a `TypeError` from `smtplib`.
22
+
23
+ ______________________________________________________________________
24
+
16
25
  ## [2.22.8] - 2026-09-17
17
26
 
18
27
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: execsql2
3
- Version: 2.22.8
3
+ Version: 2.22.9
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
@@ -384,7 +384,7 @@ execsql-format --no-sql --in-place scripts/
384
384
  ```yaml
385
385
  repos:
386
386
  - repo: https://github.com/geocoug/execsql
387
- rev: v2.22.8
387
+ rev: v2.22.9
388
388
  hooks:
389
389
  - id: execsql-format
390
390
  ```
@@ -253,7 +253,7 @@ execsql-format --no-sql --in-place scripts/
253
253
  ```yaml
254
254
  repos:
255
255
  - repo: https://github.com/geocoug/execsql
256
- rev: v2.22.8
256
+ rev: v2.22.9
257
257
  hooks:
258
258
  - id: execsql-format
259
259
  ```
@@ -325,6 +325,7 @@ These are behavioral changes driven by security or correctness issues in the ups
325
325
  | Formatter dropped clauses from unparsable SQL | The sqlglot parse ran at `ErrorLevel.IGNORE`, which answers a statement it cannot parse with a partial AST rather than an error — the tokens it failed on are absent, and regenerating from that AST wrote back SQL the author never had. `INSERT ... CROSS JOIN LATERAL (VALUES ...) AS g(a, b) ON CONFLICT (a) DO NOTHING` came back without its `DO NOTHING`, a syntax error that `--in-place` reported as `reformatted`. Every guard passed it: one statement for one semicolon, 88% of the input's alphanumeric characters, and no string literals to round-trip. The guards are quantitative and the loss is qualitative — a dropped `DISTINCT`, `CASCADE`, `DESC`, or `NOT` is smaller still — so the parse now runs at `ErrorLevel.RAISE` and SQL that does not fully parse is emitted exactly as written. |
326
326
  | Formatter moved a comment into the middle of an expression | Comments are hidden from sqlglot as `/* marker */ line`, and sqlglot attaches the marker to an AST node. When it then reflows that node across several lines the marker surfaces deep inside the expression — `COALESCE((a.meas_value).undetected, FALSE)` carried it onto the `).undetected, FALSE)` continuation line — so restoring the comment above the marker split the expression around it, and the resulting layout reformatted differently on the next run. Comments whose markers sqlglot dropped entirely (inside a `CASE`) were re-inserted by token matching, which picked continuation lines the same way. Both paths now place the comment above the line where its expression opens, found by walking back to the parenthesis depth the comment sat at in the input. |
327
327
  | Formatter left a space before punctuation where a comment had been | Marker runs are replaced by the one space that keeps their neighbours apart, so `AND /* m */ RTRIM(` does not become `ANDRTRIM(`. Where sqlglot placed the run against a `;`, `,`, `)`, or `.` that space was residue rather than a separator: `1 = 1 /* m */;` came back as `1 = 1 ;`, normalised only on the next run, so `--check` failed on output the formatter had just produced. The run is now removed without a space at the ends of a line and against punctuation that binds to the token before it. |
328
+ | `EMAIL` SMTP login without a password | Upstream calls `smtplib.login()` with only a username when `[email] password` and `enc_password` are both unset, which raises `TypeError` from the stdlib — `login()` has no single-argument form. execsql2 reports a configuration error naming the missing password instead. |
328
329
 
329
330
  ______________________________________________________________________
330
331
 
@@ -188,7 +188,7 @@ select id,name,created_at from users where active = true order by name;
188
188
  ```yaml
189
189
  repos:
190
190
  - repo: https://github.com/geocoug/execsql
191
- rev: v2.22.8
191
+ rev: v2.22.9
192
192
  hooks:
193
193
  - id: execsql-format
194
194
  ```
@@ -266,7 +266,7 @@ The section and property names that may be used in a configuration file are list
266
266
 
267
267
  `username`
268
268
 
269
- : The name of the user if the SMTP server requires login authentication.
269
+ : The name of the user if the SMTP server requires login authentication. If `username` is set, either `password` or `enc_password` must also be set; execsql reports an error rather than attempting an incomplete login.
270
270
 
271
271
  `password`
272
272
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.22.8"
7
+ version = "2.22.9"
8
8
  description = "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."
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  license = { file = "LICENSE.txt" }
@@ -178,7 +178,7 @@ skip-magic-trailing-comma = false
178
178
  line-ending = "auto"
179
179
 
180
180
  [tool.bumpversion]
181
- current_version = "2.22.8"
181
+ current_version = "2.22.9"
182
182
  commit = true
183
183
  tag = true
184
184
  tag_name = "v{new_version}"
@@ -256,6 +256,10 @@ fo = "fo"
256
256
  odf = "odf"
257
257
  # Latin-1 test data: b"caf\xe9" decodes to "café"
258
258
  caf = "caf"
259
+ # SMTP verb: EHLO-or-HELO, not the English word. Correcting it to "hello"
260
+ # renames smtplib.ehlo_or_helo_if_needed() into a method that does not exist,
261
+ # which is exactly how every EMAIL send broke in 2.22.9.
262
+ helo = "helo"
259
263
 
260
264
  [tool.tox]
261
265
  required = ["tox-uv"]
@@ -242,6 +242,7 @@
242
242
  # Settings for the EMAIL metacommand.
243
243
 
244
244
  # SMTP server connection.
245
+ # If username is set, password (or enc_password, below) is required too.
245
246
  #host=
246
247
  #port=
247
248
  #username=
@@ -66,15 +66,19 @@ class Mailer:
66
66
  self.smtpconn = smtplib.SMTP_SSL(conf.smtp_host, conf.smtp_port, timeout=smtp_timeout)
67
67
  else:
68
68
  self.smtpconn = smtplib.SMTP(conf.smtp_host, conf.smtp_port, timeout=smtp_timeout)
69
- self.smtpconn.ehlo_or_hello_if_needed()
69
+ self.smtpconn.ehlo_or_helo_if_needed()
70
70
  if conf.smtp_tls:
71
71
  self.smtpconn.starttls()
72
72
  self.smtpconn.ehlo(conf.smtp_host)
73
73
  if conf.smtp_username:
74
- if conf.smtp_password:
75
- self.smtpconn.login(conf.smtp_username, conf.smtp_password)
76
- else:
77
- self.smtpconn.login(conf.smtp_username)
74
+ if not conf.smtp_password:
75
+ # smtplib.login() requires a password; calling it with only a
76
+ # username raises TypeError deep in the stdlib.
77
+ raise ErrInfo(
78
+ type="error",
79
+ other_msg="Can't send email; an SMTP username is configured but no password.",
80
+ )
81
+ self.smtpconn.login(conf.smtp_username, conf.smtp_password)
78
82
 
79
83
  def sendmail(
80
84
  self,
@@ -1,13 +1,21 @@
1
1
  """
2
2
  Tests for execsql.utils.mail — MailSpec construction and Mailer config validation.
3
3
 
4
- Mailer.__init__ establishes an SMTP connection, so all Mailer tests mock smtplib.
5
- MailSpec is a pure data class that can be tested without mocking.
4
+ Mailer.__init__ establishes an SMTP connection. Configuration tests mock
5
+ smtplib (always with a spec, so an assertion cannot name a method smtplib
6
+ does not have); the round-trip tests drive Mailer against a real in-process
7
+ SMTP server on loopback. MailSpec is a pure data class needing no mocking.
6
8
  """
7
9
 
8
10
  from __future__ import annotations
9
11
 
10
- from unittest.mock import MagicMock, patch
12
+ import base64
13
+ import smtplib
14
+ import socket
15
+ import socketserver
16
+ import threading
17
+ from typing import cast
18
+ from unittest.mock import create_autospec, patch
11
19
 
12
20
  import pytest
13
21
 
@@ -15,6 +23,12 @@ import execsql.state as _state
15
23
  from execsql.exceptions import ErrInfo
16
24
  from execsql.utils.mail import Mailer, MailSpec
17
25
 
26
+ # Captured before any @patch("smtplib.SMTP") swaps the real class for a mock —
27
+ # create_autospec() cannot spec a Mock, and a spec'd connection is the whole
28
+ # point: it rejects an assertion naming a method smtplib does not have.
29
+ _REAL_SMTP = smtplib.SMTP
30
+ _REAL_SMTP_SSL = smtplib.SMTP_SSL
31
+
18
32
 
19
33
  # ---------------------------------------------------------------------------
20
34
  # MailSpec — construction and attributes
@@ -80,7 +94,7 @@ class TestMailerConfigValidation:
80
94
 
81
95
  @patch("smtplib.SMTP")
82
96
  def test_creates_smtp_connection(self, mock_smtp_cls, minimal_conf):
83
- mock_conn = MagicMock()
97
+ mock_conn = create_autospec(_REAL_SMTP, instance=True)
84
98
  mock_smtp_cls.return_value = mock_conn
85
99
  _state.conf.smtp_host = "mail.example.com"
86
100
  _state.conf.smtp_port = None
@@ -90,13 +104,13 @@ class TestMailerConfigValidation:
90
104
  _state.conf.smtp_password = None
91
105
  m = Mailer()
92
106
  mock_smtp_cls.assert_called_once_with("mail.example.com", timeout=30)
93
- mock_conn.ehlo_or_hello_if_needed.assert_called_once()
107
+ mock_conn.ehlo_or_helo_if_needed.assert_called_once()
94
108
  # Clean up to avoid __del__ issues
95
109
  del m.smtpconn
96
110
 
97
111
  @patch("smtplib.SMTP")
98
112
  def test_creates_smtp_connection_with_port(self, mock_smtp_cls, minimal_conf):
99
- mock_conn = MagicMock()
113
+ mock_conn = create_autospec(_REAL_SMTP, instance=True)
100
114
  mock_smtp_cls.return_value = mock_conn
101
115
  _state.conf.smtp_host = "mail.example.com"
102
116
  _state.conf.smtp_port = 587
@@ -110,7 +124,7 @@ class TestMailerConfigValidation:
110
124
 
111
125
  @patch("smtplib.SMTP_SSL")
112
126
  def test_creates_smtp_ssl_connection(self, mock_smtp_ssl_cls, minimal_conf):
113
- mock_conn = MagicMock()
127
+ mock_conn = create_autospec(_REAL_SMTP_SSL, instance=True)
114
128
  mock_smtp_ssl_cls.return_value = mock_conn
115
129
  _state.conf.smtp_host = "mail.example.com"
116
130
  _state.conf.smtp_port = None
@@ -124,7 +138,7 @@ class TestMailerConfigValidation:
124
138
 
125
139
  @patch("smtplib.SMTP")
126
140
  def test_starttls_called_when_tls_enabled(self, mock_smtp_cls, minimal_conf):
127
- mock_conn = MagicMock()
141
+ mock_conn = create_autospec(_REAL_SMTP, instance=True)
128
142
  mock_smtp_cls.return_value = mock_conn
129
143
  _state.conf.smtp_host = "mail.example.com"
130
144
  _state.conf.smtp_port = None
@@ -138,7 +152,7 @@ class TestMailerConfigValidation:
138
152
 
139
153
  @patch("smtplib.SMTP")
140
154
  def test_login_called_with_credentials(self, mock_smtp_cls, minimal_conf):
141
- mock_conn = MagicMock()
155
+ mock_conn = create_autospec(_REAL_SMTP, instance=True)
142
156
  mock_smtp_cls.return_value = mock_conn
143
157
  _state.conf.smtp_host = "mail.example.com"
144
158
  _state.conf.smtp_port = None
@@ -151,8 +165,9 @@ class TestMailerConfigValidation:
151
165
  del m.smtpconn
152
166
 
153
167
  @patch("smtplib.SMTP")
154
- def test_login_without_password(self, mock_smtp_cls, minimal_conf):
155
- mock_conn = MagicMock()
168
+ def test_username_without_password_raises(self, mock_smtp_cls, minimal_conf):
169
+ """smtplib.login() has no single-argument form, so this must be reported, not attempted."""
170
+ mock_conn = create_autospec(_REAL_SMTP, instance=True)
156
171
  mock_smtp_cls.return_value = mock_conn
157
172
  _state.conf.smtp_host = "mail.example.com"
158
173
  _state.conf.smtp_port = None
@@ -160,9 +175,9 @@ class TestMailerConfigValidation:
160
175
  _state.conf.smtp_tls = False
161
176
  _state.conf.smtp_username = "user"
162
177
  _state.conf.smtp_password = None
163
- m = Mailer()
164
- mock_conn.login.assert_called_once_with("user")
165
- del m.smtpconn
178
+ with pytest.raises(ErrInfo, match="username is configured but no password"):
179
+ Mailer()
180
+ mock_conn.login.assert_not_called()
166
181
 
167
182
 
168
183
  # ---------------------------------------------------------------------------
@@ -173,7 +188,7 @@ class TestMailerConfigValidation:
173
188
  class TestMailerSendmail:
174
189
  @patch("smtplib.SMTP")
175
190
  def _make_mailer(self, mock_smtp_cls):
176
- mock_conn = MagicMock()
191
+ mock_conn = create_autospec(_REAL_SMTP, instance=True)
177
192
  mock_smtp_cls.return_value = mock_conn
178
193
  _state.conf.smtp_host = "mail.example.com"
179
194
  _state.conf.smtp_port = None
@@ -257,7 +272,7 @@ class TestMailerContextManager:
257
272
  @patch("smtplib.SMTP")
258
273
  def test_context_manager_returns_mailer_instance(self, mock_smtp_cls, minimal_conf):
259
274
  """__enter__ should return the Mailer itself."""
260
- mock_smtp_cls.return_value = MagicMock()
275
+ mock_smtp_cls.return_value = create_autospec(_REAL_SMTP, instance=True)
261
276
  _smtp_conf(minimal_conf)
262
277
  with Mailer() as m:
263
278
  assert isinstance(m, Mailer)
@@ -265,7 +280,7 @@ class TestMailerContextManager:
265
280
  @patch("smtplib.SMTP")
266
281
  def test_context_manager_exit_calls_close(self, mock_smtp_cls, minimal_conf):
267
282
  """__exit__ must call close(), which removes smtpconn."""
268
- mock_conn = MagicMock()
283
+ mock_conn = create_autospec(_REAL_SMTP, instance=True)
269
284
  mock_smtp_cls.return_value = mock_conn
270
285
  _smtp_conf(minimal_conf)
271
286
  m = Mailer()
@@ -277,7 +292,7 @@ class TestMailerContextManager:
277
292
  @patch("smtplib.SMTP")
278
293
  def test_context_manager_exit_called_on_with_block_exit(self, mock_smtp_cls, minimal_conf):
279
294
  """Leaving a `with` block must trigger __exit__ and remove smtpconn."""
280
- mock_conn = MagicMock()
295
+ mock_conn = create_autospec(_REAL_SMTP, instance=True)
281
296
  mock_smtp_cls.return_value = mock_conn
282
297
  _smtp_conf(minimal_conf)
283
298
  with Mailer() as m:
@@ -288,7 +303,7 @@ class TestMailerContextManager:
288
303
  @patch("smtplib.SMTP")
289
304
  def test_close_is_idempotent(self, mock_smtp_cls, minimal_conf):
290
305
  """Calling close() twice must not raise."""
291
- mock_smtp_cls.return_value = MagicMock()
306
+ mock_smtp_cls.return_value = create_autospec(_REAL_SMTP, instance=True)
292
307
  _smtp_conf(minimal_conf)
293
308
  m = Mailer()
294
309
  m.close()
@@ -297,7 +312,7 @@ class TestMailerContextManager:
297
312
  @patch("smtplib.SMTP")
298
313
  def test_close_calls_quit_on_smtpconn(self, mock_smtp_cls, minimal_conf):
299
314
  """close() should call smtpconn.quit() when a connection is open."""
300
- mock_conn = MagicMock()
315
+ mock_conn = create_autospec(_REAL_SMTP, instance=True)
301
316
  mock_smtp_cls.return_value = mock_conn
302
317
  _smtp_conf(minimal_conf)
303
318
  m = Mailer()
@@ -307,7 +322,7 @@ class TestMailerContextManager:
307
322
  @patch("smtplib.SMTP")
308
323
  def test_close_survives_quit_raising(self, mock_smtp_cls, minimal_conf):
309
324
  """close() must not propagate exceptions from smtpconn.quit()."""
310
- mock_conn = MagicMock()
325
+ mock_conn = create_autospec(_REAL_SMTP, instance=True)
311
326
  mock_conn.quit.side_effect = OSError("connection already closed")
312
327
  mock_smtp_cls.return_value = mock_conn
313
328
  _smtp_conf(minimal_conf)
@@ -323,7 +338,7 @@ class TestMailerContextManager:
323
338
  @patch("smtplib.SMTP")
324
339
  def test_del_does_not_raise_on_normal_instance(self, mock_smtp_cls, minimal_conf):
325
340
  """__del__ on a fully initialised (and already closed) Mailer must not raise."""
326
- mock_smtp_cls.return_value = MagicMock()
341
+ mock_smtp_cls.return_value = create_autospec(_REAL_SMTP, instance=True)
327
342
  _smtp_conf(minimal_conf)
328
343
  m = Mailer()
329
344
  m.close()
@@ -332,7 +347,159 @@ class TestMailerContextManager:
332
347
  @patch("smtplib.SMTP")
333
348
  def test_context_manager_exit_suppresses_no_exceptions(self, mock_smtp_cls, minimal_conf):
334
349
  """__exit__ returns None, so exceptions inside the block propagate normally."""
335
- mock_smtp_cls.return_value = MagicMock()
350
+ mock_smtp_cls.return_value = create_autospec(_REAL_SMTP, instance=True)
336
351
  _smtp_conf(minimal_conf)
337
352
  with pytest.raises(ValueError, match="deliberate"), Mailer():
338
353
  raise ValueError("deliberate")
354
+
355
+
356
+ # ---------------------------------------------------------------------------
357
+ # Mailer — against a real in-process SMTP server
358
+ #
359
+ # No mock can catch a call to a method smtplib does not have, or a reply the
360
+ # protocol does not allow. These tests drive Mailer over a loopback socket
361
+ # against a minimal SMTP responder, so the whole connect/EHLO/MAIL/RCPT/DATA
362
+ # path is exercised as the stdlib actually implements it.
363
+ # ---------------------------------------------------------------------------
364
+
365
+
366
+ class _CapturingSMTPHandler(socketserver.StreamRequestHandler):
367
+ """Minimal SMTP responder: enough of RFC 5321 for smtplib to send a message."""
368
+
369
+ def handle(self) -> None:
370
+ self._reply("220 localhost execsql test server")
371
+ envelope: dict[str, object] = {"rcpt": []}
372
+ while True:
373
+ line = self.rfile.readline()
374
+ if not line:
375
+ return
376
+ command = line.decode("ascii", "replace").rstrip("\r\n")
377
+ verb = command.split(None, 1)[0].upper() if command else ""
378
+ if verb == "EHLO":
379
+ self._reply("250-localhost\r\n250-8BITMIME\r\n250 HELP")
380
+ elif verb == "HELO":
381
+ self._reply("250 localhost")
382
+ elif verb == "MAIL":
383
+ envelope["mail_from"] = command
384
+ self._reply("250 OK")
385
+ elif verb == "RCPT":
386
+ cast(list, envelope["rcpt"]).append(command)
387
+ self._reply("250 OK")
388
+ elif verb == "DATA":
389
+ self._reply("354 End data with <CR><LF>.<CR><LF>")
390
+ envelope["data"] = self._read_data()
391
+ self.server.received.append(envelope) # type: ignore[attr-defined]
392
+ self._reply("250 OK")
393
+ elif verb == "QUIT":
394
+ self._reply("221 Bye")
395
+ return
396
+ elif verb == "RSET":
397
+ self._reply("250 OK")
398
+ else:
399
+ self._reply("502 Command not implemented")
400
+
401
+ def _reply(self, text: str) -> None:
402
+ self.wfile.write(text.encode("ascii") + b"\r\n")
403
+ self.wfile.flush()
404
+
405
+ def _read_data(self) -> str:
406
+ lines: list[str] = []
407
+ while True:
408
+ line = self.rfile.readline()
409
+ if not line or line in (b".\r\n", b".\n"):
410
+ break
411
+ decoded = line.decode("utf-8", "replace").rstrip("\r\n")
412
+ # Undo dot-stuffing.
413
+ lines.append(decoded[1:] if decoded.startswith("..") else decoded)
414
+ return "\n".join(lines)
415
+
416
+
417
+ class _SMTPTestServer(socketserver.ThreadingTCPServer):
418
+ allow_reuse_address = True
419
+ daemon_threads = True
420
+
421
+ def __init__(self) -> None:
422
+ super().__init__(("127.0.0.1", 0), _CapturingSMTPHandler)
423
+ self.received: list[dict[str, object]] = []
424
+
425
+
426
+ @pytest.fixture
427
+ def smtp_server(minimal_conf):
428
+ """A loopback SMTP server, with _state.conf pointed at it."""
429
+ server = _SMTPTestServer()
430
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
431
+ thread.start()
432
+ _state.conf.smtp_host, _state.conf.smtp_port = server.server_address[:2]
433
+ _state.conf.smtp_ssl = False
434
+ _state.conf.smtp_tls = False
435
+ _state.conf.smtp_username = None
436
+ _state.conf.smtp_password = None
437
+ _state.conf.email_format = "text"
438
+ _state.conf.email_css = None
439
+ try:
440
+ yield server
441
+ finally:
442
+ server.shutdown()
443
+ server.server_close()
444
+ thread.join(timeout=5)
445
+
446
+
447
+ class TestMailerAgainstRealServer:
448
+ def test_plain_text_message_round_trip(self, smtp_server):
449
+ with Mailer() as m:
450
+ m.sendmail("from@a.com", "to@b.com", "Round trip", "Body text")
451
+ assert len(smtp_server.received) == 1
452
+ envelope = smtp_server.received[0]
453
+ assert "from@a.com" in cast(str, envelope["mail_from"])
454
+ assert any("to@b.com" in r for r in cast(list, envelope["rcpt"]))
455
+ data = cast(str, envelope["data"])
456
+ assert "Subject: Round trip" in data
457
+ assert "Body text" in data
458
+
459
+ def test_multiple_recipients_each_get_an_rcpt(self, smtp_server):
460
+ with Mailer() as m:
461
+ m.sendmail("from@a.com", "b@x.com;c@x.com,d@x.com", "Sub", "Body")
462
+ rcpt = cast(list, smtp_server.received[0]["rcpt"])
463
+ assert len(rcpt) == 3
464
+
465
+ def test_content_file_and_attachment(self, smtp_server, tmp_path):
466
+ content_file = tmp_path / "body.txt"
467
+ content_file.write_text("Appended body line")
468
+ attach_file = tmp_path / "data.csv"
469
+ attach_file.write_bytes(b"col1,col2\n1,2\n")
470
+ with Mailer() as m:
471
+ m.sendmail(
472
+ "from@a.com",
473
+ "to@b.com",
474
+ "With files",
475
+ "Body",
476
+ content_filename=str(content_file),
477
+ attach_filename=str(attach_file),
478
+ )
479
+ data = cast(str, smtp_server.received[0]["data"])
480
+ assert "Appended body line" in data
481
+ assert 'filename="data.csv"' in data
482
+ # The attachment is base64-encoded, so the raw bytes must not appear.
483
+ assert base64.b64encode(b"col1,col2\n1,2\n").decode() in data.replace("\n", "")
484
+
485
+ def test_html_format_message(self, smtp_server):
486
+ _state.conf.email_format = "html"
487
+ _state.conf.email_css = "body { color: red; }"
488
+ with Mailer() as m:
489
+ m.sendmail("from@a.com", "to@b.com", "HTML", "<p>Hello</p>")
490
+ data = cast(str, smtp_server.received[0]["data"])
491
+ assert "Content-Type: text/html" in data
492
+
493
+ def test_unreachable_host_raises_oserror(self, minimal_conf):
494
+ """A closed port must surface as a connection error, not hang."""
495
+ with socket.socket() as probe:
496
+ probe.bind(("127.0.0.1", 0))
497
+ dead_port = probe.getsockname()[1]
498
+ _state.conf.smtp_host = "127.0.0.1"
499
+ _state.conf.smtp_port = dead_port
500
+ _state.conf.smtp_ssl = False
501
+ _state.conf.smtp_tls = False
502
+ _state.conf.smtp_username = None
503
+ _state.conf.smtp_password = None
504
+ with pytest.raises(OSError):
505
+ Mailer()
@@ -722,7 +722,7 @@ wheels = [
722
722
 
723
723
  [[package]]
724
724
  name = "execsql2"
725
- version = "2.22.8"
725
+ version = "2.22.9"
726
726
  source = { editable = "." }
727
727
  dependencies = [
728
728
  { name = "python-dateutil" },
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