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.
- {execsql2-2.12.7 → execsql2-2.13.1}/CHANGELOG.md +24 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/PKG-INFO +4 -4
- {execsql2-2.12.7 → execsql2-2.13.1}/README.md +1 -1
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/about/divergence.md +2 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/reference/metacommands.md +10 -2
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/reference/substitution_vars.md +4 -1
- {execsql2-2.12.7 → execsql2-2.13.1}/pyproject.toml +3 -3
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/html.py +10 -2
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/raw.py +31 -19
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/zip.py +21 -3
- execsql2-2.13.1/src/execsql/importers/json.py +142 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/__init__.py +2 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/dispatch.py +12 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/io.py +2 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/io_import.py +36 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/system.py +4 -3
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/fileio.py +8 -2
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/mail.py +19 -3
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_zip.py +91 -0
- execsql2-2.13.1/tests/importers/test_json_importer.py +319 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_system.py +67 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_system_extra.py +3 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_mail.py +100 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/uv.lock +5 -5
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/dba.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/herald.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/inspector.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/liaison.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/oracle.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/patcher.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/qa.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/agents/scribe.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/code-oracle.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/migrate.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/review-changes.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/test-module.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/update-changelog.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/commands/where-is.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/project_context.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.claude/state/status.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.github/workflows/ci-cd.yml +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.gitignore +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.pre-commit-config.yaml +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.python-version +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/.readthedocs.yaml +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/CLAUDE.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/CONTRIBUTING.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/LICENSE.txt +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/NOTICE +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/SECURITY.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/about/contributors.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/about/copyright.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/cli.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/db.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/exporters.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/importers.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/index.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/api/metacommands.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/dev/adding_metacommands.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/dev/architecture.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/getting-started/installation.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/getting-started/syntax.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/debugging.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/documentation.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/encoding.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/examples.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/formatter.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/logging.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/usage.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/actions.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/actions2.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/checkboxes.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/connect.b64 +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/connect.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/create_conf.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/entry_form.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/execsql_console.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/fatals.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/logo_small.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/unmatched.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/index.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/reference/configuration.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/docs/reference/security.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/justfile +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/__main__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/cli/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/cli/help.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/cli/run.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/config.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/constants.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/access.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/base.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/dsn.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/duckdb.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/factory.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/firebird.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/mysql.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/oracle.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/postgres.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/sqlite.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/db/sqlserver.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/debug/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/debug/repl.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exceptions.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/base.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/delimited.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/pretty.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/format.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/gui/base.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/gui/console.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/gui/tui.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/base.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/conditions.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/connect.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/control.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/debug.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/io_export.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/io_fileops.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/io_write.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/script_ext.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/metacommands/upsert.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/models.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/parser.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/py.typed +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/script/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/script/control.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/script/engine.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/script/variables.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/state.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/types.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/crypto.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/datetime.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/errors.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/gui.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/README.md +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/config_settings.sqlite +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/execsql.conf +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/make_config_db.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/md_compare.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/md_glossary.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/md_upsert.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/pg_compare.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/pg_glossary.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/pg_upsert.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/script_template.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/ss_compare.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/ss_glossary.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/templates/ss_upsert.sql +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_cli.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_cli_run.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_lint.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_ping.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/cli/test_profile.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/conftest.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_base.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_factory.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_postgres.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_base.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_db.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_delimited.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_json.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/gui/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/gui/test_backends.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/importers/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/conftest.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/test_mysql.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/test_postgres.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_assert.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_breakpoint.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_io_export.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_io_import.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_extended.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_io.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_metacommands_script_ext.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_pg_upsert.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/metacommands/test_row_count.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_config.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_config_data.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_constants.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_engine.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_error_messages.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_exceptions.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_format.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_models.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_package.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_parser.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_registry.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_script.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_state.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/test_types.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/__init__.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_auth.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_datetime.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_errors.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_errors_extra.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_regex.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_strings.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_timer.py +0 -0
- {execsql2-2.12.7 → execsql2-2.13.1}/tests/utils/test_timer_extra.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
|
107
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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()
|
|
152
|
+
with Mailer() as m:
|
|
153
|
+
m.sendmail(send_from, send_to, subject, msg_content, content_filename, attach_filename)
|
|
138
154
|
return None
|