execsql2 2.6.0__tar.gz → 2.7.1__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 (285) hide show
  1. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/project_context.md +4 -4
  2. {execsql2-2.6.0 → execsql2-2.7.1}/CHANGELOG.md +18 -0
  3. {execsql2-2.6.0 → execsql2-2.7.1}/PKG-INFO +5 -2
  4. {execsql2-2.6.0 → execsql2-2.7.1}/README.md +1 -1
  5. {execsql2-2.6.0 → execsql2-2.7.1}/docs/about/divergence.md +7 -4
  6. {execsql2-2.6.0 → execsql2-2.7.1}/docs/reference/metacommands.md +16 -1
  7. {execsql2-2.6.0 → execsql2-2.7.1}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +1 -1
  8. {execsql2-2.6.0 → execsql2-2.7.1}/pyproject.toml +3 -3
  9. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/__init__.py +3 -3
  10. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/delimited.py +2 -2
  11. execsql2-2.7.1/src/execsql/exporters/markdown.py +126 -0
  12. execsql2-2.7.1/src/execsql/exporters/xlsx.py +317 -0
  13. execsql2-2.7.1/src/execsql/exporters/yaml.py +87 -0
  14. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/__init__.py +23 -2
  15. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/dispatch.py +11 -0
  16. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/io.py +2 -0
  17. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/io_export.py +75 -0
  18. {execsql2-2.6.0 → execsql2-2.7.1}/tests/conftest.py +2 -0
  19. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_delimited.py +2 -8
  20. execsql2-2.7.1/tests/exporters/test_markdown.py +302 -0
  21. execsql2-2.7.1/tests/exporters/test_xlsx.py +419 -0
  22. execsql2-2.7.1/tests/exporters/test_yaml.py +140 -0
  23. {execsql2-2.6.0 → execsql2-2.7.1}/uv.lock +5 -1
  24. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/agents/dba.md +0 -0
  25. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/agents/herald.md +0 -0
  26. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/agents/inspector.md +0 -0
  27. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/agents/oracle.md +0 -0
  28. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/agents/patcher.md +0 -0
  29. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/agents/qa.md +0 -0
  30. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/agents/scribe.md +0 -0
  31. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/commands/code-oracle.md +0 -0
  32. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/commands/migrate.md +0 -0
  33. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/commands/review-changes.md +0 -0
  34. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/commands/test-module.md +0 -0
  35. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/commands/update-changelog.md +0 -0
  36. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/commands/where-is.md +0 -0
  37. {execsql2-2.6.0 → execsql2-2.7.1}/.claude/state/status.md +0 -0
  38. {execsql2-2.6.0 → execsql2-2.7.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  39. {execsql2-2.6.0 → execsql2-2.7.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  40. {execsql2-2.6.0 → execsql2-2.7.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  41. {execsql2-2.6.0 → execsql2-2.7.1}/.github/workflows/ci-cd.yml +0 -0
  42. {execsql2-2.6.0 → execsql2-2.7.1}/.gitignore +0 -0
  43. {execsql2-2.6.0 → execsql2-2.7.1}/.pre-commit-config.yaml +0 -0
  44. {execsql2-2.6.0 → execsql2-2.7.1}/.pre-commit-hooks.yaml +0 -0
  45. {execsql2-2.6.0 → execsql2-2.7.1}/.python-version +0 -0
  46. {execsql2-2.6.0 → execsql2-2.7.1}/.readthedocs.yaml +0 -0
  47. {execsql2-2.6.0 → execsql2-2.7.1}/CLAUDE.md +0 -0
  48. {execsql2-2.6.0 → execsql2-2.7.1}/CONTRIBUTING.md +0 -0
  49. {execsql2-2.6.0 → execsql2-2.7.1}/LICENSE.txt +0 -0
  50. {execsql2-2.6.0 → execsql2-2.7.1}/NOTICE +0 -0
  51. {execsql2-2.6.0 → execsql2-2.7.1}/SECURITY.md +0 -0
  52. {execsql2-2.6.0 → execsql2-2.7.1}/docs/about/contributors.md +0 -0
  53. {execsql2-2.6.0 → execsql2-2.7.1}/docs/about/copyright.md +0 -0
  54. {execsql2-2.6.0 → execsql2-2.7.1}/docs/api/cli.md +0 -0
  55. {execsql2-2.6.0 → execsql2-2.7.1}/docs/api/db.md +0 -0
  56. {execsql2-2.6.0 → execsql2-2.7.1}/docs/api/exporters.md +0 -0
  57. {execsql2-2.6.0 → execsql2-2.7.1}/docs/api/importers.md +0 -0
  58. {execsql2-2.6.0 → execsql2-2.7.1}/docs/api/index.md +0 -0
  59. {execsql2-2.6.0 → execsql2-2.7.1}/docs/api/metacommands.md +0 -0
  60. {execsql2-2.6.0 → execsql2-2.7.1}/docs/dev/adding_db_adapters.md +0 -0
  61. {execsql2-2.6.0 → execsql2-2.7.1}/docs/dev/adding_exporters.md +0 -0
  62. {execsql2-2.6.0 → execsql2-2.7.1}/docs/dev/adding_importers.md +0 -0
  63. {execsql2-2.6.0 → execsql2-2.7.1}/docs/dev/adding_metacommands.md +0 -0
  64. {execsql2-2.6.0 → execsql2-2.7.1}/docs/dev/architecture.md +0 -0
  65. {execsql2-2.6.0 → execsql2-2.7.1}/docs/getting-started/installation.md +0 -0
  66. {execsql2-2.6.0 → execsql2-2.7.1}/docs/getting-started/requirements.md +0 -0
  67. {execsql2-2.6.0 → execsql2-2.7.1}/docs/getting-started/syntax.md +0 -0
  68. {execsql2-2.6.0 → execsql2-2.7.1}/docs/guides/debugging.md +0 -0
  69. {execsql2-2.6.0 → execsql2-2.7.1}/docs/guides/documentation.md +0 -0
  70. {execsql2-2.6.0 → execsql2-2.7.1}/docs/guides/encoding.md +0 -0
  71. {execsql2-2.6.0 → execsql2-2.7.1}/docs/guides/examples.md +0 -0
  72. {execsql2-2.6.0 → execsql2-2.7.1}/docs/guides/formatter.md +0 -0
  73. {execsql2-2.6.0 → execsql2-2.7.1}/docs/guides/logging.md +0 -0
  74. {execsql2-2.6.0 → execsql2-2.7.1}/docs/guides/sql_syntax.md +0 -0
  75. {execsql2-2.6.0 → execsql2-2.7.1}/docs/guides/usage.md +0 -0
  76. {execsql2-2.6.0 → execsql2-2.7.1}/docs/guides/using_scripts.md +0 -0
  77. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/Compare_planets.png +0 -0
  78. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/actions.png +0 -0
  79. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/actions2.png +0 -0
  80. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/checkboxes.png +0 -0
  81. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/connect.b64 +0 -0
  82. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/connect.png +0 -0
  83. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/create_conf.png +0 -0
  84. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/data_error1_screenshot.jpg +0 -0
  85. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/entry_form.png +0 -0
  86. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/execsql_console.png +0 -0
  87. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/execsql_logo_01.png +0 -0
  88. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/fatals.png +0 -0
  89. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/logo_small.png +0 -0
  90. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/pause_terminal.png +0 -0
  91. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/pause_terminal_sm.b64 +0 -0
  92. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/pause_terminal_sm.png +0 -0
  93. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/prompt_compare.png +0 -0
  94. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/set_build_commands.jpg +0 -0
  95. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/unit_conversions.b64 +0 -0
  96. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/unit_conversions_029.png +0 -0
  97. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/unmatched.png +0 -0
  98. {execsql2-2.6.0 → execsql2-2.7.1}/docs/images/vim_execsql_highlight.png +0 -0
  99. {execsql2-2.6.0 → execsql2-2.7.1}/docs/index.md +0 -0
  100. {execsql2-2.6.0 → execsql2-2.7.1}/docs/reference/configuration.md +0 -0
  101. {execsql2-2.6.0 → execsql2-2.7.1}/docs/reference/security.md +0 -0
  102. {execsql2-2.6.0 → execsql2-2.7.1}/docs/reference/substitution_vars.md +0 -0
  103. {execsql2-2.6.0 → execsql2-2.7.1}/extras/vscode-execsql/README.md +0 -0
  104. {execsql2-2.6.0 → execsql2-2.7.1}/extras/vscode-execsql/package.json +0 -0
  105. {execsql2-2.6.0 → execsql2-2.7.1}/justfile +0 -0
  106. {execsql2-2.6.0 → execsql2-2.7.1}/scripts/generate_vscode_grammar.py +0 -0
  107. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/__init__.py +0 -0
  108. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/__main__.py +0 -0
  109. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/cli/__init__.py +0 -0
  110. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/cli/dsn.py +0 -0
  111. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/cli/help.py +0 -0
  112. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/cli/run.py +0 -0
  113. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/config.py +0 -0
  114. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/constants.py +0 -0
  115. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/__init__.py +0 -0
  116. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/access.py +0 -0
  117. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/base.py +0 -0
  118. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/dsn.py +0 -0
  119. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/duckdb.py +0 -0
  120. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/factory.py +0 -0
  121. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/firebird.py +0 -0
  122. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/mysql.py +0 -0
  123. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/oracle.py +0 -0
  124. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/postgres.py +0 -0
  125. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/sqlite.py +0 -0
  126. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/db/sqlserver.py +0 -0
  127. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exceptions.py +0 -0
  128. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/base.py +0 -0
  129. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/duckdb.py +0 -0
  130. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/feather.py +0 -0
  131. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/html.py +0 -0
  132. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/json.py +0 -0
  133. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/latex.py +0 -0
  134. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/ods.py +0 -0
  135. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/parquet.py +0 -0
  136. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/pretty.py +0 -0
  137. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/protocol.py +0 -0
  138. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/raw.py +0 -0
  139. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/sqlite.py +0 -0
  140. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/templates.py +0 -0
  141. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/values.py +0 -0
  142. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/xls.py +0 -0
  143. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/xml.py +0 -0
  144. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/exporters/zip.py +0 -0
  145. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/format.py +0 -0
  146. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/gui/__init__.py +0 -0
  147. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/gui/base.py +0 -0
  148. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/gui/console.py +0 -0
  149. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/gui/desktop.py +0 -0
  150. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/gui/tui.py +0 -0
  151. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/importers/__init__.py +0 -0
  152. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/importers/base.py +0 -0
  153. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/importers/csv.py +0 -0
  154. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/importers/feather.py +0 -0
  155. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/importers/ods.py +0 -0
  156. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/importers/xls.py +0 -0
  157. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/conditions.py +0 -0
  158. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/connect.py +0 -0
  159. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/control.py +0 -0
  160. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/data.py +0 -0
  161. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/debug.py +0 -0
  162. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/io_fileops.py +0 -0
  163. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/io_import.py +0 -0
  164. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/io_write.py +0 -0
  165. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/prompt.py +0 -0
  166. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/script_ext.py +0 -0
  167. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/metacommands/system.py +0 -0
  168. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/models.py +0 -0
  169. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/parser.py +0 -0
  170. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/py.typed +0 -0
  171. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/script/__init__.py +0 -0
  172. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/script/control.py +0 -0
  173. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/script/engine.py +0 -0
  174. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/script/variables.py +0 -0
  175. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/state.py +0 -0
  176. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/types.py +0 -0
  177. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/__init__.py +0 -0
  178. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/auth.py +0 -0
  179. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/crypto.py +0 -0
  180. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/datetime.py +0 -0
  181. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/errors.py +0 -0
  182. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/fileio.py +0 -0
  183. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/gui.py +0 -0
  184. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/mail.py +0 -0
  185. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/numeric.py +0 -0
  186. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/regex.py +0 -0
  187. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/strings.py +0 -0
  188. {execsql2-2.6.0 → execsql2-2.7.1}/src/execsql/utils/timer.py +0 -0
  189. {execsql2-2.6.0 → execsql2-2.7.1}/templates/README.md +0 -0
  190. {execsql2-2.6.0 → execsql2-2.7.1}/templates/config_settings.sqlite +0 -0
  191. {execsql2-2.6.0 → execsql2-2.7.1}/templates/example_config_prompt.sql +0 -0
  192. {execsql2-2.6.0 → execsql2-2.7.1}/templates/execsql.conf +0 -0
  193. {execsql2-2.6.0 → execsql2-2.7.1}/templates/make_config_db.sql +0 -0
  194. {execsql2-2.6.0 → execsql2-2.7.1}/templates/md_compare.sql +0 -0
  195. {execsql2-2.6.0 → execsql2-2.7.1}/templates/md_glossary.sql +0 -0
  196. {execsql2-2.6.0 → execsql2-2.7.1}/templates/md_upsert.sql +0 -0
  197. {execsql2-2.6.0 → execsql2-2.7.1}/templates/pg_compare.sql +0 -0
  198. {execsql2-2.6.0 → execsql2-2.7.1}/templates/pg_glossary.sql +0 -0
  199. {execsql2-2.6.0 → execsql2-2.7.1}/templates/pg_upsert.sql +0 -0
  200. {execsql2-2.6.0 → execsql2-2.7.1}/templates/script_template.sql +0 -0
  201. {execsql2-2.6.0 → execsql2-2.7.1}/templates/ss_compare.sql +0 -0
  202. {execsql2-2.6.0 → execsql2-2.7.1}/templates/ss_glossary.sql +0 -0
  203. {execsql2-2.6.0 → execsql2-2.7.1}/templates/ss_upsert.sql +0 -0
  204. {execsql2-2.6.0 → execsql2-2.7.1}/tests/__init__.py +0 -0
  205. {execsql2-2.6.0 → execsql2-2.7.1}/tests/cli/__init__.py +0 -0
  206. {execsql2-2.6.0 → execsql2-2.7.1}/tests/cli/test_cli.py +0 -0
  207. {execsql2-2.6.0 → execsql2-2.7.1}/tests/cli/test_cli_e2e.py +0 -0
  208. {execsql2-2.6.0 → execsql2-2.7.1}/tests/cli/test_cli_run.py +0 -0
  209. {execsql2-2.6.0 → execsql2-2.7.1}/tests/db/__init__.py +0 -0
  210. {execsql2-2.6.0 → execsql2-2.7.1}/tests/db/test_base.py +0 -0
  211. {execsql2-2.6.0 → execsql2-2.7.1}/tests/db/test_duckdb.py +0 -0
  212. {execsql2-2.6.0 → execsql2-2.7.1}/tests/db/test_factory.py +0 -0
  213. {execsql2-2.6.0 → execsql2-2.7.1}/tests/db/test_postgres.py +0 -0
  214. {execsql2-2.6.0 → execsql2-2.7.1}/tests/db/test_sqlite.py +0 -0
  215. {execsql2-2.6.0 → execsql2-2.7.1}/tests/db/test_sqlite_extra.py +0 -0
  216. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/__init__.py +0 -0
  217. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_base.py +0 -0
  218. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_db.py +0 -0
  219. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_duckdb_exporter.py +0 -0
  220. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_exporters.py +0 -0
  221. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_feather.py +0 -0
  222. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_html_latex.py +0 -0
  223. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_json.py +0 -0
  224. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_ods.py +0 -0
  225. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_parquet.py +0 -0
  226. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_sqlite_exporter.py +0 -0
  227. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_templates.py +0 -0
  228. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_xls_xlsx.py +0 -0
  229. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_xml.py +0 -0
  230. {execsql2-2.6.0 → execsql2-2.7.1}/tests/exporters/test_zip.py +0 -0
  231. {execsql2-2.6.0 → execsql2-2.7.1}/tests/gui/__init__.py +0 -0
  232. {execsql2-2.6.0 → execsql2-2.7.1}/tests/gui/test_backends.py +0 -0
  233. {execsql2-2.6.0 → execsql2-2.7.1}/tests/importers/__init__.py +0 -0
  234. {execsql2-2.6.0 → execsql2-2.7.1}/tests/importers/test_csv_importer.py +0 -0
  235. {execsql2-2.6.0 → execsql2-2.7.1}/tests/importers/test_feather_importer.py +0 -0
  236. {execsql2-2.6.0 → execsql2-2.7.1}/tests/importers/test_ods_importer.py +0 -0
  237. {execsql2-2.6.0 → execsql2-2.7.1}/tests/importers/test_xls_importer.py +0 -0
  238. {execsql2-2.6.0 → execsql2-2.7.1}/tests/integration/__init__.py +0 -0
  239. {execsql2-2.6.0 → execsql2-2.7.1}/tests/integration/conftest.py +0 -0
  240. {execsql2-2.6.0 → execsql2-2.7.1}/tests/integration/test_dsn.py +0 -0
  241. {execsql2-2.6.0 → execsql2-2.7.1}/tests/integration/test_duckdb.py +0 -0
  242. {execsql2-2.6.0 → execsql2-2.7.1}/tests/integration/test_mysql.py +0 -0
  243. {execsql2-2.6.0 → execsql2-2.7.1}/tests/integration/test_postgres.py +0 -0
  244. {execsql2-2.6.0 → execsql2-2.7.1}/tests/integration/test_sqlite.py +0 -0
  245. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/__init__.py +0 -0
  246. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/test_connect.py +0 -0
  247. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/test_metacommands.py +0 -0
  248. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/test_metacommands_connect.py +0 -0
  249. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/test_metacommands_data.py +0 -0
  250. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/test_metacommands_extended.py +0 -0
  251. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  252. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/test_metacommands_io.py +0 -0
  253. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  254. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  255. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/test_metacommands_system.py +0 -0
  256. {execsql2-2.6.0 → execsql2-2.7.1}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  257. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_config.py +0 -0
  258. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_config_data.py +0 -0
  259. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_constants.py +0 -0
  260. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_engine.py +0 -0
  261. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_exceptions.py +0 -0
  262. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_format.py +0 -0
  263. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_mail.py +0 -0
  264. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_models.py +0 -0
  265. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_package.py +0 -0
  266. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_parser.py +0 -0
  267. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_registry.py +0 -0
  268. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_script.py +0 -0
  269. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_state.py +0 -0
  270. {execsql2-2.6.0 → execsql2-2.7.1}/tests/test_types.py +0 -0
  271. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/__init__.py +0 -0
  272. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_auth.py +0 -0
  273. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_auth_extra.py +0 -0
  274. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_crypto.py +0 -0
  275. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_datetime.py +0 -0
  276. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_errors.py +0 -0
  277. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_errors_extra.py +0 -0
  278. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_fileio.py +0 -0
  279. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_fileio_extra.py +0 -0
  280. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_numeric.py +0 -0
  281. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_regex.py +0 -0
  282. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_strings.py +0 -0
  283. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_timer.py +0 -0
  284. {execsql2-2.6.0 → execsql2-2.7.1}/tests/utils/test_timer_extra.py +0 -0
  285. {execsql2-2.6.0 → execsql2-2.7.1}/zensical.toml +0 -0
@@ -265,10 +265,10 @@ ______________________________________________________________________
265
265
 
266
266
  ### v2.7 — New Export/Import Formats
267
267
 
268
- - [ ] **Parquet import** — complement existing Parquet export and Feather import. Natural fit with DuckDB support.
269
- - [ ] **YAML export** — popular for config-generation workflows; rounds out the format matrix.
270
- - [ ] **Markdown (GFM) export** — GitHub-flavored markdown tables. Lightweight and useful for docs/reports.
271
- - [ ] **Excel (XLSX) multi-sheet export** — multiple queries multiple named sheets in one workbook, with basic formatting.
268
+ - [x] **Parquet import** — already existed as `IMPORT TO table FROM PARQUET file` (verified present).
269
+ - [x] **YAML export** — `FORMAT YAML` via PyYAML, list-of-dicts with native type preservation.
270
+ - [x] **Markdown (GFM) export** — `FORMAT MARKDOWN` / `MD`, pipe tables with alignment and escaping.
271
+ - [x] **Excel (XLSX) multi-sheet export** — `FORMAT XLSX` single + multi-sheet via openpyxl, bold headers, inventory sheet, sheet name deduplication.
272
272
 
273
273
  ### v2.8 — Scripting Power Features
274
274
 
@@ -13,6 +13,24 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.7.1] - 2026-04-01
17
+
18
+ ### Fixed
19
+
20
+ - Fix `AttributeError: module 'execsql.state' has no attribute 'dedup_words'` when importing CSV files with `DEDUP_COL_HDRS` enabled — `dedup_words` is now correctly imported from `execsql.utils.strings` instead of accessed through the state module.
21
+
22
+ ______________________________________________________________________
23
+
24
+ ## [2.7.0] - 2026-04-01
25
+
26
+ ### Added
27
+
28
+ - **Markdown export** (`FORMAT MARKDOWN` / `FORMAT MD`) — GitHub-flavored pipe tables with column alignment, pipe/backslash escaping, and zip support. No dependencies required.
29
+ - **YAML export** (`FORMAT YAML`) — list-of-dicts output via PyYAML with native type preservation (int, float, null). Requires `PyYAML` (included in `formats` extras).
30
+ - **XLSX export** (`FORMAT XLSX`) — single-sheet and multi-sheet Excel export via openpyxl with bold headers, native type preservation, sheet name deduplication, and a "Datasheets" inventory sheet. Multi-sheet syntax: `EXPORT table1, table2 TO file.xlsx AS XLSX`.
31
+
32
+ ______________________________________________________________________
33
+
16
34
  ## [2.6.0] - 2026-04-01
17
35
 
18
36
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.6.0
3
+ Version: 2.7.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: Repository, https://github.com/geocoug/execsql
6
6
  Project-URL: Issues, https://github.com/geocoug/execsql/issues
@@ -54,6 +54,7 @@ Requires-Dist: polars; extra == 'all'
54
54
  Requires-Dist: psycopg2-binary; extra == 'all'
55
55
  Requires-Dist: pymysql; extra == 'all'
56
56
  Requires-Dist: pyodbc; extra == 'all'
57
+ Requires-Dist: pyyaml; extra == 'all'
57
58
  Requires-Dist: tables; extra == 'all'
58
59
  Requires-Dist: xlrd; extra == 'all'
59
60
  Provides-Extra: all-db
@@ -76,6 +77,7 @@ Requires-Dist: openpyxl; extra == 'dev'
76
77
  Requires-Dist: polars; extra == 'dev'
77
78
  Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
78
79
  Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
80
+ Requires-Dist: pyyaml; extra == 'dev'
79
81
  Requires-Dist: ruff>=0.4; extra == 'dev'
80
82
  Requires-Dist: tables; extra == 'dev'
81
83
  Requires-Dist: tox-uv>=1.13.1; extra == 'dev'
@@ -91,6 +93,7 @@ Requires-Dist: jinja2; extra == 'formats'
91
93
  Requires-Dist: odfpy; extra == 'formats'
92
94
  Requires-Dist: openpyxl; extra == 'formats'
93
95
  Requires-Dist: polars; extra == 'formats'
96
+ Requires-Dist: pyyaml; extra == 'formats'
94
97
  Requires-Dist: tables; extra == 'formats'
95
98
  Requires-Dist: xlrd; extra == 'formats'
96
99
  Provides-Extra: mssql
@@ -227,7 +230,7 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
227
230
  # Features
228
231
 
229
232
  - Import data from CSV, TSV, Excel, OpenDocument, Feather, or Parquet files into a database table.
230
- - Export query results in 15+ formats including CSV, TSV, JSON, XML, HTML, LaTeX, OpenDocument, Feather, HDF5, DuckDB, SQLite, plain text, and Jinja2 templates.
233
+ - 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.
231
234
  - Copy data between databases, including across different DBMS types.
232
235
  - Conditionally execute SQL and metacommands using `IF`/`ELSE`/`ENDIF` based on data values, DBMS type, or user input.
233
236
  - Loop over blocks of SQL and metacommands using `LOOP`/`ENDLOOP`.
@@ -120,7 +120,7 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
120
120
  # Features
121
121
 
122
122
  - Import data from CSV, TSV, Excel, OpenDocument, Feather, or Parquet files into a database table.
123
- - Export query results in 15+ formats including CSV, TSV, JSON, XML, HTML, LaTeX, OpenDocument, Feather, HDF5, DuckDB, SQLite, plain text, and Jinja2 templates.
123
+ - 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.
124
124
  - Copy data between databases, including across different DBMS types.
125
125
  - Conditionally execute SQL and metacommands using `IF`/`ELSE`/`ENDIF` based on data values, DBMS type, or user input.
126
126
  - Loop over blocks of SQL and metacommands using `LOOP`/`ENDLOOP`.
@@ -26,10 +26,13 @@ ______________________________________________________________________
26
26
 
27
27
  ### Export Formats
28
28
 
29
- | Format | Description |
30
- | --------- | ------------------------------------------------------------------------------- |
31
- | `PARQUET` | Export query or table results to Apache Parquet via `polars`. |
32
- | `FEATHER` | Export to Apache Feather/IPC via `polars` + `pyarrow` (upstream used `pandas`). |
29
+ | Format | Description |
30
+ | ----------------- | -------------------------------------------------------------------------------------------------------------------- |
31
+ | `PARQUET` | Export query or table results to Apache Parquet via `polars`. |
32
+ | `FEATHER` | Export to Apache Feather/IPC via `polars` + `pyarrow` (upstream used `pandas`). |
33
+ | `YAML` | Export query or table results as a YAML sequence of mappings via `PyYAML`. |
34
+ | `MARKDOWN` / `MD` | Export query or table results as a GitHub-Flavored Markdown (GFM) pipe table. Pure Python, no optional dependencies. |
35
+ | `XLSX` | Export query or table results to an Excel XLSX workbook via `openpyxl` (single or multi-sheet). |
33
36
 
34
37
  ### Metacommands
35
38
 
@@ -932,7 +932,22 @@ JSON_TS or JSON_TABLESCHEMA
932
932
 
933
933
  LATEX
934
934
 
935
- : Input for the [LaTeΧ](https://www.latex-project.org/) typesetting system. If the "APPEND" keyword is not used, a complete document (of class article) will be written. If the "APPEND" keyword is used, only the table definition will be written to the output file. If the "APPEND" keyword is used and an existing output file contains an \\end directive, the table will be written before that directive rather than at the physical end of the file. Wide or long tables may exceed LaTeΧ's default page size. If the "DESCRIPTION" keyword is used, the given description will be used as the table's caption. Data exported in LaTeX format cannot be written into a zipfile.
935
+ : Input for the [LaTeX](https://www.latex-project.org/) typesetting system. If the "APPEND" keyword is not used, a complete document (of class article) will be written. If the "APPEND" keyword is used, only the table definition will be written to the output file. If the "APPEND" keyword is used and an existing output file contains an \\end directive, the table will be written before that directive rather than at the physical end of the file. Wide or long tables may exceed LaTeΧ's default page size. If the "DESCRIPTION" keyword is used, the given description will be used as the table's caption. Data exported in LaTeX format cannot be written into a zipfile.
936
+
937
+
938
+ MARKDOWN or MD
939
+
940
+ : [GitHub-Flavored Markdown](https://github.github.com/gfm/) pipe table. Column values are aligned and pipe (`|`) and backslash (`\`) characters in data are escaped. If the "DESCRIPTION" keyword is used, the description is written as an HTML comment (`<!-- ... -->`) before the table. If the "APPEND" keyword is used, only the table is appended (no repeated headers). No optional dependencies required.
941
+
942
+
943
+ XLSX
944
+
945
+ : [Excel](https://www.microsoft.com/en-us/microsoft-365/excel) workbook in the Office Open XML format. One or more tables (or views) can be exported to an XLSX workbook. Each table will be exported to a separate worksheet within the workbook, with the first row containing bold column headers. To export multiple tables, their names must be separated by commas. The "APPEND" keyword can be used to add worksheets to an existing workbook. The name of the view or table exported will be used as the worksheet name; if this conflicts with a sheet already in the workbook, a number will be appended to make the sheet name unique. A "Datasheets" inventory sheet is created with author, date, description, and source information for each data sheet. Data types are preserved natively (integers, floats, dates, datetimes, booleans). The `openpyxl` library must be installed (`pip install execsql2[excel]`). Data exported in XLSX format cannot be written into a zipfile.
946
+
947
+
948
+ YAML
949
+
950
+ : [YAML](https://yaml.org/) sequence of mappings. Each row is represented as a mapping (dictionary) with column names as keys. Python data types are preserved — integers remain integers, floats remain floats, and `None` becomes YAML `null`. If the "APPEND" keyword is used, a new YAML document is appended to the file (multi-document stream). The `PyYAML` library must be installed (`pip install execsql2[formats]`). No description text is included in the output even if provided.
936
951
 
937
952
 
938
953
  SQLITE
@@ -118,7 +118,7 @@
118
118
  },
119
119
  "export-formats": {
120
120
  "comment": "Export/import format names",
121
- "match": "(?i)\\b(json_tableschema|cgi-html|feather|json_ts|txt-and|unitsep|duckdb|sqlite|values|latex|plain|hdf5|html|json|tabq|tsvq|b64|csv|ods|raw|tab|tsv|txt|xml|us)\\b",
121
+ "match": "(?i)\\b(json_tableschema|cgi-html|markdown|feather|json_ts|txt-and|unitsep|duckdb|sqlite|values|latex|plain|hdf5|html|json|tabq|tsvq|xlsx|yaml|b64|csv|ods|raw|tab|tsv|txt|xml|md|us)\\b",
122
122
  "name": "support.constant.execsql"
123
123
  },
124
124
  "secondary-operators": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.6.0"
7
+ version = "2.7.1"
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" }
@@ -56,7 +56,7 @@ firebird = ["firebird-driver"]
56
56
  oracle = ["oracledb"]
57
57
  odbc = ["pyodbc"]
58
58
  # Feature bundles
59
- formats = ["odfpy", "xlrd", "openpyxl", "Jinja2", "polars", "tables"]
59
+ formats = ["odfpy", "xlrd", "openpyxl", "Jinja2", "polars", "tables", "PyYAML"]
60
60
  auth = ["keyring"]
61
61
  # Convenience groups
62
62
  all-db = [
@@ -158,7 +158,7 @@ skip-magic-trailing-comma = false
158
158
  line-ending = "auto"
159
159
 
160
160
  [tool.bumpversion]
161
- current_version = "2.6.0"
161
+ current_version = "2.7.1"
162
162
  commit = true
163
163
  commit_args = "--no-verify"
164
164
  tag = true
@@ -9,9 +9,9 @@ handlers can access them via ``_state.write_query_to_csv`` etc. without
9
9
  importing directly from here.
10
10
 
11
11
  Sub-modules: ``base``, ``delimited``, ``json``, ``xml``, ``html``,
12
- ``latex``, ``ods``, ``xls``, ``zip``, ``raw``, ``pretty``, ``values``,
13
- ``templates``, ``feather``, ``parquet``, ``duckdb``, ``sqlite``,
14
- ``protocol``.
12
+ ``latex``, ``markdown``, ``ods``, ``xls``, ``zip``, ``raw``, ``pretty``,
13
+ ``values``, ``templates``, ``feather``, ``parquet``, ``duckdb``,
14
+ ``sqlite``, ``protocol``.
15
15
  """
16
16
 
17
17
  from execsql.exporters.protocol import QueryExporter, RowsetExporter
@@ -27,7 +27,7 @@ from execsql.exceptions import ErrInfo
27
27
  from execsql.models import DataTable
28
28
  from execsql.utils.errors import exception_desc
29
29
  from execsql.utils.fileio import filewriter_close
30
- from execsql.utils.strings import clean_words, fold_words
30
+ from execsql.utils.strings import clean_words, dedup_words, fold_words
31
31
 
32
32
  __all__ = ["LineDelimiter", "CsvFile", "CsvWriter", "DelimitedWriter", "write_delimited_file"]
33
33
 
@@ -677,7 +677,7 @@ class CsvFile(EncodedFile):
677
677
  if conf.fold_col_hdrs != "no":
678
678
  colnames = fold_words(colnames, conf.fold_col_hdrs)
679
679
  if conf.dedup_col_hdrs:
680
- colnames = _state.dedup_words(colnames)
680
+ colnames = dedup_words(colnames)
681
681
  return colnames
682
682
 
683
683
  def column_headers(self) -> list[str]:
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ GitHub-Flavored Markdown (GFM) pipe table export for execsql.
5
+
6
+ Provides :func:`write_query_to_markdown`, which serializes a query result
7
+ set as a GFM pipe table suitable for inclusion in GitHub README files,
8
+ wikis, and any Markdown renderer that supports the pipe-table extension.
9
+
10
+ Example output::
11
+
12
+ | id | name | score |
13
+ |----|--------|-------|
14
+ | 1 | Alice | 95.2 |
15
+ | 2 | Bob | 87.0 |
16
+
17
+ No optional dependencies — pure Python.
18
+ """
19
+
20
+ from typing import Any
21
+
22
+ import execsql.state as _state
23
+ from execsql.exceptions import ErrInfo
24
+ from execsql.exporters.zip import ZipWriter
25
+ from execsql.utils.errors import exception_desc
26
+ from execsql.utils.fileio import filewriter_close
27
+
28
+ __all__ = ["write_query_to_markdown"]
29
+
30
+ _PIPE_ESCAPE = str.maketrans({"|": r"\|", "\\": "\\\\"})
31
+
32
+
33
+ def _cell(value: Any) -> str:
34
+ """Render a single cell value as a Markdown-safe string.
35
+
36
+ Args:
37
+ value: The cell value from the result set. ``None`` is rendered as
38
+ an empty string. Pipe characters are escaped so they do not
39
+ break the table structure.
40
+
41
+ Returns:
42
+ A string safe to embed between pipe characters in a GFM table row.
43
+ """
44
+ if value is None:
45
+ return ""
46
+ return str(value).translate(_PIPE_ESCAPE)
47
+
48
+
49
+ def write_query_to_markdown(
50
+ select_stmt: str,
51
+ db: Any,
52
+ outfile: str,
53
+ append: bool = False,
54
+ desc: str | None = None,
55
+ zipfile: str | None = None,
56
+ ) -> None:
57
+ """Execute *select_stmt* and write the result set as a GFM pipe table.
58
+
59
+ Writes a GitHub-Flavored Markdown pipe table to *outfile* (or into
60
+ *zipfile* when provided). Column widths are derived from the widest
61
+ value in each column (including the header), so the table renders
62
+ legibly in plain-text editors as well as in Markdown renderers.
63
+
64
+ Args:
65
+ select_stmt: SQL SELECT statement to execute.
66
+ db: Database connection object exposing ``select_rowsource()``.
67
+ outfile: Destination file path, or ``"stdout"`` for console output.
68
+ append: When ``True`` open the file in append mode. A blank line
69
+ is written before the table so consecutive appended tables are
70
+ visually separated.
71
+ desc: Optional human-readable description. When provided it is
72
+ written as an HTML comment (``<!-- desc -->``), which is valid
73
+ Markdown and invisible in rendered output.
74
+ zipfile: When set, write into this ZIP archive instead of a plain
75
+ file. *outfile* becomes the entry name inside the archive.
76
+ """
77
+ conf = _state.conf
78
+ try:
79
+ hdrs, rows = db.select_rowsource(select_stmt)
80
+ except ErrInfo:
81
+ raise
82
+ except Exception as e:
83
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
84
+
85
+ # Materialise the full result set so we can compute column widths in one
86
+ # pass before writing. GFM tables require consistent column widths for
87
+ # readability; width computation requires seeing all rows first.
88
+ str_hdrs: list[str] = [_cell(h) for h in hdrs]
89
+ str_rows: list[list[str]] = [[_cell(v) for v in row] for row in rows]
90
+
91
+ # Minimum separator width is 3 dashes (GFM spec minimum for alignment row).
92
+ col_widths: list[int] = [max(3, len(h)) for h in str_hdrs]
93
+ for row in str_rows:
94
+ for i, cell in enumerate(row):
95
+ if len(cell) > col_widths[i]:
96
+ col_widths[i] = len(cell)
97
+
98
+ def _format_row(cells: list[str]) -> str:
99
+ padded = (f" {c:<{col_widths[i]}} " for i, c in enumerate(cells))
100
+ return "|" + "|".join(padded) + "|\n"
101
+
102
+ def _format_separator() -> str:
103
+ dashes = (f" {'-' * col_widths[i]} " for i in range(len(str_hdrs)))
104
+ return "|" + "|".join(dashes) + "|\n"
105
+
106
+ if zipfile is None:
107
+ filewriter_close(outfile)
108
+ from execsql.utils.fileio import EncodedFile
109
+
110
+ ef = EncodedFile(outfile, conf.output_encoding)
111
+ f = ef.open("at" if append else "wt")
112
+ else:
113
+ f = ZipWriter(zipfile, outfile, append)
114
+
115
+ try:
116
+ if append:
117
+ # Blank line separates consecutive tables when appending.
118
+ f.write("\n")
119
+ if desc is not None:
120
+ f.write(f"<!-- {desc} -->\n\n")
121
+ f.write(_format_row(str_hdrs))
122
+ f.write(_format_separator())
123
+ for row in str_rows:
124
+ f.write(_format_row(row))
125
+ finally:
126
+ f.close()
@@ -0,0 +1,317 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ XLSX (Excel Open XML) export for execsql.
5
+
6
+ Provides :func:`write_query_to_xlsx` (single-sheet export) and
7
+ :func:`write_queries_to_xlsx` (multi-sheet export). Requires the
8
+ ``openpyxl`` package (``execsql2[excel]``).
9
+ """
10
+
11
+ import datetime
12
+ import getpass
13
+ import os
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import execsql.state as _state
18
+ from execsql.exceptions import ErrInfo
19
+ from execsql.exporters.base import ExportRecord
20
+ from execsql.exporters.pretty import prettyprint_query
21
+ from execsql.script import current_script_line
22
+ from execsql.utils.errors import exception_desc, fatal_error
23
+ from execsql.utils.fileio import filewriter_close
24
+ from execsql.utils.strings import unquoted
25
+
26
+ __all__ = ["write_query_to_xlsx", "write_queries_to_xlsx"]
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Internal helpers
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ def _require_openpyxl() -> Any:
34
+ """Import and return the openpyxl module, raising a fatal error if absent."""
35
+ try:
36
+ import openpyxl
37
+
38
+ return openpyxl
39
+ except ImportError:
40
+ fatal_error("The openpyxl library is needed to write Excel (.xlsx) spreadsheets (install execsql2[excel]).")
41
+
42
+
43
+ def _cell_value(item: Any) -> Any:
44
+ """Return a value suitable for writing directly to an openpyxl cell.
45
+
46
+ openpyxl natively handles int, float, bool, str, datetime.datetime,
47
+ datetime.date, and None. datetime.time is converted to a string because
48
+ openpyxl does not have a native time-only cell type.
49
+ """
50
+ if item is None:
51
+ return None
52
+ if isinstance(item, bool):
53
+ # bool must be checked before int — bool is a subclass of int.
54
+ return item
55
+ if isinstance(item, int | float):
56
+ return item
57
+ if isinstance(item, datetime.datetime):
58
+ return item
59
+ if isinstance(item, datetime.date):
60
+ return item
61
+ if isinstance(item, datetime.time):
62
+ # openpyxl has no native time-only type; store as HH:MM:SS string.
63
+ return item.strftime("%H:%M:%S")
64
+ return str(item)
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Public API
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ def write_query_to_xlsx(
73
+ select_stmt: str,
74
+ db: Any,
75
+ outfile: str,
76
+ append: bool = False,
77
+ desc: str | None = None,
78
+ sheetname: str | None = None,
79
+ ) -> None:
80
+ """Execute *select_stmt* and write the result to a single worksheet in an XLSX file.
81
+
82
+ Args:
83
+ select_stmt: SQL SELECT statement to execute.
84
+ db: An execsql database adapter with a ``select_rowsource()`` method.
85
+ outfile: Destination ``.xlsx`` file path.
86
+ append: If ``True`` and *outfile* exists, add a new sheet to the
87
+ existing workbook. If ``False``, overwrite any existing file.
88
+ desc: Optional human-readable description stored in the inventory sheet.
89
+ sheetname: Name for the new worksheet. Defaults to ``"Sheet1"``
90
+ (or ``"Sheet2"``, ``"Sheet3"``, etc. when appending to avoid
91
+ name collisions).
92
+ """
93
+ openpyxl = _require_openpyxl()
94
+
95
+ try:
96
+ hdrs, rows = db.select_rowsource(select_stmt)
97
+ except ErrInfo:
98
+ raise
99
+ except Exception as e:
100
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
101
+
102
+ # ------------------------------------------------------------------
103
+ # Determine sheet name and open/create workbook
104
+ # ------------------------------------------------------------------
105
+ if append and Path(outfile).is_file():
106
+ wb = openpyxl.load_workbook(outfile)
107
+ existing_names = wb.sheetnames
108
+ base = sheetname or "Sheet"
109
+ sheet_name = base
110
+ sheet_no = 1
111
+ while sheet_name in existing_names:
112
+ sheet_no += 1
113
+ sheet_name = f"{base}{sheet_no}"
114
+ else:
115
+ sheet_name = sheetname or "Sheet1"
116
+ if Path(outfile).is_file():
117
+ filewriter_close(outfile)
118
+ os.unlink(outfile)
119
+ wb = openpyxl.Workbook()
120
+ # openpyxl creates a default sheet named "Sheet"; remove it so we
121
+ # start with a clean workbook.
122
+ if wb.sheetnames:
123
+ del wb[wb.sheetnames[0]]
124
+
125
+ # ------------------------------------------------------------------
126
+ # Ensure the inventory sheet exists
127
+ # ------------------------------------------------------------------
128
+ inventory_name = "Datasheets"
129
+ if inventory_name not in wb.sheetnames:
130
+ inv_ws = wb.create_sheet(inventory_name)
131
+ bold_font = openpyxl.styles.Font(bold=True)
132
+ for col_idx, hdr in enumerate(
133
+ ("datasheet_name", "created_on", "created_by", "description", "source"),
134
+ start=1,
135
+ ):
136
+ cell = inv_ws.cell(row=1, column=col_idx, value=hdr)
137
+ cell.font = bold_font
138
+ else:
139
+ inv_ws = wb[inventory_name]
140
+
141
+ # ------------------------------------------------------------------
142
+ # Write data to a new sheet
143
+ # ------------------------------------------------------------------
144
+ ws = wb.create_sheet(sheet_name)
145
+ bold_font = openpyxl.styles.Font(bold=True)
146
+
147
+ # Header row
148
+ for col_idx, hdr in enumerate(hdrs, start=1):
149
+ cell = ws.cell(row=1, column=col_idx, value=str(hdr))
150
+ cell.font = bold_font
151
+
152
+ # Data rows
153
+ for row_idx, row in enumerate(rows, start=2):
154
+ for col_idx, item in enumerate(row, start=1):
155
+ ws.cell(row=row_idx, column=col_idx, value=_cell_value(item))
156
+
157
+ # ------------------------------------------------------------------
158
+ # Update inventory sheet
159
+ # ------------------------------------------------------------------
160
+ script, lno = current_script_line()
161
+ src = (
162
+ f"{select_stmt} with database {_state.dbs.current().name()}, "
163
+ f"with script {str(Path(script).resolve())}, line {lno}"
164
+ )
165
+ next_row = inv_ws.max_row + 1
166
+ for col_idx, value in enumerate(
167
+ (
168
+ sheet_name,
169
+ datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
170
+ getpass.getuser(),
171
+ desc,
172
+ src,
173
+ ),
174
+ start=1,
175
+ ):
176
+ inv_ws.cell(row=next_row, column=col_idx, value=value)
177
+
178
+ wb.save(outfile)
179
+ wb.close()
180
+
181
+
182
+ def write_queries_to_xlsx(
183
+ table_list: str,
184
+ db: Any,
185
+ outfile: str,
186
+ append: bool = False,
187
+ tee: bool = False,
188
+ desc: str | None = None,
189
+ ) -> None:
190
+ """Write multiple tables/queries to separate worksheets in a single XLSX workbook.
191
+
192
+ Args:
193
+ table_list: Comma-separated list of table names (optionally schema-qualified).
194
+ db: An execsql database adapter with a ``select_rowsource()`` method.
195
+ outfile: Destination ``.xlsx`` file path.
196
+ append: If ``True`` and *outfile* exists, add new sheets to the existing
197
+ workbook rather than replacing it.
198
+ tee: If ``True``, also pretty-print each query result to stdout.
199
+ desc: Optional description(s). A single string is applied to every
200
+ sheet; a comma-separated string with the same count as *table_list*
201
+ assigns individual descriptions per sheet.
202
+ """
203
+ openpyxl = _require_openpyxl()
204
+
205
+ tables = [t.strip() for t in table_list.split(",")]
206
+ if desc is not None:
207
+ descriptions = [d.strip() for d in desc.split(",")]
208
+ one_desc = len(descriptions) != len(tables)
209
+ else:
210
+ descriptions = []
211
+ one_desc = False
212
+
213
+ # ------------------------------------------------------------------
214
+ # Open or create workbook
215
+ # ------------------------------------------------------------------
216
+ if Path(outfile).is_file() and not append:
217
+ filewriter_close(outfile)
218
+ os.unlink(outfile)
219
+
220
+ if Path(outfile).is_file():
221
+ wb = openpyxl.load_workbook(outfile)
222
+ else:
223
+ wb = openpyxl.Workbook()
224
+ # Remove the default empty sheet created by openpyxl.
225
+ if wb.sheetnames:
226
+ del wb[wb.sheetnames[0]]
227
+
228
+ # ------------------------------------------------------------------
229
+ # Ensure the inventory sheet exists
230
+ # ------------------------------------------------------------------
231
+ inventory_name = "Datasheets"
232
+ if inventory_name not in wb.sheetnames:
233
+ inv_ws = wb.create_sheet(inventory_name)
234
+ bold_font = openpyxl.styles.Font(bold=True)
235
+ for col_idx, hdr in enumerate(
236
+ ("datasheet_name", "created_on", "created_by", "description", "source"),
237
+ start=1,
238
+ ):
239
+ cell = inv_ws.cell(row=1, column=col_idx, value=hdr)
240
+ cell.font = bold_font
241
+ else:
242
+ inv_ws = wb[inventory_name]
243
+
244
+ # ------------------------------------------------------------------
245
+ # Write each table to its own sheet
246
+ # ------------------------------------------------------------------
247
+ bold_font = openpyxl.styles.Font(bold=True)
248
+
249
+ for i, t in enumerate(tables):
250
+ # Determine the table name used for the sheet label.
251
+ if "." in t:
252
+ st = t.split(".")
253
+ if len(st) != 2:
254
+ raise ErrInfo("cmd", other_msg=f"Unrecognized table specification in <{t}>")
255
+ tblname = unquoted(st[1])
256
+ else:
257
+ tblname = unquoted(t)
258
+
259
+ # Avoid duplicate sheet names.
260
+ existing_names = wb.sheetnames
261
+ sheet_name = tblname
262
+ sheet_no = 1
263
+ while sheet_name in existing_names:
264
+ sheet_name = f"{tblname}_{sheet_no}"
265
+ sheet_no += 1
266
+
267
+ # Fetch data.
268
+ select_stmt = f"select * from {t};"
269
+ try:
270
+ hdrs, rows = db.select_rowsource(select_stmt)
271
+ except ErrInfo:
272
+ raise
273
+ except Exception as e:
274
+ raise ErrInfo("db", select_stmt, exception_msg=exception_desc()) from e
275
+
276
+ # Write data sheet.
277
+ ws = wb.create_sheet(sheet_name)
278
+
279
+ for col_idx, hdr in enumerate(hdrs, start=1):
280
+ cell = ws.cell(row=1, column=col_idx, value=str(hdr))
281
+ cell.font = bold_font
282
+
283
+ for row_idx, row in enumerate(rows, start=2):
284
+ for col_idx, item in enumerate(row, start=1):
285
+ ws.cell(row=row_idx, column=col_idx, value=_cell_value(item))
286
+
287
+ # Determine per-sheet description.
288
+ if desc is None:
289
+ d = None
290
+ elif one_desc:
291
+ d = desc
292
+ else:
293
+ d = descriptions[i]
294
+
295
+ # Update inventory sheet.
296
+ script, lno = current_script_line()
297
+ src = f"From database {_state.dbs.current().name()}, with script {str(Path(script).resolve())}, line {lno}"
298
+ next_row = inv_ws.max_row + 1
299
+ for col_idx, value in enumerate(
300
+ (
301
+ sheet_name,
302
+ datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
303
+ getpass.getuser(),
304
+ d,
305
+ src,
306
+ ),
307
+ start=1,
308
+ ):
309
+ inv_ws.cell(row=next_row, column=col_idx, value=value)
310
+
311
+ if tee and outfile.lower() != "stdout":
312
+ prettyprint_query(select_stmt, db, "stdout", False, desc=d)
313
+
314
+ _state.export_metadata.add(ExportRecord(queryname=select_stmt, outfile=outfile, zipfile=None, description=d))
315
+
316
+ wb.save(outfile)
317
+ wb.close()