execsql2 2.12.7__tar.gz → 2.13.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 (302) hide show
  1. {execsql2-2.12.7 → execsql2-2.13.1}/CHANGELOG.md +24 -0
  2. {execsql2-2.12.7 → execsql2-2.13.1}/PKG-INFO +4 -4
  3. {execsql2-2.12.7 → execsql2-2.13.1}/README.md +1 -1
  4. {execsql2-2.12.7 → execsql2-2.13.1}/docs/about/divergence.md +2 -0
  5. {execsql2-2.12.7 → execsql2-2.13.1}/docs/reference/metacommands.md +10 -2
  6. {execsql2-2.12.7 → execsql2-2.13.1}/docs/reference/substitution_vars.md +4 -1
  7. {execsql2-2.12.7 → execsql2-2.13.1}/pyproject.toml +3 -3
  8. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/html.py +10 -2
  9. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/raw.py +31 -19
  10. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/zip.py +21 -3
  11. execsql2-2.13.1/src/execsql/importers/json.py +142 -0
  12. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/__init__.py +2 -0
  13. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/dispatch.py +12 -0
  14. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/io.py +2 -0
  15. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/io_import.py +36 -0
  16. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/system.py +4 -3
  17. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/fileio.py +8 -2
  18. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/mail.py +19 -3
  19. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_zip.py +91 -0
  20. execsql2-2.13.1/tests/importers/test_json_importer.py +319 -0
  21. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_system.py +67 -0
  22. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_system_extra.py +3 -0
  23. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_mail.py +100 -0
  24. {execsql2-2.12.7 → execsql2-2.13.1}/uv.lock +5 -5
  25. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/dba.md +0 -0
  26. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/herald.md +0 -0
  27. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/inspector.md +0 -0
  28. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/liaison.md +0 -0
  29. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/oracle.md +0 -0
  30. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/patcher.md +0 -0
  31. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/qa.md +0 -0
  32. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/scribe.md +0 -0
  33. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/code-oracle.md +0 -0
  34. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/migrate.md +0 -0
  35. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/review-changes.md +0 -0
  36. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/test-module.md +0 -0
  37. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/update-changelog.md +0 -0
  38. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/where-is.md +0 -0
  39. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/project_context.md +0 -0
  40. {execsql2-2.12.7 → execsql2-2.13.1}/.claude/state/status.md +0 -0
  41. {execsql2-2.12.7 → execsql2-2.13.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  42. {execsql2-2.12.7 → execsql2-2.13.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  43. {execsql2-2.12.7 → execsql2-2.13.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  44. {execsql2-2.12.7 → execsql2-2.13.1}/.github/workflows/ci-cd.yml +0 -0
  45. {execsql2-2.12.7 → execsql2-2.13.1}/.gitignore +0 -0
  46. {execsql2-2.12.7 → execsql2-2.13.1}/.pre-commit-config.yaml +0 -0
  47. {execsql2-2.12.7 → execsql2-2.13.1}/.pre-commit-hooks.yaml +0 -0
  48. {execsql2-2.12.7 → execsql2-2.13.1}/.python-version +0 -0
  49. {execsql2-2.12.7 → execsql2-2.13.1}/.readthedocs.yaml +0 -0
  50. {execsql2-2.12.7 → execsql2-2.13.1}/CLAUDE.md +0 -0
  51. {execsql2-2.12.7 → execsql2-2.13.1}/CONTRIBUTING.md +0 -0
  52. {execsql2-2.12.7 → execsql2-2.13.1}/LICENSE.txt +0 -0
  53. {execsql2-2.12.7 → execsql2-2.13.1}/NOTICE +0 -0
  54. {execsql2-2.12.7 → execsql2-2.13.1}/SECURITY.md +0 -0
  55. {execsql2-2.12.7 → execsql2-2.13.1}/docs/about/contributors.md +0 -0
  56. {execsql2-2.12.7 → execsql2-2.13.1}/docs/about/copyright.md +0 -0
  57. {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/cli.md +0 -0
  58. {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/db.md +0 -0
  59. {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/exporters.md +0 -0
  60. {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/importers.md +0 -0
  61. {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/index.md +0 -0
  62. {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/metacommands.md +0 -0
  63. {execsql2-2.12.7 → execsql2-2.13.1}/docs/dev/adding_db_adapters.md +0 -0
  64. {execsql2-2.12.7 → execsql2-2.13.1}/docs/dev/adding_exporters.md +0 -0
  65. {execsql2-2.12.7 → execsql2-2.13.1}/docs/dev/adding_importers.md +0 -0
  66. {execsql2-2.12.7 → execsql2-2.13.1}/docs/dev/adding_metacommands.md +0 -0
  67. {execsql2-2.12.7 → execsql2-2.13.1}/docs/dev/architecture.md +0 -0
  68. {execsql2-2.12.7 → execsql2-2.13.1}/docs/getting-started/installation.md +0 -0
  69. {execsql2-2.12.7 → execsql2-2.13.1}/docs/getting-started/requirements.md +0 -0
  70. {execsql2-2.12.7 → execsql2-2.13.1}/docs/getting-started/syntax.md +0 -0
  71. {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/debugging.md +0 -0
  72. {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/documentation.md +0 -0
  73. {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/encoding.md +0 -0
  74. {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/examples.md +0 -0
  75. {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/formatter.md +0 -0
  76. {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/logging.md +0 -0
  77. {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/sql_syntax.md +0 -0
  78. {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/usage.md +0 -0
  79. {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/using_scripts.md +0 -0
  80. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/Compare_planets.png +0 -0
  81. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/actions.png +0 -0
  82. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/actions2.png +0 -0
  83. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/checkboxes.png +0 -0
  84. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/connect.b64 +0 -0
  85. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/connect.png +0 -0
  86. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/create_conf.png +0 -0
  87. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/data_error1_screenshot.jpg +0 -0
  88. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/entry_form.png +0 -0
  89. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/execsql_console.png +0 -0
  90. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/execsql_logo_01.png +0 -0
  91. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/fatals.png +0 -0
  92. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/logo_small.png +0 -0
  93. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/pause_terminal.png +0 -0
  94. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/pause_terminal_sm.b64 +0 -0
  95. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/pause_terminal_sm.png +0 -0
  96. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/prompt_compare.png +0 -0
  97. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/set_build_commands.jpg +0 -0
  98. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/unit_conversions.b64 +0 -0
  99. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/unit_conversions_029.png +0 -0
  100. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/unmatched.png +0 -0
  101. {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/vim_execsql_highlight.png +0 -0
  102. {execsql2-2.12.7 → execsql2-2.13.1}/docs/index.md +0 -0
  103. {execsql2-2.12.7 → execsql2-2.13.1}/docs/reference/configuration.md +0 -0
  104. {execsql2-2.12.7 → execsql2-2.13.1}/docs/reference/security.md +0 -0
  105. {execsql2-2.12.7 → execsql2-2.13.1}/extras/vscode-execsql/README.md +0 -0
  106. {execsql2-2.12.7 → execsql2-2.13.1}/extras/vscode-execsql/package.json +0 -0
  107. {execsql2-2.12.7 → execsql2-2.13.1}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  108. {execsql2-2.12.7 → execsql2-2.13.1}/justfile +0 -0
  109. {execsql2-2.12.7 → execsql2-2.13.1}/scripts/generate_vscode_grammar.py +0 -0
  110. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/__init__.py +0 -0
  111. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/__main__.py +0 -0
  112. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/cli/__init__.py +0 -0
  113. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/cli/dsn.py +0 -0
  114. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/cli/help.py +0 -0
  115. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/cli/lint.py +0 -0
  116. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/cli/run.py +0 -0
  117. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/config.py +0 -0
  118. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/constants.py +0 -0
  119. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/__init__.py +0 -0
  120. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/access.py +0 -0
  121. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/base.py +0 -0
  122. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/dsn.py +0 -0
  123. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/duckdb.py +0 -0
  124. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/factory.py +0 -0
  125. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/firebird.py +0 -0
  126. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/mysql.py +0 -0
  127. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/oracle.py +0 -0
  128. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/postgres.py +0 -0
  129. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/sqlite.py +0 -0
  130. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/sqlserver.py +0 -0
  131. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/debug/__init__.py +0 -0
  132. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/debug/repl.py +0 -0
  133. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exceptions.py +0 -0
  134. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/__init__.py +0 -0
  135. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/base.py +0 -0
  136. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/delimited.py +0 -0
  137. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/duckdb.py +0 -0
  138. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/feather.py +0 -0
  139. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/json.py +0 -0
  140. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/latex.py +0 -0
  141. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/markdown.py +0 -0
  142. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/ods.py +0 -0
  143. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/parquet.py +0 -0
  144. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/pretty.py +0 -0
  145. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/protocol.py +0 -0
  146. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/sqlite.py +0 -0
  147. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/templates.py +0 -0
  148. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/values.py +0 -0
  149. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/xls.py +0 -0
  150. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/xlsx.py +0 -0
  151. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/xml.py +0 -0
  152. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/yaml.py +0 -0
  153. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/format.py +0 -0
  154. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/gui/__init__.py +0 -0
  155. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/gui/base.py +0 -0
  156. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/gui/console.py +0 -0
  157. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/gui/desktop.py +0 -0
  158. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/gui/tui.py +0 -0
  159. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/__init__.py +0 -0
  160. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/base.py +0 -0
  161. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/csv.py +0 -0
  162. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/feather.py +0 -0
  163. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/ods.py +0 -0
  164. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/xls.py +0 -0
  165. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/conditions.py +0 -0
  166. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/connect.py +0 -0
  167. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/control.py +0 -0
  168. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/data.py +0 -0
  169. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/debug.py +0 -0
  170. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/io_export.py +0 -0
  171. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/io_fileops.py +0 -0
  172. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/io_write.py +0 -0
  173. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/prompt.py +0 -0
  174. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/script_ext.py +0 -0
  175. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/upsert.py +0 -0
  176. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/models.py +0 -0
  177. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/parser.py +0 -0
  178. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/py.typed +0 -0
  179. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/script/__init__.py +0 -0
  180. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/script/control.py +0 -0
  181. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/script/engine.py +0 -0
  182. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/script/variables.py +0 -0
  183. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/state.py +0 -0
  184. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/types.py +0 -0
  185. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/__init__.py +0 -0
  186. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/auth.py +0 -0
  187. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/crypto.py +0 -0
  188. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/datetime.py +0 -0
  189. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/errors.py +0 -0
  190. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/gui.py +0 -0
  191. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/numeric.py +0 -0
  192. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/regex.py +0 -0
  193. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/strings.py +0 -0
  194. {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/timer.py +0 -0
  195. {execsql2-2.12.7 → execsql2-2.13.1}/templates/README.md +0 -0
  196. {execsql2-2.12.7 → execsql2-2.13.1}/templates/config_settings.sqlite +0 -0
  197. {execsql2-2.12.7 → execsql2-2.13.1}/templates/example_config_prompt.sql +0 -0
  198. {execsql2-2.12.7 → execsql2-2.13.1}/templates/execsql.conf +0 -0
  199. {execsql2-2.12.7 → execsql2-2.13.1}/templates/make_config_db.sql +0 -0
  200. {execsql2-2.12.7 → execsql2-2.13.1}/templates/md_compare.sql +0 -0
  201. {execsql2-2.12.7 → execsql2-2.13.1}/templates/md_glossary.sql +0 -0
  202. {execsql2-2.12.7 → execsql2-2.13.1}/templates/md_upsert.sql +0 -0
  203. {execsql2-2.12.7 → execsql2-2.13.1}/templates/pg_compare.sql +0 -0
  204. {execsql2-2.12.7 → execsql2-2.13.1}/templates/pg_glossary.sql +0 -0
  205. {execsql2-2.12.7 → execsql2-2.13.1}/templates/pg_upsert.sql +0 -0
  206. {execsql2-2.12.7 → execsql2-2.13.1}/templates/script_template.sql +0 -0
  207. {execsql2-2.12.7 → execsql2-2.13.1}/templates/ss_compare.sql +0 -0
  208. {execsql2-2.12.7 → execsql2-2.13.1}/templates/ss_glossary.sql +0 -0
  209. {execsql2-2.12.7 → execsql2-2.13.1}/templates/ss_upsert.sql +0 -0
  210. {execsql2-2.12.7 → execsql2-2.13.1}/tests/__init__.py +0 -0
  211. {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/__init__.py +0 -0
  212. {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_cli.py +0 -0
  213. {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_cli_e2e.py +0 -0
  214. {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_cli_run.py +0 -0
  215. {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_lint.py +0 -0
  216. {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_ping.py +0 -0
  217. {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_profile.py +0 -0
  218. {execsql2-2.12.7 → execsql2-2.13.1}/tests/conftest.py +0 -0
  219. {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/__init__.py +0 -0
  220. {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_base.py +0 -0
  221. {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_duckdb.py +0 -0
  222. {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_factory.py +0 -0
  223. {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_postgres.py +0 -0
  224. {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_sqlite.py +0 -0
  225. {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_sqlite_extra.py +0 -0
  226. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/__init__.py +0 -0
  227. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_base.py +0 -0
  228. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_db.py +0 -0
  229. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_delimited.py +0 -0
  230. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_duckdb_exporter.py +0 -0
  231. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_exporters.py +0 -0
  232. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_feather.py +0 -0
  233. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_html_latex.py +0 -0
  234. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_json.py +0 -0
  235. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_markdown.py +0 -0
  236. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_ods.py +0 -0
  237. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_parquet.py +0 -0
  238. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_sqlite_exporter.py +0 -0
  239. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_templates.py +0 -0
  240. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_xls_xlsx.py +0 -0
  241. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_xlsx.py +0 -0
  242. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_xml.py +0 -0
  243. {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_yaml.py +0 -0
  244. {execsql2-2.12.7 → execsql2-2.13.1}/tests/gui/__init__.py +0 -0
  245. {execsql2-2.12.7 → execsql2-2.13.1}/tests/gui/test_backends.py +0 -0
  246. {execsql2-2.12.7 → execsql2-2.13.1}/tests/importers/__init__.py +0 -0
  247. {execsql2-2.12.7 → execsql2-2.13.1}/tests/importers/test_csv_importer.py +0 -0
  248. {execsql2-2.12.7 → execsql2-2.13.1}/tests/importers/test_feather_importer.py +0 -0
  249. {execsql2-2.12.7 → execsql2-2.13.1}/tests/importers/test_ods_importer.py +0 -0
  250. {execsql2-2.12.7 → execsql2-2.13.1}/tests/importers/test_xls_importer.py +0 -0
  251. {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/__init__.py +0 -0
  252. {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/conftest.py +0 -0
  253. {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/test_dsn.py +0 -0
  254. {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/test_duckdb.py +0 -0
  255. {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/test_mysql.py +0 -0
  256. {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/test_postgres.py +0 -0
  257. {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/test_sqlite.py +0 -0
  258. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/__init__.py +0 -0
  259. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_assert.py +0 -0
  260. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_breakpoint.py +0 -0
  261. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_connect.py +0 -0
  262. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_io_export.py +0 -0
  263. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_io_import.py +0 -0
  264. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands.py +0 -0
  265. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_connect.py +0 -0
  266. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_data.py +0 -0
  267. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_extended.py +0 -0
  268. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  269. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_io.py +0 -0
  270. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  271. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  272. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_pg_upsert.py +0 -0
  273. {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_row_count.py +0 -0
  274. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_config.py +0 -0
  275. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_config_data.py +0 -0
  276. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_constants.py +0 -0
  277. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_engine.py +0 -0
  278. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_error_messages.py +0 -0
  279. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_exceptions.py +0 -0
  280. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_format.py +0 -0
  281. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_models.py +0 -0
  282. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_package.py +0 -0
  283. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_parser.py +0 -0
  284. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_registry.py +0 -0
  285. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_script.py +0 -0
  286. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_state.py +0 -0
  287. {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_types.py +0 -0
  288. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/__init__.py +0 -0
  289. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_auth.py +0 -0
  290. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_auth_extra.py +0 -0
  291. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_crypto.py +0 -0
  292. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_datetime.py +0 -0
  293. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_errors.py +0 -0
  294. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_errors_extra.py +0 -0
  295. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_fileio.py +0 -0
  296. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_fileio_extra.py +0 -0
  297. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_numeric.py +0 -0
  298. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_regex.py +0 -0
  299. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_strings.py +0 -0
  300. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_timer.py +0 -0
  301. {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_timer_extra.py +0 -0
  302. {execsql2-2.12.7 → execsql2-2.13.1}/zensical.toml +0 -0
@@ -13,6 +13,30 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.13.1] - 2026-04-04
17
+
18
+ ### Changed
19
+
20
+ - Bump pg-upsert minimum to >=1.20.0.
21
+
22
+ ______________________________________________________________________
23
+
24
+ ## [2.13.0] - 2026-04-04
25
+
26
+ ### Added
27
+
28
+ - New `IMPORT … FROM JSON` metacommand — imports a JSON array of objects or newline-delimited JSON (NDJSON) file into a database table. Nested objects are flattened with dot-separated column names; nested arrays are stored as JSON strings. Missing keys across records become NULL.
29
+ - `SHELL … CONTINUE` now sets `$SYSTEM_CMD_PID` substitution variable with the PID of the background process.
30
+
31
+ ### Fixed
32
+
33
+ - `Mailer`, `WriteableZipfile`, `ZipWriter` now support context manager protocol (`with` statement) for reliable resource cleanup. `__del__` methods are guarded against exceptions during interpreter shutdown.
34
+ - `FileWriter` and `FileControl` `__del__` methods no longer raise during interpreter shutdown.
35
+ - Raw/base64 binary export now uses `with open(…)` context managers instead of bare `open()`.
36
+ - HTML export append mode now cleans up temporary files if the final rename fails.
37
+
38
+ ______________________________________________________________________
39
+
16
40
  ## [2.12.7] - 2026-04-03
17
41
 
18
42
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.12.7
3
+ Version: 2.13.1
4
4
  Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
5
5
  Project-URL: Homepage, https://execsql2.readthedocs.io
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -51,7 +51,7 @@ Requires-Dist: keyring; extra == 'all'
51
51
  Requires-Dist: odfpy; extra == 'all'
52
52
  Requires-Dist: openpyxl; extra == 'all'
53
53
  Requires-Dist: oracledb; extra == 'all'
54
- Requires-Dist: pg-upsert>=1.18.2; extra == 'all'
54
+ Requires-Dist: pg-upsert>=1.20.0; extra == 'all'
55
55
  Requires-Dist: polars; extra == 'all'
56
56
  Requires-Dist: psycopg2-binary; extra == 'all'
57
57
  Requires-Dist: pymysql; extra == 'all'
@@ -109,7 +109,7 @@ Requires-Dist: oracledb; extra == 'oracle'
109
109
  Provides-Extra: postgres
110
110
  Requires-Dist: psycopg2-binary; extra == 'postgres'
111
111
  Provides-Extra: upsert
112
- Requires-Dist: pg-upsert>=1.18.2; extra == 'upsert'
112
+ Requires-Dist: pg-upsert>=1.20.0; extra == 'upsert'
113
113
  Description-Content-Type: text/markdown
114
114
 
115
115
  > [!NOTE]
@@ -238,7 +238,7 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
238
238
 
239
239
  # Features
240
240
 
241
- - Import data from CSV, TSV, Excel, OpenDocument, Feather, or Parquet files into a database table.
241
+ - Import data from CSV, TSV, JSON, Excel, OpenDocument, Feather, or Parquet files into a database table.
242
242
  - Export query results in 20+ formats including CSV, TSV, JSON, YAML, XML, HTML, Markdown, LaTeX, XLSX, OpenDocument, Feather, Parquet, HDF5, DuckDB, SQLite, plain text, and Jinja2 templates.
243
243
  - Copy data between databases, including across different DBMS types.
244
244
  - Conditionally execute SQL and metacommands using `IF`/`ELSE`/`ENDIF` based on data values, DBMS type, or user input.
@@ -124,7 +124,7 @@ Run `execsql --help` for the full option list, or `execsql -m` to list all metac
124
124
 
125
125
  # Features
126
126
 
127
- - Import data from CSV, TSV, Excel, OpenDocument, Feather, or Parquet files into a database table.
127
+ - Import data from CSV, TSV, JSON, Excel, OpenDocument, Feather, or Parquet files into a database table.
128
128
  - 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.
129
129
  - Copy data between databases, including across different DBMS types.
130
130
  - Conditionally execute SQL and metacommands using `IF`/`ELSE`/`ENDIF` based on data values, DBMS type, or user input.
@@ -48,6 +48,7 @@ ______________________________________________________________________
48
48
  | `CONFIG SHOW_PROGRESS` | Enable the Rich progress bar for IMPORT operations at runtime. |
49
49
  | `CONFIG LOG_SQL` | Enable SQL query audit logging — writes executed SQL to the log file. |
50
50
  | `PG_UPSERT` | QA-checked, FK-dependency-ordered upserts from staging to base schema on PostgreSQL. Integrates [pg-upsert](https://pg-upsert.readthedocs.io/) as an optional dependency. Three modes: full pipeline, QA-only, and schema check. |
51
+ | `IMPORT … FROM JSON` | Import a JSON file (array of objects or NDJSON) into a database table. Nested objects are flattened with dot-separated column names; arrays are stored as JSON strings. |
51
52
 
52
53
  ### Conditional Tests
53
54
 
@@ -162,6 +163,7 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
162
163
 
163
164
  ### Substitution Variables
164
165
 
166
+ - **`$SYSTEM_CMD_PID`** — New system variable set to the PID of the background process when `SHELL … CONTINUE` is used.
165
167
  - **Cycle detection** — `substitute_vars()` raises an error after 100 iterations to prevent infinite loops when variables reference each other cyclically. Upstream had no protection.
166
168
  - **O(1) substitution** — Variable substitution uses a single combined regex and dict lookup instead of O(V) per-variable regex passes. Behavior is identical; performance is improved.
167
169
  - **Lazy `$RANDOM`/`$UUID`** — These system variables are now computed on first access rather than generated unconditionally for every statement. Behavior is identical when referenced; scripts that never reference them skip the computation entirely.
@@ -1747,6 +1747,14 @@ The syntax for importing data from a data file in [Feather](https://arrow.apache
1747
1747
  IMPORT TO [NEW|REPLACEMENT] <table_name> FROM FEATHER <file_name>
1748
1748
  ```
1749
1749
 
1750
+ The syntax for importing data from a [JSON](https://www.json.org/) file is:
1751
+
1752
+ ```
1753
+ IMPORT TO [NEW|REPLACEMENT] <table_name> FROM JSON <file_name>
1754
+ ```
1755
+
1756
+ The JSON file must contain either a JSON array of objects (`[{…}, …]`) or newline-delimited JSON (NDJSON, one object per line). Nested objects are flattened with dot-separated column names (e.g., an object `{"address": {"city": "Portland"}}` produces column `address.city`). Nested arrays within objects are stored as JSON strings. Records with different keys produce a superset of columns — missing keys become NULL.
1757
+
1750
1758
  Column names in the input must be valid for the DBMS in use.
1751
1759
 
1752
1760
  If the "WITH QUOTE \<quote_char\> DELIMITER \<delim_char\>" clause is not used with text files, *execsql* will scan the text file to determine the quote and delimiter characters that are used in the file. By default, the first 100 lines of the file will be scanned. You can control the number of lines scanned with the "-s" option on *execsql*'s command line and with the *scan_lines* setting in a [configuration file](configuration.md#scan_lines). If the "WITH\..." clause is used, the file will not be scanned to identify the quote and delimiter characters regardless of the setting of the "-s" option.
@@ -1766,7 +1774,7 @@ If neither the NEW or REPLACEMENT keywords are used, the table must exist, must
1766
1774
 
1767
1775
  If a table is scanned to determine data types, any column that is completely empty (all null) will be created with the text data type. This provides the greatest flexibility for subsequent addition of data to the table. However, if that column ought to have a different data type, and a WHERE clause is applied to that column assuming a different data type, the DBMS may report an error because of incomparable data types.
1768
1776
 
1769
- When data are imported from Parquet or Feather data formats, and either the NEW or REPLACEMENT keywords are used, these data will be scanned to identify data types to use in the table-creation statement, regardless of the data types identified in the input file.
1777
+ When data are imported from Parquet, Feather, or JSON data formats, and either the NEW or REPLACEMENT keywords are used, these data will be scanned to identify data types to use in the table-creation statement, regardless of the data types identified in the input file.
1770
1778
 
1771
1779
  The handling of Boolean data types when data are imported depends on the capabilities of the DBMS in use. See the relevant section of the [SQL syntax notes](../guides/sql_syntax.md#boolean_data_types).
1772
1780
 
@@ -2800,7 +2808,7 @@ On non-POSIX operating systems (specifically, Windows), any backslashes in the c
2800
2808
 
2801
2809
  The command line that is run will be automatically [logged](../guides/logging.md#logging) in `execsql.log`.
2802
2810
 
2803
- The exit status of the command that is invoked will be stored in the [system variable](substitution_vars.md#system_vars) \$SYSTEM_CMD_EXIT_STATUS if the CONTINUE keyword has not been used.
2811
+ The exit status of the command that is invoked will be stored in the [system variable](substitution_vars.md#system_vars) \$SYSTEM_CMD_EXIT_STATUS if the CONTINUE keyword has not been used. When the CONTINUE keyword is used, the process ID of the background process is stored in \$SYSTEM_CMD_PID.
2804
2812
 
2805
2813
 
2806
2814
  ## TIMER
@@ -196,7 +196,10 @@ $STARTING_SCRIPT_REVTIME
196
196
  : The date and time of the script specified on the command line when execsql is run.
197
197
 
198
198
  $SYSTEM_CMD_EXIT_STATUS
199
- : The exit status of the command executed by the [SYSTEM_CMD](metacommands.md#system_cmd) metacommand. The value is "0" (zero) prior to the first use of the SYSTEM_CMD metacommand.
199
+ : The exit status of the command executed by the [SYSTEM_CMD](metacommands.md#system_cmd) metacommand. The value is "0" (zero) prior to the first use of the SYSTEM_CMD metacommand. Not set when the CONTINUE keyword is used.
200
+
201
+ $SYSTEM_CMD_PID
202
+ : The process ID (PID) of the background process launched by `SHELL … CONTINUE`. Only set when the CONTINUE keyword is used.
200
203
 
201
204
  $TIMER
202
205
  : The elapsed time of the script timer. If the [TIMER ON](metacommands.md#timer) command has never been used, this value will be zero. If the timer has been started but not stopped, this value will be the elapsed time since the timer was started. If the timer has been started and stopped, this value will be the elapsed time when the timer was stopped.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.12.7"
7
+ version = "2.13.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" }
@@ -58,7 +58,7 @@ odbc = ["pyodbc"]
58
58
  # Feature bundles
59
59
  formats = ["odfpy", "xlrd", "openpyxl", "Jinja2", "polars", "tables", "PyYAML"]
60
60
  auth = ["keyring"]
61
- upsert = ["pg-upsert>=1.18.2"]
61
+ upsert = ["pg-upsert>=1.20.0"]
62
62
  # Convenience groups
63
63
  all-db = [
64
64
  "psycopg2-binary",
@@ -161,7 +161,7 @@ skip-magic-trailing-comma = false
161
161
  line-ending = "auto"
162
162
 
163
163
  [tool.bumpversion]
164
- current_version = "2.12.7"
164
+ current_version = "2.13.1"
165
165
  commit = true
166
166
  commit_args = "--no-verify"
167
167
  tag = true
@@ -154,8 +154,16 @@ def export_html(
154
154
  finally:
155
155
  t.close()
156
156
  f.close()
157
- os.unlink(outfile)
158
- os.rename(tempfname, outfile)
157
+ try:
158
+ os.unlink(outfile)
159
+ os.rename(tempfname, outfile)
160
+ except OSError:
161
+ # Clean up temp file if rename fails.
162
+ try:
163
+ os.unlink(tempfname)
164
+ except OSError:
165
+ pass
166
+ raise
159
167
 
160
168
 
161
169
  def export_cgi_html(
@@ -29,21 +29,30 @@ def write_query_raw(
29
29
  if zipfile is None:
30
30
  filewriter_close(outfile)
31
31
  mode = "wb" if not append else "ab"
32
- of = open(outfile, mode) # noqa: SIM115
32
+ with open(outfile, mode) as of:
33
+ for row in rowsource:
34
+ for col in row:
35
+ if isinstance(col, bytearray):
36
+ of.write(col)
37
+ else:
38
+ if isinstance(col, str):
39
+ of.write(bytes(col, db_encoding))
40
+ else:
41
+ of.write(bytes(str(col), db_encoding))
33
42
  else:
34
43
  of = ZipWriter(zipfile, outfile, append)
35
- try:
36
- for row in rowsource:
37
- for col in row:
38
- if isinstance(col, bytearray):
39
- of.write(col)
40
- else:
41
- if isinstance(col, str):
42
- of.write(bytes(col, db_encoding))
44
+ try:
45
+ for row in rowsource:
46
+ for col in row:
47
+ if isinstance(col, bytearray):
48
+ of.write(col)
43
49
  else:
44
- of.write(bytes(str(col), db_encoding))
45
- finally:
46
- of.close()
50
+ if isinstance(col, str):
51
+ of.write(bytes(col, db_encoding))
52
+ else:
53
+ of.write(bytes(str(col), db_encoding))
54
+ finally:
55
+ of.close()
47
56
 
48
57
 
49
58
  def write_query_b64(outfile: str, rowsource: Any, append: bool = False, zipfile: str | None = None) -> None:
@@ -51,12 +60,15 @@ def write_query_b64(outfile: str, rowsource: Any, append: bool = False, zipfile:
51
60
  if zipfile is None:
52
61
  filewriter_close(outfile)
53
62
  mode = "wb" if not append else "ab"
54
- of = open(outfile, mode) # noqa: SIM115
63
+ with open(outfile, mode) as of:
64
+ for row in rowsource:
65
+ for col in row:
66
+ of.write(base64.standard_b64decode(col))
55
67
  else:
56
68
  of = ZipWriter(zipfile, outfile, append)
57
- try:
58
- for row in rowsource:
59
- for col in row:
60
- of.write(base64.standard_b64decode(col))
61
- finally:
62
- of.close()
69
+ try:
70
+ for row in rowsource:
71
+ for col in row:
72
+ of.write(base64.standard_b64decode(col))
73
+ finally:
74
+ of.close()
@@ -32,8 +32,18 @@ class WriteableZipfile:
32
32
  self.zf = zipfile.ZipFile(zipfile_name, mode=zmode, compression=comp, compresslevel=9)
33
33
  self.current_handle = None
34
34
 
35
- def __del__(self) -> None:
35
+ def __enter__(self) -> WriteableZipfile:
36
+ return self
37
+
38
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
36
39
  self.close()
40
+ return None
41
+
42
+ def __del__(self) -> None:
43
+ try:
44
+ self.close()
45
+ except Exception:
46
+ pass # Best-effort cleanup at interpreter shutdown.
37
47
 
38
48
  def member_file(self, member_filename: str) -> None:
39
49
  """Create a new member entry in the archive and open it for writing."""
@@ -97,11 +107,19 @@ class ZipWriter:
97
107
  self.zwriter = WriteableZipfile(self.zip_fname, append)
98
108
  self.member = self.zwriter.member_file(member_fname)
99
109
 
110
+ def __enter__(self) -> ZipWriter:
111
+ return self
112
+
113
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
114
+ self.close()
115
+ return None
116
+
100
117
  def write(self, str_data: str) -> None:
101
118
  """Write a string to the current zip member."""
102
119
  self.zwriter.write(str_data)
103
120
 
104
121
  def close(self) -> None:
105
122
  """Close the zip member and finalise the archive."""
106
- self.zwriter.close()
107
- self.zwriter = None
123
+ if self.zwriter is not None:
124
+ self.zwriter.close()
125
+ self.zwriter = None
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ JSON import for execsql.
5
+
6
+ Provides :func:`import_json`, used by ``IMPORT … FORMAT json``.
7
+ Supports JSON arrays of objects (``[{…}, …]``) and newline-delimited
8
+ JSON (NDJSON, one object per line). Nested objects are flattened with
9
+ dot-separated keys; nested arrays and non-object values are serialized
10
+ as JSON strings so every column maps to a scalar database value.
11
+ """
12
+
13
+ import json
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from execsql.db.base import Database
18
+ from execsql.exceptions import ErrInfo
19
+ from execsql.importers.base import import_data_table
20
+
21
+ __all__ = ["import_json"]
22
+
23
+
24
+ def _flatten(obj: Any, prefix: str = "", sep: str = ".") -> dict[str, Any]:
25
+ """Recursively flatten a nested dict.
26
+
27
+ Nested dicts produce dot-separated keys. All other compound values
28
+ (lists, nested lists-of-dicts) are serialized as JSON strings so the
29
+ result is always ``{str: scalar}``.
30
+ """
31
+ items: dict[str, Any] = {}
32
+ if isinstance(obj, dict):
33
+ for key, value in obj.items():
34
+ new_key = f"{prefix}{sep}{key}" if prefix else key
35
+ if isinstance(value, dict):
36
+ items.update(_flatten(value, new_key, sep))
37
+ elif isinstance(value, list):
38
+ # Serialize arrays as JSON strings — tables are flat.
39
+ items[new_key] = json.dumps(value, default=str)
40
+ else:
41
+ items[new_key] = value
42
+ return items
43
+
44
+
45
+ def _parse_json_file(filename: str, encoding: str) -> list[dict[str, Any]]:
46
+ """Read a JSON file and return a list of flat dicts.
47
+
48
+ Accepts either a JSON array of objects or newline-delimited JSON
49
+ (NDJSON).
50
+ """
51
+ text = Path(filename).read_text(encoding=encoding)
52
+ stripped = text.strip()
53
+
54
+ if stripped.startswith("["):
55
+ # Standard JSON array.
56
+ raw = json.loads(stripped)
57
+ if not isinstance(raw, list):
58
+ raise ErrInfo(type="error", other_msg="JSON file root is not an array of objects.")
59
+ records = raw
60
+ elif stripped.startswith("{"):
61
+ # Try NDJSON (one object per line) or a single object.
62
+ records = []
63
+ for lineno, line in enumerate(stripped.splitlines(), 1):
64
+ line = line.strip()
65
+ if not line:
66
+ continue
67
+ try:
68
+ obj = json.loads(line)
69
+ except json.JSONDecodeError as exc:
70
+ raise ErrInfo(
71
+ type="error",
72
+ other_msg=f"Invalid JSON on line {lineno}: {exc}",
73
+ ) from exc
74
+ if not isinstance(obj, dict):
75
+ raise ErrInfo(
76
+ type="error",
77
+ other_msg=f"Line {lineno} is not a JSON object.",
78
+ )
79
+ records.append(obj)
80
+ else:
81
+ raise ErrInfo(
82
+ type="error",
83
+ other_msg="JSON import expects a file starting with '[' (array) or '{' (object/NDJSON).",
84
+ )
85
+
86
+ if not records:
87
+ raise ErrInfo(type="error", other_msg="JSON file contains no records.")
88
+
89
+ # Validate that all records are dicts.
90
+ for i, rec in enumerate(records):
91
+ if not isinstance(rec, dict):
92
+ raise ErrInfo(
93
+ type="error",
94
+ other_msg=f"Record {i} in JSON file is not an object (got {type(rec).__name__}).",
95
+ )
96
+
97
+ return [_flatten(rec) for rec in records]
98
+
99
+
100
+ def import_json(
101
+ db: Database,
102
+ schemaname: str | None,
103
+ tablename: str,
104
+ filename: str,
105
+ is_new: Any,
106
+ encoding: str | None = None,
107
+ ) -> None:
108
+ """Import a JSON file into a database table.
109
+
110
+ Objects are flattened so that nested keys become dot-separated column
111
+ names (e.g. ``address.city``). Arrays within objects are stored as
112
+ JSON strings.
113
+ """
114
+ from execsql.utils.errors import exception_desc
115
+
116
+ import execsql.state as _state
117
+
118
+ enc = encoding if encoding else _state.conf.import_encoding
119
+
120
+ try:
121
+ flat_records = _parse_json_file(filename, enc)
122
+ except ErrInfo:
123
+ raise
124
+ except Exception as e:
125
+ raise ErrInfo(
126
+ "exception",
127
+ exception_msg=exception_desc(),
128
+ other_msg=f"Can't parse JSON file {filename}",
129
+ ) from e
130
+
131
+ # Build a union of all keys across records (preserving first-seen order).
132
+ seen: dict[str, None] = {}
133
+ for rec in flat_records:
134
+ for key in rec:
135
+ if key not in seen:
136
+ seen[key] = None
137
+ hdrs = list(seen)
138
+
139
+ # Build row data aligned to hdrs — missing keys become None.
140
+ data = [[rec.get(h) for h in hdrs] for rec in flat_records]
141
+
142
+ import_data_table(db, schemaname, tablename, is_new, hdrs, data)
@@ -118,6 +118,7 @@ from execsql.metacommands.io import (
118
118
  x_import_xls_pattern,
119
119
  x_import_parquet,
120
120
  x_import_feather,
121
+ x_import_json,
121
122
  x_import_row_buffer,
122
123
  x_show_progress,
123
124
  x_export_row_buffer,
@@ -325,6 +326,7 @@ __all__ = [
325
326
  "x_import_xls_pattern",
326
327
  "x_import_parquet",
327
328
  "x_import_feather",
329
+ "x_import_json",
328
330
  "x_import_row_buffer",
329
331
  "x_show_progress",
330
332
  "x_export_row_buffer",
@@ -118,6 +118,7 @@ from execsql.metacommands.io import (
118
118
  x_import,
119
119
  x_import_feather,
120
120
  x_import_file,
121
+ x_import_json,
121
122
  x_import_ods,
122
123
  x_import_ods_pattern,
123
124
  x_import_parquet,
@@ -540,6 +541,17 @@ def build_dispatch_table() -> MetaCommandList:
540
541
  x_import_feather,
541
542
  )
542
543
 
544
+ # ------------------------------------------------------------------
545
+ # IMPORT JSON
546
+ # ------------------------------------------------------------------
547
+ mcl.add(
548
+ ins_table_rxs(
549
+ r"^\s*IMPORT\s+TO\s+(?:(?P<new>NEW|REPLACEMENT)\s+)?",
550
+ ins_fn_rxs(r"\s+FROM\s+JSON\s+", r"\s*$"),
551
+ ),
552
+ x_import_json,
553
+ )
554
+
543
555
  # ------------------------------------------------------------------
544
556
  # PROMPT ACTION
545
557
  # ------------------------------------------------------------------
@@ -34,6 +34,7 @@ from execsql.metacommands.io_import import ( # noqa: F401
34
34
  x_import,
35
35
  x_import_feather,
36
36
  x_import_file,
37
+ x_import_json,
37
38
  x_import_ods,
38
39
  x_import_ods_pattern,
39
40
  x_import_parquet,
@@ -88,6 +89,7 @@ __all__ = [
88
89
  "x_import",
89
90
  "x_import_feather",
90
91
  "x_import_file",
92
+ "x_import_json",
91
93
  "x_import_ods",
92
94
  "x_import_ods_pattern",
93
95
  "x_import_parquet",
@@ -14,6 +14,7 @@ import execsql.state as _state
14
14
  from execsql.exceptions import ErrInfo
15
15
  from execsql.importers.csv import importfile, importtable
16
16
  from execsql.importers.feather import import_feather, import_parquet
17
+ from execsql.importers.json import import_json
17
18
  from execsql.importers.ods import OdsFile, importods
18
19
  from execsql.exporters.xls import XlsFile, XlsxFile
19
20
  from execsql.importers.xls import importxls
@@ -388,6 +389,41 @@ def x_import_feather(**kwargs: Any) -> None:
388
389
  return None
389
390
 
390
391
 
392
+ def x_import_json(**kwargs: Any) -> None:
393
+ newstr = kwargs["new"]
394
+ if newstr:
395
+ is_new = 1 + ["new", "replacement"].index(newstr.lower())
396
+ else:
397
+ is_new = 0
398
+ schemaname = kwargs["schema"]
399
+ tablename = kwargs["table"]
400
+ filename = kwargs["filename"]
401
+ if len(filename) > 1 and filename[0] == "~" and filename[1] == os.sep:
402
+ filename = str(Path.home() / filename[2:])
403
+ if not Path(filename).exists():
404
+ raise ErrInfo(
405
+ type="cmd",
406
+ command_text=kwargs["metacommandline"],
407
+ other_msg=f"Input file {filename} does not exist",
408
+ )
409
+ enc = kwargs.get("encoding")
410
+ from execsql.metacommands.conditions import file_size_date
411
+
412
+ sz, dt = file_size_date(filename)
413
+ _state.exec_log.log_status_info(f"IMPORTing from JSON file {filename} ({sz}, {dt})")
414
+ try:
415
+ import_json(_state.dbs.current(), schemaname, tablename, filename, is_new, encoding=enc)
416
+ except ErrInfo:
417
+ raise
418
+ except Exception as e:
419
+ raise ErrInfo(
420
+ "exception",
421
+ exception_msg=exception_desc(),
422
+ other_msg=f"Can't import data from JSON file {filename}",
423
+ ) from e
424
+ return None
425
+
426
+
391
427
  def x_import_row_buffer(**kwargs: Any) -> None:
392
428
  rows = kwargs["rows"]
393
429
  _state.conf.import_row_buffer = int(rows)
@@ -51,7 +51,8 @@ def x_system_cmd(**kwargs: Any) -> None:
51
51
  returncode = subprocess.call(cmdargs)
52
52
  _state.subvars.add_substitution("$SYSTEM_CMD_EXIT_STATUS", str(returncode))
53
53
  else:
54
- subprocess.Popen(cmdargs)
54
+ proc = subprocess.Popen(cmdargs)
55
+ _state.subvars.add_substitution("$SYSTEM_CMD_PID", str(proc.pid))
55
56
  return None
56
57
 
57
58
 
@@ -62,8 +63,8 @@ def x_email(**kwargs: Any) -> None:
62
63
  msg = kwargs["msg"]
63
64
  msg_file = kwargs["msg_file"]
64
65
  att_file = kwargs["att_file"]
65
- m = Mailer()
66
- m.sendmail(from_addr, to_addr, subject, msg, msg_file, att_file)
66
+ with Mailer() as m:
67
+ m.sendmail(from_addr, to_addr, subject, msg, msg_file, att_file)
67
68
 
68
69
 
69
70
  def x_timer(**kwargs: Any) -> None:
@@ -119,7 +119,10 @@ class FileWriter(multiprocessing.Process):
119
119
  self.close_after_write = False
120
120
 
121
121
  def __del__(self) -> None:
122
- self.close()
122
+ try:
123
+ self.close()
124
+ except Exception:
125
+ pass # Best-effort cleanup at interpreter shutdown.
123
126
 
124
127
  def write_queue(self) -> None:
125
128
  while len(self.output_queue) > 0:
@@ -215,7 +218,10 @@ class FileWriter(multiprocessing.Process):
215
218
  )
216
219
 
217
220
  def __del__(self) -> None:
218
- self.close_all()
221
+ try:
222
+ self.close_all()
223
+ except Exception:
224
+ pass # Best-effort cleanup at interpreter shutdown.
219
225
 
220
226
  def close_all(self) -> None:
221
227
  for fc in getattr(self, "files", {}).values():
@@ -28,9 +28,24 @@ class Mailer:
28
28
  def __repr__(self) -> str:
29
29
  return "Mailer()"
30
30
 
31
- def __del__(self) -> None:
31
+ def __enter__(self) -> Mailer:
32
+ return self
33
+
34
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
35
+ self.close()
36
+ return None
37
+
38
+ def close(self) -> None:
32
39
  if hasattr(self, "smtpconn"):
33
- self.smtpconn.quit()
40
+ try:
41
+ self.smtpconn.quit()
42
+ except Exception:
43
+ pass # Best-effort; connection may already be closed.
44
+ finally:
45
+ del self.smtpconn
46
+
47
+ def __del__(self) -> None:
48
+ self.close()
34
49
 
35
50
  def __init__(self) -> None:
36
51
  conf = _state.conf
@@ -134,5 +149,6 @@ class MailSpec:
134
149
  content_filename = _state.subvars.substitute_all(content_filename)
135
150
  attach_filename = _state.commandliststack[-1].localvars.substitute_all(self.attach_filename)
136
151
  attach_filename = _state.subvars.substitute_all(attach_filename)
137
- Mailer().sendmail(send_from, send_to, subject, msg_content, content_filename, attach_filename)
152
+ with Mailer() as m:
153
+ m.sendmail(send_from, send_to, subject, msg_content, content_filename, attach_filename)
138
154
  return None