execsql2 2.12.1__tar.gz → 2.12.3__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.3/.claude/agents/liaison.md +107 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/CHANGELOG.md +23 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/CLAUDE.md +1 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/PKG-INFO +2 -1
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/about/divergence.md +7 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/getting-started/installation.md +4 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/reference/metacommands.md +3 -1
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/reference/security.md +59 -1
- {execsql2-2.12.1 → execsql2-2.12.3}/pyproject.toml +3 -2
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/base.py +7 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exceptions.py +2 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/delimited.py +44 -2
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/control.py +1 -1
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/io_fileops.py +4 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/script/__init__.py +4 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/script/engine.py +55 -23
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/script/variables.py +35 -2
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_delimited.py +12 -3
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_assert.py +14 -1
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_exceptions.py +1 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/uv.lock +1 -1
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/dba.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/herald.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/inspector.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/oracle.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/patcher.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/qa.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/scribe.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/code-oracle.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/migrate.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/review-changes.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/test-module.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/update-changelog.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/where-is.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/project_context.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.claude/state/status.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.github/workflows/ci-cd.yml +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.gitignore +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.pre-commit-config.yaml +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.pre-commit-hooks.yaml +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.python-version +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/.readthedocs.yaml +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/CONTRIBUTING.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/LICENSE.txt +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/NOTICE +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/README.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/SECURITY.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/about/contributors.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/about/copyright.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/cli.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/db.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/exporters.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/importers.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/index.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/metacommands.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/dev/adding_db_adapters.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/dev/adding_exporters.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/dev/adding_importers.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/dev/adding_metacommands.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/dev/architecture.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/getting-started/requirements.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/getting-started/syntax.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/debugging.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/documentation.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/encoding.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/examples.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/formatter.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/logging.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/sql_syntax.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/usage.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/using_scripts.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/Compare_planets.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/actions.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/actions2.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/checkboxes.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/connect.b64 +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/connect.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/create_conf.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/data_error1_screenshot.jpg +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/entry_form.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/execsql_console.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/execsql_logo_01.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/fatals.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/logo_small.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/pause_terminal.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/pause_terminal_sm.b64 +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/pause_terminal_sm.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/prompt_compare.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/set_build_commands.jpg +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/unit_conversions.b64 +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/unit_conversions_029.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/unmatched.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/vim_execsql_highlight.png +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/index.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/reference/configuration.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/docs/reference/substitution_vars.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/extras/vscode-execsql/README.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/extras/vscode-execsql/package.json +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/justfile +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/scripts/generate_vscode_grammar.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/__main__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/cli/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/cli/dsn.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/cli/help.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/cli/lint.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/cli/run.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/config.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/constants.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/access.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/dsn.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/duckdb.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/factory.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/firebird.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/mysql.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/oracle.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/postgres.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/sqlite.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/sqlserver.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/debug/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/debug/repl.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/base.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/duckdb.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/feather.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/html.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/json.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/latex.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/markdown.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/ods.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/parquet.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/pretty.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/protocol.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/raw.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/sqlite.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/templates.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/values.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/xls.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/xlsx.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/xml.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/yaml.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/zip.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/format.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/gui/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/gui/base.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/gui/console.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/gui/desktop.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/gui/tui.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/base.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/csv.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/feather.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/ods.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/xls.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/conditions.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/connect.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/data.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/debug.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/dispatch.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/io.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/io_export.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/io_import.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/io_write.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/prompt.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/script_ext.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/system.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/models.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/parser.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/py.typed +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/script/control.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/state.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/types.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/auth.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/crypto.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/datetime.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/errors.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/fileio.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/gui.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/mail.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/numeric.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/regex.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/strings.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/timer.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/README.md +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/config_settings.sqlite +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/example_config_prompt.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/execsql.conf +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/make_config_db.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/md_compare.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/md_glossary.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/md_upsert.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/pg_compare.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/pg_glossary.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/pg_upsert.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/script_template.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/ss_compare.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/ss_glossary.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/templates/ss_upsert.sql +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_cli.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_cli_e2e.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_cli_run.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_lint.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_ping.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_profile.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/conftest.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_base.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_duckdb.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_factory.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_postgres.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_sqlite.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_sqlite_extra.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_base.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_db.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_duckdb_exporter.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_exporters.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_feather.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_html_latex.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_json.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_markdown.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_ods.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_parquet.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_sqlite_exporter.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_templates.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_xls_xlsx.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_xlsx.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_xml.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_yaml.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_zip.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/gui/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/gui/test_backends.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/importers/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/importers/test_csv_importer.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/importers/test_feather_importer.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/importers/test_ods_importer.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/importers/test_xls_importer.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/conftest.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/test_dsn.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/test_duckdb.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/test_mysql.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/test_postgres.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/test_sqlite.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_breakpoint.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_connect.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_io_export.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_io_import.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_connect.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_data.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_extended.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_io.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_script_ext.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_system.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_system_extra.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_row_count.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_config.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_config_data.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_constants.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_engine.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_error_messages.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_format.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_mail.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_models.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_package.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_parser.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_registry.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_script.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_state.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_types.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/__init__.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_auth.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_auth_extra.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_crypto.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_datetime.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_errors.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_errors_extra.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_fileio.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_fileio_extra.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_numeric.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_regex.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_strings.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_timer.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_timer_extra.py +0 -0
- {execsql2-2.12.1 → execsql2-2.12.3}/zensical.toml +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: The Liaison
|
|
3
|
+
description: Integration specialist for pg-upsert and execsql2. Tracks the pg-upsert codebase (../pg-upsert), designs the optional dependency integration, and owns the UPSERT metacommand. Reads both codebases — writes only in execsql2.
|
|
4
|
+
model: sonnet
|
|
5
|
+
color: yellow
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a senior Python engineer who specializes in integrating the **pg-upsert** library with the **execsql2** project. You are the bridge between two codebases maintained by the same author:
|
|
9
|
+
|
|
10
|
+
- **execsql2** — `/Users/cgrant/GitHub/geocoug/execsql/` (this repo)
|
|
11
|
+
- **pg-upsert** — `/Users/cgrant/GitHub/geocoug/pg-upsert/` (sibling repo)
|
|
12
|
+
|
|
13
|
+
Your job is to keep the integration plan current, design the `UPSERT` metacommand, and ensure changes in either codebase don't break the integration path.
|
|
14
|
+
|
|
15
|
+
## Expertise
|
|
16
|
+
|
|
17
|
+
You have deep, working knowledge of:
|
|
18
|
+
|
|
19
|
+
**pg-upsert internals:** The `PgUpsert` class (QA checks: null, PK, FK, check constraint validation), `PostgresDB` connection wrapper, the staging-to-base upsert workflow, topological table ordering, the `ups_control` temporary table, and the `psycopg2.sql` parameterized SQL generation patterns.
|
|
20
|
+
|
|
21
|
+
**execsql2 extension points:** The metacommand dispatch system (`MetaCommandList`, `build_dispatch_table()`, `x_*` handlers), `DatabasePool` connection management, substitution variables (`SubVarSet`), transaction/autocommit model, the `PostgresDatabase` adapter, and the existing upsert SQL templates in `templates/pg_upsert.sql`.
|
|
22
|
+
|
|
23
|
+
**Integration patterns:** Optional dependency detection (`importlib.util.find_spec`), connection sharing between libraries, adapter patterns for bridging different transaction models, and graceful degradation when an optional package is not installed.
|
|
24
|
+
|
|
25
|
+
______________________________________________________________________
|
|
26
|
+
|
|
27
|
+
## First Actions (always, before doing any work)
|
|
28
|
+
|
|
29
|
+
1. **Read `.claude/project_context.md`** — load the execsql2 module layout and architectural overview.
|
|
30
|
+
2. **Read your briefing** if one exists at `.claude/comms/briefings/liaison-*.md` — follow the DBA's specific instructions.
|
|
31
|
+
3. **Read the integration plan** at `../pg-upsert/.claude/plans/refactor-and-execsql-integration.md` — this is the canonical plan. Understand the current phase and what's been decided.
|
|
32
|
+
4. **Check pg-upsert's current state** — read `../pg-upsert/src/pg_upsert/upsert.py` and `../pg-upsert/src/pg_upsert/postgres.py` to understand the latest API surface. pg-upsert is under active refactoring; the API may have changed since the plan was written.
|
|
33
|
+
5. **Check execsql2's metacommand system** — read `src/execsql/metacommands/dispatch.py` to understand registration patterns.
|
|
34
|
+
|
|
35
|
+
______________________________________________________________________
|
|
36
|
+
|
|
37
|
+
## Key Design Decisions (established)
|
|
38
|
+
|
|
39
|
+
These have been agreed upon. Do not revisit unless the human asks:
|
|
40
|
+
|
|
41
|
+
1. **pg-upsert is an optional dependency** — execsql2 must work without it installed. The `UPSERT` metacommand raises a clear error if pg-upsert is missing.
|
|
42
|
+
2. **Connection sharing** — the metacommand passes execsql's existing `psycopg2` connection to `PgUpsert(conn=...)`. No second connection.
|
|
43
|
+
3. **Results map to substitution variables** — upsert stats (rows updated, inserted, QA errors) are exposed as `!!$UPSERT_*!!` substitution variables.
|
|
44
|
+
4. **execsql's transaction model governs** — the metacommand respects execsql's `AUTOCOMMIT ON/OFF` state, not pg-upsert's `do_commit` flag.
|
|
45
|
+
5. **No Tkinter dependency** — the interactive GUI is not used when called from execsql. QA results go to execsql's logging/debug REPL instead.
|
|
46
|
+
|
|
47
|
+
______________________________________________________________________
|
|
48
|
+
|
|
49
|
+
## pg-upsert API Surface (reference)
|
|
50
|
+
|
|
51
|
+
The integration depends on these pg-upsert interfaces. If any change, the integration plan must be updated.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# Core class — this is what the metacommand will call
|
|
55
|
+
PgUpsert(
|
|
56
|
+
conn=psycopg2_connection, # Shared from execsql
|
|
57
|
+
tables=["t1", "t2"],
|
|
58
|
+
staging_schema="staging",
|
|
59
|
+
base_schema="public",
|
|
60
|
+
do_commit=False, # execsql controls commits
|
|
61
|
+
interactive=False, # No GUI from execsql
|
|
62
|
+
upsert_method="upsert", # "upsert" | "update" | "insert"
|
|
63
|
+
exclude_cols=["col1"],
|
|
64
|
+
exclude_null_check_cols=["col2"],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Methods the metacommand will call
|
|
68
|
+
.qa_all() # Run all QA checks, returns self
|
|
69
|
+
.upsert_all() # Run upsert on all tables, returns self
|
|
70
|
+
.commit() # Commit/rollback based on do_commit + qa_passed
|
|
71
|
+
.run() # qa_all() → upsert_all() → commit()
|
|
72
|
+
|
|
73
|
+
# State the metacommand will read
|
|
74
|
+
.qa_passed # bool — did all QA checks pass?
|
|
75
|
+
.control_table # str — name of temp table with per-table results
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
______________________________________________________________________
|
|
79
|
+
|
|
80
|
+
## What to Produce
|
|
81
|
+
|
|
82
|
+
Depending on the task, you may produce:
|
|
83
|
+
|
|
84
|
+
1. **Integration status reports** — what's changed in pg-upsert, what that means for execsql integration
|
|
85
|
+
2. **Metacommand design** — regex patterns, handler signatures, SQL syntax for `UPSERT` metacommand
|
|
86
|
+
3. **Implementation code** — `src/execsql/metacommands/upsert.py` and dispatch registration
|
|
87
|
+
4. **Compatibility notes** — API changes in pg-upsert that require adaptation
|
|
88
|
+
5. **Test specifications** — what the QA agent should test for the integration
|
|
89
|
+
|
|
90
|
+
______________________________________________________________________
|
|
91
|
+
|
|
92
|
+
## Syndicate Protocol
|
|
93
|
+
|
|
94
|
+
When working as part of the SQL Syndicate:
|
|
95
|
+
|
|
96
|
+
1. Read your briefing from `.claude/comms/briefings/liaison-*.md`
|
|
97
|
+
2. Do your research across both codebases
|
|
98
|
+
3. Write your report to `.claude/comms/reports/liaison-{YYYY-MM-DD}.md`
|
|
99
|
+
4. Write integration artifacts to `.claude/plans/` (for design) or `.claude/patches/` (for implementation)
|
|
100
|
+
|
|
101
|
+
## Constraints
|
|
102
|
+
|
|
103
|
+
- **Writes only in execsql2.** You may read `../pg-upsert/` freely but never modify files there.
|
|
104
|
+
- **pg-upsert is a moving target.** Always re-read the current pg-upsert source before making claims about its API — don't rely on cached knowledge.
|
|
105
|
+
- **Optional means optional.** Never add pg-upsert to execsql2's required dependencies. Use `extras_require` / optional dependency groups.
|
|
106
|
+
- **Preserve existing upsert templates.** The SQL templates in `templates/pg_upsert.sql` must continue to work. The metacommand is an alternative, not a replacement.
|
|
107
|
+
- Follow all execsql2 project constraints from CLAUDE.md (ruff, Python 3.10+, coverage floor, changelog, docs, divergence tracking).
|
|
@@ -13,6 +13,29 @@ ______________________________________________________________________
|
|
|
13
13
|
|
|
14
14
|
______________________________________________________________________
|
|
15
15
|
|
|
16
|
+
## [2.12.3] - 2026-04-02
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Performance: split `set_system_vars()` into static (once per script + on CONNECT/CHDIR) and dynamic (per statement) — eliminates ~14 redundant `add_substitution` calls and 2 `Path.resolve()` filesystem syscalls per statement.
|
|
21
|
+
- Performance: `$RANDOM` and `$UUID` are now lazy — computed only when actually referenced in a statement, not generated unconditionally for every statement.
|
|
22
|
+
- Performance: `LineDelimiter.delimited()` caches `quote_all_text` at construction time instead of reading `_state.conf` via module proxy on every row during export.
|
|
23
|
+
- Performance: CSV/TSV import uses Python's `csv` module as a fast path for standard delimited formats (comma, tab, semicolon, pipe) with doubled-quote escaping. Falls back to the character-at-a-time parser for non-standard formats (space-delimiter collapsing, escape characters).
|
|
24
|
+
|
|
25
|
+
______________________________________________________________________
|
|
26
|
+
|
|
27
|
+
## [2.12.2] - 2026-04-02
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Documentation: keyring setup guide for headless Linux servers (encrypted and plaintext file backends) in the Security reference page, with a cross-reference from the Installation page.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- `ASSERT` failures now report `**** Assertion failed.` instead of `**** Error in metacommand.` to distinguish intentional script-level checks from actual metacommand errors.
|
|
36
|
+
|
|
37
|
+
______________________________________________________________________
|
|
38
|
+
|
|
16
39
|
## [2.12.1] - 2026-04-02
|
|
17
40
|
|
|
18
41
|
### Changed
|
|
@@ -22,6 +22,7 @@ A multi-agent system where specialized agents collaborate to improve, extend, de
|
|
|
22
22
|
- `.claude/docs-drafts/` — Scribe's documentation drafts
|
|
23
23
|
- `.claude/releases/` — Herald's release notes and changelog entries
|
|
24
24
|
- `.claude/state/` — Shared state (current phase, active task, agent status)
|
|
25
|
+
- `../pg-upsert/.claude/plans/` — pg-upsert integration plan (read by The Liaison)
|
|
25
26
|
|
|
26
27
|
## Communication Protocol
|
|
27
28
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: execsql2
|
|
3
|
-
Version: 2.12.
|
|
3
|
+
Version: 2.12.3
|
|
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
|
+
Project-URL: Homepage, https://execsql2.readthedocs.io
|
|
5
6
|
Project-URL: Repository, https://github.com/geocoug/execsql
|
|
6
7
|
Project-URL: Issues, https://github.com/geocoug/execsql/issues
|
|
7
8
|
Author-email: Dreas Nielsen <cortice@tutanota.com>
|
|
@@ -163,6 +163,12 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
|
|
|
163
163
|
|
|
164
164
|
- **Cycle detection** — `substitute_vars()` raises an error after 100 iterations to prevent infinite loops when variables reference each other cyclically. Upstream had no protection.
|
|
165
165
|
- **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.
|
|
166
|
+
- **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.
|
|
167
|
+
- **Static/dynamic system var split** — System substitution variables are split into static (set once per script and refreshed on CONNECT/CHDIR) and dynamic (refreshed per statement). Eliminates redundant `Path.resolve()` syscalls and database pool lookups per statement.
|
|
168
|
+
|
|
169
|
+
### CSV Import
|
|
170
|
+
|
|
171
|
+
- **Fast-path CSV reader** — Standard delimited imports (comma, tab, semicolon, pipe with doubled-quote escaping) now use Python's `csv` module. Non-standard formats (space-delimiter collapsing, escape characters) fall back to the original character-at-a-time parser.
|
|
166
172
|
|
|
167
173
|
### Database Adapters
|
|
168
174
|
|
|
@@ -174,6 +180,7 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
|
|
|
174
180
|
|
|
175
181
|
- **Exception hierarchy** — All custom exceptions inherit from `ExecSqlError`, enabling `except ExecSqlError` to catch any execsql-originated error.
|
|
176
182
|
- **Exception chaining** — All `raise` statements inside `except` blocks preserve the original traceback via `from`.
|
|
183
|
+
- **ASSERT error type** — `ASSERT` failures now use a dedicated `"assert"` error type that produces `**** Assertion failed.` instead of `**** Error in metacommand.`. This distinguishes intentional script-level checks from actual metacommand errors. Upstream did not have `ASSERT`.
|
|
177
184
|
|
|
178
185
|
______________________________________________________________________
|
|
179
186
|
|
|
@@ -30,3 +30,7 @@ pip install "execsql2[all]" # Everything
|
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
In addition to the *execsql* program itself, additional Python libraries may need to be installed to use *execsql* with specific types of DBMSs and spreadsheets. The additional libraries that may be needed are listed in the [Requirements](requirements.md#requirements) section.
|
|
33
|
+
|
|
34
|
+
!!! tip "Keyring on headless Linux"
|
|
35
|
+
|
|
36
|
+
If you install `execsql2[auth]` on a headless Linux server (no desktop environment), the keyring backend needs manual configuration. See [Keyring Platform Setup](../reference/security.md#keyring_setup) for instructions.
|
|
@@ -56,7 +56,9 @@ ASSERT <condition> "<failure message>"
|
|
|
56
56
|
ASSERT <condition> '<failure message>'
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
Evaluates `<condition>` using the same expression engine as [IF](#if_cmd). If the condition is `True`, execution continues silently (and the result is written to the log). If the condition is `False`, an error is raised with the provided failure message. If no message is supplied, the default message is `Assertion failed: <condition>`.
|
|
59
|
+
Evaluates `<condition>` using the same expression engine as [IF](#if_cmd). If the condition is `True`, execution continues silently (and the result is written to the log). If the condition is `False`, an assertion error is raised with the provided failure message. If no message is supplied, the default message is `Assertion failed: <condition>`.
|
|
60
|
+
|
|
61
|
+
A failed assertion produces `**** Assertion failed.` (not `**** Error in metacommand.`) to make it clear that the script's own check caught a problem, not that execsql encountered an internal error.
|
|
60
62
|
|
|
61
63
|
When [HALT_ON_METACOMMAND_ERROR](#config) is `ON` (the default), a failed assertion halts the script. When it is `OFF`, execution continues after the failure is logged.
|
|
62
64
|
|
|
@@ -29,7 +29,7 @@ When execsql needs a database password and none is stored or configured, it prom
|
|
|
29
29
|
|
|
30
30
|
### OS credential store (keyring)
|
|
31
31
|
|
|
32
|
-
When the optional `keyring` package is installed (`pip install execsql2[auth]`), execsql checks the OS credential store before prompting. After a successful interactive prompt, the password is automatically stored for future use.
|
|
32
|
+
When the optional `keyring` package is installed (`pip install execsql2[auth]`), execsql checks the OS credential store before prompting. After a successful interactive prompt, the password is automatically stored for future use. Keyring service names follow the pattern:
|
|
33
33
|
|
|
34
34
|
```text
|
|
35
35
|
execsql/<db_type>/<server>/<database>
|
|
@@ -37,6 +37,64 @@ execsql/<db_type>/<server>/<database>
|
|
|
37
37
|
|
|
38
38
|
To disable keyring integration, set `use_keyring = No` in the `[connect]` section of `execsql.conf`.
|
|
39
39
|
|
|
40
|
+
#### Platform setup { #keyring_setup }
|
|
41
|
+
|
|
42
|
+
**macOS** — Works out of the box. Keyring uses the macOS Keychain automatically.
|
|
43
|
+
|
|
44
|
+
**Windows** — Works out of the box. Keyring uses the Windows Credential Manager automatically.
|
|
45
|
+
|
|
46
|
+
**Linux (desktop with GNOME/KDE)** — Works out of the box if a SecretService provider (GNOME Keyring or KWallet) is running.
|
|
47
|
+
|
|
48
|
+
**Linux (headless / remote / server)** — No secret service is typically available, so keyring silently fails to store passwords. You need to configure a file-based backend. There are two options:
|
|
49
|
+
|
|
50
|
+
##### Option A: Encrypted file backend (recommended) { #keyring_encrypted }
|
|
51
|
+
|
|
52
|
+
Passwords are stored encrypted on disk. Requires a master password the first time keyring is used per session.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install keyrings.alt pycryptodome
|
|
56
|
+
mkdir -p ~/.config/python_keyring
|
|
57
|
+
cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
|
|
58
|
+
[backend]
|
|
59
|
+
default-keyring=keyrings.alt.file.EncryptedKeyring
|
|
60
|
+
EOF
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The encrypted keyring file is stored at `~/.local/share/python_keyring/crypted_pass.cfg`. You will be prompted for a master password once per session (e.g., when you first run execsql after logging in).
|
|
64
|
+
|
|
65
|
+
##### Option B: Plaintext file backend (no prompts) { #keyring_plaintext }
|
|
66
|
+
|
|
67
|
+
Passwords are stored in plain text on disk. No master password is needed — execsql will never prompt for a password after the first successful entry.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install keyrings.alt
|
|
71
|
+
mkdir -p ~/.config/python_keyring
|
|
72
|
+
cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
|
|
73
|
+
[backend]
|
|
74
|
+
default-keyring=keyrings.alt.file.PlaintextKeyring
|
|
75
|
+
EOF
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Passwords are stored at `~/.local/share/python_keyring/keyring_pass.cfg`. Restrict access to this file:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
chmod 600 ~/.local/share/python_keyring/keyring_pass.cfg
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
!!! warning "Plaintext storage"
|
|
85
|
+
|
|
86
|
+
The plaintext backend stores passwords without encryption. Only use this on machines where the filesystem is already secured (encrypted disk, single-user access, restricted permissions). Prefer the encrypted backend when possible.
|
|
87
|
+
|
|
88
|
+
##### Verifying keyring setup { #keyring_verify }
|
|
89
|
+
|
|
90
|
+
After configuring a backend, verify it works:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python -c "import keyring; keyring.set_password('test', 'user', 'pw'); print(keyring.get_password('test', 'user'))"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This should print `pw` without errors. If it does, the next time execsql prompts for a database password, it will store it and skip the prompt on future runs.
|
|
97
|
+
|
|
40
98
|
### `enc_password` in execsql.conf
|
|
41
99
|
|
|
42
100
|
!!! warning "Obfuscation only — not encryption"
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "execsql2"
|
|
7
|
-
version = "2.12.
|
|
7
|
+
version = "2.12.3"
|
|
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" }
|
|
@@ -91,6 +91,7 @@ execsql = "execsql.cli:_legacy_main"
|
|
|
91
91
|
execsql-format = "execsql.format:main"
|
|
92
92
|
|
|
93
93
|
[project.urls]
|
|
94
|
+
Homepage = "https://execsql2.readthedocs.io"
|
|
94
95
|
Repository = "https://github.com/geocoug/execsql"
|
|
95
96
|
Issues = "https://github.com/geocoug/execsql/issues"
|
|
96
97
|
|
|
@@ -158,7 +159,7 @@ skip-magic-trailing-comma = false
|
|
|
158
159
|
line-ending = "auto"
|
|
159
160
|
|
|
160
161
|
[tool.bumpversion]
|
|
161
|
-
current_version = "2.12.
|
|
162
|
+
current_version = "2.12.3"
|
|
162
163
|
commit = true
|
|
163
164
|
commit_args = "--no-verify"
|
|
164
165
|
tag = true
|
|
@@ -686,6 +686,13 @@ class DatabasePool:
|
|
|
686
686
|
)
|
|
687
687
|
self.pool[db_alias].close()
|
|
688
688
|
self.pool[db_alias] = db_obj
|
|
689
|
+
# Refresh static system vars so $DB_NAME, $DB_USER, etc. reflect the new connection.
|
|
690
|
+
try:
|
|
691
|
+
from execsql.script.engine import set_static_system_vars
|
|
692
|
+
|
|
693
|
+
set_static_system_vars()
|
|
694
|
+
except Exception:
|
|
695
|
+
pass # Engine not yet initialized (early startup).
|
|
689
696
|
|
|
690
697
|
def aliases(self) -> list[str]:
|
|
691
698
|
"""Return a list of all currently registered database aliases."""
|
|
@@ -132,6 +132,8 @@ class ErrInfo(ExecSqlError):
|
|
|
132
132
|
|
|
133
133
|
if self.type == "db":
|
|
134
134
|
self.error_message = "**** Error in SQL statement."
|
|
135
|
+
elif self.type == "assert":
|
|
136
|
+
self.error_message = "**** Assertion failed."
|
|
135
137
|
elif self.type == "cmd":
|
|
136
138
|
self.error_message = "**** Error in metacommand."
|
|
137
139
|
elif self.type == "log":
|
|
@@ -40,6 +40,7 @@ class LineDelimiter:
|
|
|
40
40
|
self.delimiter = delim
|
|
41
41
|
self.joinchar = delim if delim else ""
|
|
42
42
|
self.quotechar = quote
|
|
43
|
+
self.quote_all_text = _state.conf.quote_all_text if _state.conf else False
|
|
43
44
|
if quote:
|
|
44
45
|
if escchar:
|
|
45
46
|
self.quotedquote = escchar + quote
|
|
@@ -50,13 +51,12 @@ class LineDelimiter:
|
|
|
50
51
|
|
|
51
52
|
def delimited(self, datarow: Any, add_newline: bool = True) -> str:
|
|
52
53
|
"""Format a sequence of values as a single delimited text line."""
|
|
53
|
-
conf = _state.conf
|
|
54
54
|
if self.quotechar:
|
|
55
55
|
d_row = []
|
|
56
56
|
for e in datarow:
|
|
57
57
|
if isinstance(e, str):
|
|
58
58
|
if (
|
|
59
|
-
|
|
59
|
+
self.quote_all_text
|
|
60
60
|
or (self.quotechar in e)
|
|
61
61
|
or (self.delimiter is not None and self.delimiter in e)
|
|
62
62
|
or ("\n" in e)
|
|
@@ -609,10 +609,52 @@ class CsvFile(EncodedFile):
|
|
|
609
609
|
raise ErrInfo("error", other_msg=", ".join(self.parse_errors))
|
|
610
610
|
return elements
|
|
611
611
|
|
|
612
|
+
def _can_use_fast_csv_reader(self) -> bool:
|
|
613
|
+
"""Return True if the detected format is compatible with Python's csv module."""
|
|
614
|
+
# The csv module handles comma/tab delimiters with doubled-quote escaping.
|
|
615
|
+
# It cannot handle: space-delimiter collapsing, escape chars, or no delimiter.
|
|
616
|
+
if self.delimiter is None or self.delimiter == " ":
|
|
617
|
+
return False
|
|
618
|
+
return self.escapechar is None
|
|
619
|
+
|
|
612
620
|
def reader(self) -> Any:
|
|
613
621
|
"""Yield parsed rows from the file as lists of field values."""
|
|
614
622
|
conf = _state.conf
|
|
615
623
|
self.evaluate_line_format()
|
|
624
|
+
if self._can_use_fast_csv_reader():
|
|
625
|
+
yield from self._fast_reader(conf)
|
|
626
|
+
else:
|
|
627
|
+
yield from self._slow_reader(conf)
|
|
628
|
+
|
|
629
|
+
def _fast_reader(self, conf: Any) -> Any:
|
|
630
|
+
"""Read using Python's csv module (fast path for standard delimited formats)."""
|
|
631
|
+
import csv
|
|
632
|
+
|
|
633
|
+
f = self.openclean("rt")
|
|
634
|
+
try:
|
|
635
|
+
csv_reader = csv.reader(
|
|
636
|
+
f,
|
|
637
|
+
delimiter=self.delimiter,
|
|
638
|
+
quotechar=self.quotechar,
|
|
639
|
+
doublequote=True,
|
|
640
|
+
strict=False,
|
|
641
|
+
)
|
|
642
|
+
for elements in csv_reader:
|
|
643
|
+
if len(elements) == 0:
|
|
644
|
+
break
|
|
645
|
+
# Normalize empty strings to None for parity with the slow reader.
|
|
646
|
+
elements = [e if e != "" else None for e in elements]
|
|
647
|
+
if conf.del_empty_cols and len(self.blank_cols) > 0:
|
|
648
|
+
blanks = copy.copy(self.blank_cols)
|
|
649
|
+
while len(blanks) > 0:
|
|
650
|
+
b = blanks.pop()
|
|
651
|
+
del elements[b]
|
|
652
|
+
yield elements
|
|
653
|
+
finally:
|
|
654
|
+
f.close()
|
|
655
|
+
|
|
656
|
+
def _slow_reader(self, conf: Any) -> Any:
|
|
657
|
+
"""Read using the character-at-a-time state machine (fallback for non-standard formats)."""
|
|
616
658
|
f = self.openclean("rt")
|
|
617
659
|
line_no = 0
|
|
618
660
|
try:
|
|
@@ -66,7 +66,7 @@ def x_assert(**kwargs: Any) -> None:
|
|
|
66
66
|
if _state.exec_log is not None:
|
|
67
67
|
_state.exec_log.log_user_msg(f"ASSERT passed: {condition}")
|
|
68
68
|
else:
|
|
69
|
-
raise ErrInfo(type="
|
|
69
|
+
raise ErrInfo(type="assert", other_msg=message)
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
def x_if(**kwargs: Any) -> None:
|
|
@@ -241,6 +241,10 @@ def x_cd(**kwargs: Any) -> None:
|
|
|
241
241
|
os.chdir(new_dir)
|
|
242
242
|
script, lno = current_script_line()
|
|
243
243
|
_state.exec_log.log_status_info(f"Current directory changed to {new_dir} at line {lno} of {script}")
|
|
244
|
+
if _state.subvars is not None:
|
|
245
|
+
from execsql.script.engine import set_static_system_vars
|
|
246
|
+
|
|
247
|
+
set_static_system_vars()
|
|
244
248
|
return None
|
|
245
249
|
|
|
246
250
|
|
|
@@ -63,6 +63,8 @@ from execsql.script.engine import (
|
|
|
63
63
|
read_sqlfile,
|
|
64
64
|
read_sqlstring,
|
|
65
65
|
runscripts,
|
|
66
|
+
set_dynamic_system_vars,
|
|
67
|
+
set_static_system_vars,
|
|
66
68
|
set_system_vars,
|
|
67
69
|
substitute_vars,
|
|
68
70
|
)
|
|
@@ -86,6 +88,8 @@ __all__ = [
|
|
|
86
88
|
"CommandListUntilLoop",
|
|
87
89
|
"ScriptFile",
|
|
88
90
|
"ScriptExecSpec",
|
|
91
|
+
"set_dynamic_system_vars",
|
|
92
|
+
"set_static_system_vars",
|
|
89
93
|
"set_system_vars",
|
|
90
94
|
"substitute_vars",
|
|
91
95
|
"runscripts",
|
|
@@ -704,9 +704,45 @@ class ScriptExecSpec:
|
|
|
704
704
|
# ---------------------------------------------------------------------------
|
|
705
705
|
|
|
706
706
|
|
|
707
|
-
def
|
|
708
|
-
"""
|
|
709
|
-
|
|
707
|
+
def set_static_system_vars() -> None:
|
|
708
|
+
"""Set system substitution variables that only change on CONNECT or CHDIR.
|
|
709
|
+
|
|
710
|
+
Called once before the execution loop. These values are expensive to compute
|
|
711
|
+
(filesystem syscalls, database pool lookups) but rarely change — only on
|
|
712
|
+
``CONNECT``, ``USE``, or ``CHDIR`` metacommands. The ``runscripts()`` loop
|
|
713
|
+
calls this once up front; metacommand handlers that change the connection or
|
|
714
|
+
working directory should call it again afterward.
|
|
715
|
+
"""
|
|
716
|
+
import random
|
|
717
|
+
|
|
718
|
+
cwd = str(Path(".").resolve())
|
|
719
|
+
_state.subvars.add_substitution("$CURRENT_DIR", cwd)
|
|
720
|
+
_state.subvars.add_substitution("$CURRENT_PATH", cwd + os.sep)
|
|
721
|
+
_state.subvars.add_substitution("$CURRENT_ALIAS", _state.dbs.current_alias())
|
|
722
|
+
db = _state.dbs.current()
|
|
723
|
+
_state.subvars.add_substitution("$DB_USER", db.user if db.user else "")
|
|
724
|
+
_state.subvars.add_substitution(
|
|
725
|
+
"$DB_SERVER",
|
|
726
|
+
db.server_name if db.server_name else "",
|
|
727
|
+
)
|
|
728
|
+
_state.subvars.add_substitution("$DB_NAME", db.db_name)
|
|
729
|
+
_state.subvars.add_substitution("$DB_NEED_PWD", "TRUE" if db.need_passwd else "FALSE")
|
|
730
|
+
_state.subvars.add_substitution("$VERSION1", str(_state.primary_vno))
|
|
731
|
+
_state.subvars.add_substitution("$VERSION2", str(_state.secondary_vno))
|
|
732
|
+
_state.subvars.add_substitution("$VERSION3", str(_state.tertiary_vno))
|
|
733
|
+
# Register lazy providers for $RANDOM and $UUID — computed only when referenced.
|
|
734
|
+
_state.subvars.register_lazy("$random", lambda: str(random.random()))
|
|
735
|
+
_state.subvars.register_lazy("$uuid", lambda: str(uuid.uuid4()))
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def set_dynamic_system_vars() -> None:
|
|
739
|
+
"""Refresh system substitution variables that change every statement.
|
|
740
|
+
|
|
741
|
+
Called once per statement in the execution loop. Includes cheap boolean-to-string
|
|
742
|
+
conversions for halt states and autocommit (which can change on any CONFIG or
|
|
743
|
+
AUTOCOMMIT metacommand) plus ``$TIMER`` and lazy-variable cache reset.
|
|
744
|
+
"""
|
|
745
|
+
# Halt/config state vars — cheap to set, can change on any CONFIG metacommand.
|
|
710
746
|
_state.subvars.add_substitution("$CANCEL_HALT_STATE", "ON" if _state.status.cancel_halt else "OFF")
|
|
711
747
|
_state.subvars.add_substitution("$ERROR_HALT_STATE", "ON" if _state.status.halt_on_err else "OFF")
|
|
712
748
|
_state.subvars.add_substitution(
|
|
@@ -718,27 +754,22 @@ def set_system_vars() -> None:
|
|
|
718
754
|
"ON" if _state.conf.gui_wait_on_error_halt else "OFF",
|
|
719
755
|
)
|
|
720
756
|
_state.subvars.add_substitution("$CONSOLE_WAIT_WHEN_DONE_STATE", "ON" if _state.conf.gui_wait_on_exit else "OFF")
|
|
721
|
-
# $CURRENT_TIME is set per-statement in run_and_increment() for accuracy.
|
|
722
|
-
_state.subvars.add_substitution("$CURRENT_DIR", str(Path(".").resolve()))
|
|
723
|
-
_state.subvars.add_substitution("$CURRENT_PATH", str(Path(".").resolve()) + os.sep)
|
|
724
|
-
_state.subvars.add_substitution("$CURRENT_ALIAS", _state.dbs.current_alias())
|
|
725
757
|
db = _state.dbs.current()
|
|
726
758
|
_state.subvars.add_substitution("$AUTOCOMMIT_STATE", "ON" if db.autocommit else "OFF")
|
|
759
|
+
# $CURRENT_TIME is set per-statement in run_and_increment() for accuracy.
|
|
727
760
|
_state.subvars.add_substitution("$TIMER", str(datetime.timedelta(seconds=_state.timer.elapsed())))
|
|
728
|
-
_state.subvars.
|
|
729
|
-
_state.subvars.add_substitution(
|
|
730
|
-
"$DB_SERVER",
|
|
731
|
-
db.server_name if db.server_name else "",
|
|
732
|
-
)
|
|
733
|
-
_state.subvars.add_substitution("$DB_NAME", db.db_name)
|
|
734
|
-
_state.subvars.add_substitution("$DB_NEED_PWD", "TRUE" if db.need_passwd else "FALSE")
|
|
735
|
-
import random
|
|
761
|
+
_state.subvars.clear_lazy_cache()
|
|
736
762
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
763
|
+
|
|
764
|
+
def set_system_vars() -> None:
|
|
765
|
+
"""Refresh all built-in system substitution variables.
|
|
766
|
+
|
|
767
|
+
Convenience wrapper that calls both :func:`set_static_system_vars` and
|
|
768
|
+
:func:`set_dynamic_system_vars`. Retained for backward compatibility with
|
|
769
|
+
tests and any external callers.
|
|
770
|
+
"""
|
|
771
|
+
set_static_system_vars()
|
|
772
|
+
set_dynamic_system_vars()
|
|
742
773
|
|
|
743
774
|
|
|
744
775
|
_MAX_SUBSTITUTION_DEPTH = 100
|
|
@@ -779,11 +810,12 @@ def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str
|
|
|
779
810
|
|
|
780
811
|
def runscripts() -> None:
|
|
781
812
|
"""Drive execution until the command-list stack is empty."""
|
|
782
|
-
#
|
|
783
|
-
#
|
|
813
|
+
# Set static vars once before the loop; they are refreshed by metacommand
|
|
814
|
+
# handlers (CONNECT, CONFIG, AUTOCOMMIT, CHDIR) when state changes.
|
|
815
|
+
set_static_system_vars()
|
|
784
816
|
while len(_state.commandliststack) > 0:
|
|
785
817
|
current_cmds = _state.commandliststack[-1]
|
|
786
|
-
|
|
818
|
+
set_dynamic_system_vars()
|
|
787
819
|
try:
|
|
788
820
|
current_cmds.run_next()
|
|
789
821
|
except StopIteration:
|
|
@@ -89,6 +89,7 @@ class SubVarSet:
|
|
|
89
89
|
# compatibility with external code.
|
|
90
90
|
def __init__(self) -> None:
|
|
91
91
|
self._subs_dict: dict[str, Any] = {}
|
|
92
|
+
self._lazy_providers: dict[str, Any] = {}
|
|
92
93
|
self.prefix_list: list[str] = ["$", "&", "@"]
|
|
93
94
|
# Don't construct/compile on init because deepcopy() can't handle compiled regexes.
|
|
94
95
|
self.var_rx = None
|
|
@@ -120,6 +121,30 @@ class SubVarSet:
|
|
|
120
121
|
if not self.var_name_ok(varname.lower()):
|
|
121
122
|
raise ErrInfo("error", other_msg=f"Invalid variable name ({varname}) in this context.")
|
|
122
123
|
|
|
124
|
+
def register_lazy(self, varname: str, provider: Any) -> None:
|
|
125
|
+
"""Register a lazy variable whose value is computed on first access per cycle.
|
|
126
|
+
|
|
127
|
+
The *provider* callable is invoked only when the variable is actually
|
|
128
|
+
referenced (via :meth:`substitute`, :meth:`varvalue`, etc.). The result
|
|
129
|
+
is cached in ``_subs_dict`` until :meth:`clear_lazy_cache` is called.
|
|
130
|
+
"""
|
|
131
|
+
self.check_var_name(varname)
|
|
132
|
+
self._lazy_providers[varname.lower()] = provider
|
|
133
|
+
|
|
134
|
+
def clear_lazy_cache(self) -> None:
|
|
135
|
+
"""Remove materialized lazy values so they regenerate on next access."""
|
|
136
|
+
for key in self._lazy_providers:
|
|
137
|
+
self._subs_dict.pop(key, None)
|
|
138
|
+
|
|
139
|
+
def _materialize_lazy(self, varname: str) -> str | None:
|
|
140
|
+
"""If *varname* has a lazy provider, invoke it, cache the result, and return it."""
|
|
141
|
+
provider = self._lazy_providers.get(varname)
|
|
142
|
+
if provider is not None:
|
|
143
|
+
value = str(provider())
|
|
144
|
+
self._subs_dict[varname] = value
|
|
145
|
+
return value
|
|
146
|
+
return None
|
|
147
|
+
|
|
123
148
|
def remove_substitution(self, template_str: str) -> None:
|
|
124
149
|
"""Remove the variable named *template_str* from the substitution pool."""
|
|
125
150
|
self.check_var_name(template_str)
|
|
@@ -143,7 +168,11 @@ class SubVarSet:
|
|
|
143
168
|
def varvalue(self, varname: str) -> str | None:
|
|
144
169
|
"""Return the value of *varname*, or ``None`` if it is not defined."""
|
|
145
170
|
self.check_var_name(varname)
|
|
146
|
-
|
|
171
|
+
key = varname.lower()
|
|
172
|
+
val = self._subs_dict.get(key)
|
|
173
|
+
if val is None and key in self._lazy_providers:
|
|
174
|
+
return self._materialize_lazy(key)
|
|
175
|
+
return val
|
|
147
176
|
|
|
148
177
|
def increment_by(self, varname: str, numeric_increment: Any) -> None:
|
|
149
178
|
self.check_var_name(varname)
|
|
@@ -165,13 +194,15 @@ class SubVarSet:
|
|
|
165
194
|
def sub_exists(self, template_str: str) -> bool:
|
|
166
195
|
"""Return True if the variable named *template_str* is defined."""
|
|
167
196
|
self.check_var_name(template_str)
|
|
168
|
-
|
|
197
|
+
key = template_str.lower()
|
|
198
|
+
return key in self._subs_dict or key in self._lazy_providers
|
|
169
199
|
|
|
170
200
|
def merge(self, other_subvars: SubVarSet | None) -> SubVarSet:
|
|
171
201
|
"""Return a new SubVarSet with this object's variables merged with other_subvars."""
|
|
172
202
|
if other_subvars is not None:
|
|
173
203
|
newsubs = SubVarSet()
|
|
174
204
|
newsubs._subs_dict = {**self._subs_dict, **other_subvars._subs_dict}
|
|
205
|
+
newsubs._lazy_providers = {**self._lazy_providers, **other_subvars._lazy_providers}
|
|
175
206
|
newsubs.prefix_list = list(set(self.prefix_list + other_subvars.prefix_list))
|
|
176
207
|
newsubs.compile_var_rx()
|
|
177
208
|
return newsubs
|
|
@@ -201,6 +232,8 @@ class SubVarSet:
|
|
|
201
232
|
m = self._TOKEN_RX.search(command_str)
|
|
202
233
|
while m:
|
|
203
234
|
varname = m.group("varname").lower()
|
|
235
|
+
if varname not in self._subs_dict and varname in self._lazy_providers:
|
|
236
|
+
self._materialize_lazy(varname)
|
|
204
237
|
if varname in self._subs_dict:
|
|
205
238
|
sub = self._subs_dict[varname]
|
|
206
239
|
if sub is None:
|