mycli 2.26.0__tar.gz → 2.27.0__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.
- {mycli-2.26.0 → mycli-2.27.0}/.github/workflows/ci.yml +4 -4
- {mycli-2.26.0 → mycli-2.27.0}/.github/workflows/publish.yml +4 -4
- {mycli-2.26.0 → mycli-2.27.0}/.github/workflows/typecheck.yml +1 -1
- {mycli-2.26.0 → mycli-2.27.0}/AGENTS.md +1 -0
- {mycli-2.26.0/mycli.egg-info → mycli-2.27.0}/PKG-INFO +1 -1
- {mycli-2.26.0 → mycli-2.27.0}/changelog.md +15 -0
- {mycli-2.26.0 → mycli-2.27.0}/doc/transforms.md +0 -1
- {mycli-2.26.0 → mycli-2.27.0}/docker/resources/test_db-1.0.7.tar.gz +1 -1
- {mycli-2.26.0 → mycli-2.27.0}/mycli/constants.py +11 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/main_modes/repl.py +66 -26
- {mycli-2.26.0 → mycli-2.27.0}/mycli/myclirc +6 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/output.py +14 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/hybrid_redirection.py +75 -27
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/polars_completion.py +3 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/polars_transform.py +36 -1
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/special/iocommands.py +106 -22
- mycli-2.27.0/mycli/query_runner.py +355 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/sqlexecute.py +32 -1
- {mycli-2.26.0 → mycli-2.27.0/mycli.egg-info}/PKG-INFO +1 -1
- {mycli-2.26.0 → mycli-2.27.0}/mycli.egg-info/SOURCES.txt +2 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli.egg-info/scm_file_list.json +2 -0
- mycli-2.27.0/mycli.egg-info/scm_version.json +8 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/myclirc +6 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_hybrid_redirection.py +74 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_main_modes_repl.py +256 -3
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_output.py +38 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_polars_completion.py +5 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_polars_transform.py +80 -0
- mycli-2.27.0/test/pytests/test_query_runner.py +855 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_special_iocommands.py +294 -11
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_sqlexecute.py +143 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/utils.py +1 -1
- mycli-2.26.0/mycli.egg-info/scm_version.json +0 -8
- {mycli-2.26.0 → mycli-2.27.0}/.git-blame-ignore-revs +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/.github/dependabot.yml +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/.github/workflows/lint.yml +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/.gitignore +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/AUTHORS.md +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/CONTRIBUTING.md +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/LICENSE.txt +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/MANIFEST.in +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/README.md +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/SPONSORS.md +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/doc/key_bindings.md +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/doc/llm.md +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/doc/screenshots/main.gif +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/doc/screenshots/polars_describe.png +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/doc/screenshots/polars_group_by.png +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/doc/screenshots/polars_histogram.png +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/doc/screenshots/polars_pivot.png +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/doc/screenshots/tables.png +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/docker/Dockerfile +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/docker/resources/test_db_license.md +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/AUTHORS +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/SPONSORS +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/TIPS +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/__init__.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/app_state.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/boundary_tunnel.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/cli_runner.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/clibuffer.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/client.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/client_commands.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/client_connection.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/client_query.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/clistyle.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/clitoolbar.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/compat.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/completion_refresher.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/config.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/key_bindings.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/keyring_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/kubectl_tunnel.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/lexer.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/macos_keychain.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/main.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/main_modes/batch.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/main_modes/completions.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/main_modes/execute.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/__init__.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/batch_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/cli_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/completion_engine.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/filepaths.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/interactive_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/key_binding_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/ptoolkit/__init__.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/ptoolkit/fzf.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/ptoolkit/history.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/ptoolkit/utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/special/__init__.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/special/dbcommands.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/special/delimitercommand.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/special/dsn_aliases.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/special/favoritequeries.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/special/llm.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/special/main.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/special/source.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/special/utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/sql_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/sqlresult.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/string_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/password_sources.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/resources/completions/bash/mycli +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/resources/completions/fish/mycli.fish +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/resources/completions/zsh/_mycli +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/schema_prefetcher.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/sqlcompleter.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/ssh_tunnel.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/types.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli/vault.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli.egg-info/requires.txt +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/pyproject.toml +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/setup.cfg +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/__init__.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/__init__.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/auto_vertical.feature +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/basic_commands.feature +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/connection.feature +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/crud_database.feature +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/crud_table.feature +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/db_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/environment.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/fixture_data/help_commands.txt +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/fixture_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/iocommands.feature +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/named_queries.feature +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/specials.feature +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/steps/__init__.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/steps/auto_vertical.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/steps/basic_commands.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/steps/connection.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/steps/crud_database.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/steps/crud_table.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/steps/iocommands.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/steps/named_queries.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/steps/specials.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/steps/utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/steps/wrappers.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/features/wrappager.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/mylogin.cnf +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/conftest.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_app_state.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_bash_completion.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_batch_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_boundary_tunnel.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_checkup.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_cli_runner.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_cli_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_clibuffer.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_client.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_client_commands.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_client_connection.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_client_query.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_clistyle.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_clitoolbar.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_completion_engine.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_completion_refresher.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_config.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_dsn_aliases.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_filepaths.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_fish_completion.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_key_bindings.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_keyring_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_kubectl_tunnel.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_macos_keychain.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_main.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_main_modes_batch.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_main_modes_execute.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_naive_completion.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_password_sources.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_ptoolkit_history.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_ptoolkit_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_special_dbcommands.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_special_init.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_special_llm.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_special_main.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_special_source.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_special_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_sql_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_sqlcompleter.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_sqlresult.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_ssh_tunnel.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_string_utils.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_tabular_output.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_vault.py +0 -0
- {mycli-2.26.0 → mycli-2.27.0}/test/pytests/test_zsh_completion.py +0 -0
|
@@ -24,7 +24,7 @@ jobs:
|
|
|
24
24
|
steps:
|
|
25
25
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
26
26
|
|
|
27
|
-
- uses: astral-sh/setup-uv@
|
|
27
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
28
28
|
with:
|
|
29
29
|
version: "latest"
|
|
30
30
|
|
|
@@ -66,7 +66,7 @@ jobs:
|
|
|
66
66
|
steps:
|
|
67
67
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
68
68
|
|
|
69
|
-
- uses: astral-sh/setup-uv@
|
|
69
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
70
70
|
with:
|
|
71
71
|
version: "latest"
|
|
72
72
|
|
|
@@ -116,7 +116,7 @@ jobs:
|
|
|
116
116
|
steps:
|
|
117
117
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
118
118
|
|
|
119
|
-
- uses: astral-sh/setup-uv@
|
|
119
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
120
120
|
with:
|
|
121
121
|
version: 'latest'
|
|
122
122
|
|
|
@@ -220,7 +220,7 @@ jobs:
|
|
|
220
220
|
steps:
|
|
221
221
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
222
222
|
|
|
223
|
-
- uses: astral-sh/setup-uv@
|
|
223
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
224
224
|
with:
|
|
225
225
|
version: "latest"
|
|
226
226
|
|
|
@@ -32,7 +32,7 @@ jobs:
|
|
|
32
32
|
|
|
33
33
|
steps:
|
|
34
34
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
35
|
-
- uses: astral-sh/setup-uv@
|
|
35
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
36
36
|
with:
|
|
37
37
|
version: "latest"
|
|
38
38
|
|
|
@@ -77,7 +77,7 @@ jobs:
|
|
|
77
77
|
steps:
|
|
78
78
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
79
79
|
|
|
80
|
-
- uses: astral-sh/setup-uv@
|
|
80
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
81
81
|
with:
|
|
82
82
|
version: "latest"
|
|
83
83
|
|
|
@@ -127,7 +127,7 @@ jobs:
|
|
|
127
127
|
steps:
|
|
128
128
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
129
129
|
|
|
130
|
-
- uses: astral-sh/setup-uv@
|
|
130
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
131
131
|
with:
|
|
132
132
|
version: 'latest'
|
|
133
133
|
|
|
@@ -230,7 +230,7 @@ jobs:
|
|
|
230
230
|
|
|
231
231
|
steps:
|
|
232
232
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
233
|
-
- uses: astral-sh/setup-uv@
|
|
233
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
234
234
|
with:
|
|
235
235
|
version: "latest"
|
|
236
236
|
|
|
@@ -56,6 +56,7 @@ A command line client for MySQL with auto-completion and syntax highlighting.
|
|
|
56
56
|
├── mycli/packages/string_utils.py # generic string utilities
|
|
57
57
|
├── mycli/packages/tabular_output/ # extends cli_helper with additional output formats
|
|
58
58
|
├── mycli/password_sources.py # password sources and precedence
|
|
59
|
+
├── mycli/query_runner.py # run queries with query state and timing updates
|
|
59
60
|
├── mycli/resources/completions/ # shell completions for CLI interface
|
|
60
61
|
├── mycli/ssh_tunnel.py # connecting over a tunnel with `--ssh-jump`
|
|
61
62
|
├── mycli/schema_prefetcher.py # background prefetcher for multi-schema auto-completion
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
v2.27.0 (2026/09/29)
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Features
|
|
5
|
+
--------
|
|
6
|
+
* Show query state and timings while waiting.
|
|
7
|
+
* Allow composing trailing shell redirections with `.|` transforms.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
Bug Fixes
|
|
11
|
+
--------
|
|
12
|
+
* Don't treat `-o` as a flag after `$>` redirects.
|
|
13
|
+
* Timed-out shell pipelines no longer report success.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
v2.26.0 (2026/09/26)
|
|
2
17
|
==============
|
|
3
18
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
1
3
|
HOME_URL = 'https://mycli.net'
|
|
2
4
|
REPO_URL = 'https://github.com/dbcli/mycli'
|
|
3
5
|
DOCS_URL = f'{HOME_URL}/docs'
|
|
@@ -51,3 +53,12 @@ MYSQL_ESCAPES = {
|
|
|
51
53
|
't': '\t',
|
|
52
54
|
'Z': '\x1a',
|
|
53
55
|
}
|
|
56
|
+
|
|
57
|
+
TTY_ERASE_LINE = '\r\x1b[2K'
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class QueryState(enum.StrEnum):
|
|
61
|
+
RENDERING = 'rendering'
|
|
62
|
+
TRANSFORMING = 'transforming'
|
|
63
|
+
UNKNOWN = 'unknown'
|
|
64
|
+
INITIAL = 'finding state'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Generator, Iterable
|
|
4
|
+
from contextlib import nullcontext
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
import functools
|
|
@@ -57,6 +58,7 @@ from mycli.constants import (
|
|
|
57
58
|
ER_MUST_CHANGE_PASSWORD,
|
|
58
59
|
HOME_URL,
|
|
59
60
|
ISSUES_URL,
|
|
61
|
+
QueryState,
|
|
60
62
|
)
|
|
61
63
|
from mycli.key_bindings import mycli_bindings
|
|
62
64
|
from mycli.lexer import MyCliLexer
|
|
@@ -76,6 +78,7 @@ from mycli.packages.polars_transform import (
|
|
|
76
78
|
run_polars_transform,
|
|
77
79
|
)
|
|
78
80
|
from mycli.packages.ptoolkit.history import FRECENCY_HISTORY_ENTRIES, FRECENCY_REFRESH_INTERVAL, FileHistoryWithTimestamp
|
|
81
|
+
from mycli.packages.special.iocommands import temporary_redirect
|
|
79
82
|
from mycli.packages.special.utils import format_uptime, get_ssl_version, get_uptime, get_warning_count
|
|
80
83
|
from mycli.packages.sql_utils import (
|
|
81
84
|
extract_new_password,
|
|
@@ -89,6 +92,7 @@ from mycli.packages.sql_utils import (
|
|
|
89
92
|
)
|
|
90
93
|
from mycli.packages.sqlresult import SQLResult
|
|
91
94
|
from mycli.packages.string_utils import sanitize_terminal_title
|
|
95
|
+
from mycli.query_runner import QueryCancelled, QueryRunner, runner_for
|
|
92
96
|
from mycli.sqlexecute import SQLExecute
|
|
93
97
|
from mycli.types import Query
|
|
94
98
|
|
|
@@ -452,6 +456,8 @@ def _output_results(
|
|
|
452
456
|
state: ReplState,
|
|
453
457
|
results: Iterable[SQLResult],
|
|
454
458
|
start: float,
|
|
459
|
+
*,
|
|
460
|
+
raise_interrupts: bool = False,
|
|
455
461
|
) -> None:
|
|
456
462
|
sqlexecute = mycli.sqlexecute
|
|
457
463
|
assert sqlexecute is not None
|
|
@@ -468,7 +474,9 @@ def _output_results(
|
|
|
468
474
|
return
|
|
469
475
|
paged_output = _single_paged_output_results(mycli, state, result_iterator, start)
|
|
470
476
|
try:
|
|
471
|
-
|
|
477
|
+
runner = runner_for(mycli)
|
|
478
|
+
with runner.suspend_display() if runner else nullcontext():
|
|
479
|
+
click.echo_via_pager(paged_output)
|
|
472
480
|
except KeyboardInterrupt:
|
|
473
481
|
pass
|
|
474
482
|
finally:
|
|
@@ -504,6 +512,8 @@ def _output_results(
|
|
|
504
512
|
watch_count += 1
|
|
505
513
|
|
|
506
514
|
if is_select(result.status_plain) and isinstance(result.rows, Cursor) and result.rows.rowcount > threshold:
|
|
515
|
+
if runner := runner_for(mycli):
|
|
516
|
+
runner.reset_progress()
|
|
507
517
|
mycli.echo(
|
|
508
518
|
f'The result set has more than {threshold} rows.',
|
|
509
519
|
fg='red',
|
|
@@ -537,14 +547,16 @@ def _output_results(
|
|
|
537
547
|
try:
|
|
538
548
|
mycli.output(formatted, result)
|
|
539
549
|
except KeyboardInterrupt:
|
|
540
|
-
|
|
550
|
+
if raise_interrupts:
|
|
551
|
+
raise
|
|
541
552
|
if mycli.beep_after_seconds > 0 and duration >= mycli.beep_after_seconds:
|
|
542
553
|
assert mycli.prompt_session is not None
|
|
543
554
|
mycli.prompt_session.output.bell()
|
|
544
555
|
if special.is_timing_enabled():
|
|
545
556
|
mycli.output_timing(f'Time: {duration:0.03f}s')
|
|
546
557
|
except KeyboardInterrupt:
|
|
547
|
-
|
|
558
|
+
if raise_interrupts:
|
|
559
|
+
raise
|
|
548
560
|
|
|
549
561
|
start = time.time()
|
|
550
562
|
result_count += 1
|
|
@@ -928,38 +940,52 @@ def _one_iteration(
|
|
|
928
940
|
mycli.log_query(query_history_text)
|
|
929
941
|
|
|
930
942
|
start = time.time()
|
|
943
|
+
if runner := runner_for(mycli):
|
|
944
|
+
runner.reset_progress()
|
|
931
945
|
results = sqlexecute.run(text)
|
|
932
946
|
mycli.main_formatter.query = text
|
|
933
947
|
mycli.redirect_formatter.query = text
|
|
934
948
|
mycli.explorer_formatter.query = text
|
|
935
949
|
if polars_transform is not None:
|
|
936
950
|
assert polars_pipeline is not None
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
951
|
+
runner = runner_for(mycli)
|
|
952
|
+
with runner.rendering(QueryState.TRANSFORMING) if runner else nullcontext():
|
|
953
|
+
if polars_pipeline.output_path is None:
|
|
954
|
+
polars_result = run_polars_transform(
|
|
955
|
+
polars_transform,
|
|
956
|
+
results,
|
|
957
|
+
image_protocol=mycli.image_protocol,
|
|
958
|
+
plot_scale_factor=mycli.plot_scale_factor,
|
|
959
|
+
plot_ppi=mycli.plot_ppi,
|
|
960
|
+
plot_theme=mycli.plot_theme,
|
|
961
|
+
allow_plots=polars_pipeline.shell_redirect is None,
|
|
962
|
+
)
|
|
963
|
+
else:
|
|
964
|
+
polars_result = run_polars_transform(
|
|
965
|
+
polars_transform,
|
|
966
|
+
results,
|
|
967
|
+
polars_pipeline.output_path,
|
|
968
|
+
original_query=original_text,
|
|
969
|
+
image_protocol=mycli.image_protocol,
|
|
970
|
+
plot_scale_factor=mycli.plot_scale_factor,
|
|
971
|
+
plot_ppi=mycli.plot_ppi,
|
|
972
|
+
plot_theme=mycli.plot_theme,
|
|
973
|
+
)
|
|
957
974
|
if polars_pipeline.output_path is None:
|
|
958
975
|
if polars_pipeline.output_mode == 'explorer':
|
|
959
976
|
special.set_explorer_output(True)
|
|
960
977
|
elif polars_pipeline.output_mode == 'expanded':
|
|
961
978
|
special.set_expanded_output(True)
|
|
962
|
-
|
|
979
|
+
redirect = polars_pipeline.shell_redirect
|
|
980
|
+
try:
|
|
981
|
+
with (
|
|
982
|
+
temporary_redirect(redirect.command, redirect.file_operator, redirect.filename, mycli.post_redirect_command)
|
|
983
|
+
if redirect is not None
|
|
984
|
+
else nullcontext()
|
|
985
|
+
):
|
|
986
|
+
_output_results(mycli, state, iter([polars_result]), start, raise_interrupts=redirect is not None)
|
|
987
|
+
except KeyboardInterrupt:
|
|
988
|
+
raise QueryCancelled(False) from None
|
|
963
989
|
if polars_pipeline.output_path is not None:
|
|
964
990
|
special.run_post_redirect_hook(
|
|
965
991
|
mycli.post_redirect_command,
|
|
@@ -970,10 +996,15 @@ def _one_iteration(
|
|
|
970
996
|
special.unset_once_if_written(mycli.post_redirect_command)
|
|
971
997
|
special.flush_pipe_once_if_written(mycli.post_redirect_command)
|
|
972
998
|
successful = True
|
|
999
|
+
except QueryCancelled as exc:
|
|
1000
|
+
mycli.echo('Query cancelled.', err=True, fg='blue')
|
|
1001
|
+
if exc.disconnected:
|
|
1002
|
+
mycli.echo('Connection closed. Session state was lost.', err=True, fg='yellow')
|
|
1003
|
+
mycli.reconnect()
|
|
973
1004
|
except pymysql.err.InterfaceError:
|
|
974
1005
|
if not mycli.reconnect():
|
|
975
1006
|
return
|
|
976
|
-
_one_iteration(mycli, state, text)
|
|
1007
|
+
_one_iteration(mycli, state, original_text if polars_pipeline is not None else text)
|
|
977
1008
|
return
|
|
978
1009
|
except EOFError as e:
|
|
979
1010
|
raise e
|
|
@@ -1017,7 +1048,7 @@ def _one_iteration(
|
|
|
1017
1048
|
elif e1.args[0] in (2003, 2006, 2013):
|
|
1018
1049
|
if not mycli.reconnect():
|
|
1019
1050
|
return
|
|
1020
|
-
_one_iteration(mycli, state, text)
|
|
1051
|
+
_one_iteration(mycli, state, original_text if polars_pipeline is not None else text)
|
|
1021
1052
|
return
|
|
1022
1053
|
else:
|
|
1023
1054
|
mycli.logger.error('sql: %r, error: %r', text, e1)
|
|
@@ -1139,6 +1170,11 @@ def main_repl(mycli: 'MyCli') -> None:
|
|
|
1139
1170
|
_build_prompt_session(mycli, state, history, key_bindings)
|
|
1140
1171
|
set_all_external_titles(mycli)
|
|
1141
1172
|
|
|
1173
|
+
config = mycli.config['main']
|
|
1174
|
+
show_state_interval = config.as_float('show_query_state_interval')
|
|
1175
|
+
runner = QueryRunner(show_state_interval)
|
|
1176
|
+
if isinstance(sqlexecute, SQLExecute):
|
|
1177
|
+
sqlexecute.set_query_runner(runner)
|
|
1142
1178
|
try:
|
|
1143
1179
|
while True:
|
|
1144
1180
|
_one_iteration(mycli, state)
|
|
@@ -1147,3 +1183,7 @@ def main_repl(mycli: 'MyCli') -> None:
|
|
|
1147
1183
|
special.close_tee()
|
|
1148
1184
|
if mycli.verbosity >= 0:
|
|
1149
1185
|
mycli.echo('Goodbye!')
|
|
1186
|
+
finally:
|
|
1187
|
+
if isinstance(sqlexecute, SQLExecute):
|
|
1188
|
+
sqlexecute.set_query_runner(None)
|
|
1189
|
+
runner.close()
|
|
@@ -111,6 +111,12 @@ log_level = INFO
|
|
|
111
111
|
# Timing of SQL statements and table rendering, or LLM commands.
|
|
112
112
|
timing = True
|
|
113
113
|
|
|
114
|
+
# Seconds between query state and elapsed time updates, off if zero. Disabling
|
|
115
|
+
# this saves a second connection to the server. If set to a very low positive
|
|
116
|
+
# value, would lead to excessive SHOW PROCESSLIST queries. 0.5 is probably a
|
|
117
|
+
# good lower bound.
|
|
118
|
+
show_query_state_interval = 0.5
|
|
119
|
+
|
|
114
120
|
# Show the full SQL when running a favorite query. Set to False to hide.
|
|
115
121
|
show_favorite_query = True
|
|
116
122
|
|
|
@@ -35,6 +35,7 @@ import mycli.main_modes.repl as repl_mode
|
|
|
35
35
|
from mycli.packages import special
|
|
36
36
|
from mycli.packages.sqlresult import SQLResult
|
|
37
37
|
from mycli.packages.tabular_output import sql_format
|
|
38
|
+
from mycli.query_runner import rendering_output, runner_for
|
|
38
39
|
from mycli.sqlexecute import FIELD_TYPES
|
|
39
40
|
|
|
40
41
|
|
|
@@ -109,6 +110,7 @@ class OutputMixin(MyCliState):
|
|
|
109
110
|
|
|
110
111
|
return margin
|
|
111
112
|
|
|
113
|
+
@rendering_output
|
|
112
114
|
def output(
|
|
113
115
|
self,
|
|
114
116
|
output: itertools.chain[str],
|
|
@@ -116,8 +118,15 @@ class OutputMixin(MyCliState):
|
|
|
116
118
|
is_warnings_style: bool = False,
|
|
117
119
|
) -> None:
|
|
118
120
|
"""Output text to stdout or a pager command."""
|
|
121
|
+
runner = runner_for(self)
|
|
122
|
+
|
|
123
|
+
def before_output() -> None:
|
|
124
|
+
if runner:
|
|
125
|
+
runner.stop_rendering()
|
|
126
|
+
|
|
119
127
|
prompt_output = self.prompt_session.output if self.prompt_session is not None else None
|
|
120
128
|
if result.image is not None and not is_windows_console(prompt_output):
|
|
129
|
+
before_output()
|
|
121
130
|
if result.image_protocol == 'iterm2':
|
|
122
131
|
click.secho('')
|
|
123
132
|
self.output_iterm2_image(result.image)
|
|
@@ -156,13 +165,16 @@ class OutputMixin(MyCliState):
|
|
|
156
165
|
output_via_pager = True
|
|
157
166
|
|
|
158
167
|
if not output_via_pager:
|
|
168
|
+
before_output()
|
|
159
169
|
for buf_line in buf:
|
|
160
170
|
click.secho(buf_line)
|
|
161
171
|
buf = []
|
|
162
172
|
else:
|
|
173
|
+
before_output()
|
|
163
174
|
click.secho(line)
|
|
164
175
|
|
|
165
176
|
if buf:
|
|
177
|
+
before_output()
|
|
166
178
|
|
|
167
179
|
def newlinewrapper(text: list[str]) -> Generator[str, None, None]:
|
|
168
180
|
for line in text:
|
|
@@ -186,6 +198,7 @@ class OutputMixin(MyCliState):
|
|
|
186
198
|
click.secho(line)
|
|
187
199
|
|
|
188
200
|
if result.status:
|
|
201
|
+
before_output()
|
|
189
202
|
self.log_output(result.status_plain)
|
|
190
203
|
add_style = 'class:warnings.status' if is_warnings_style else 'class:output.status'
|
|
191
204
|
if isinstance(result.status, FormattedText):
|
|
@@ -234,6 +247,7 @@ class OutputMixin(MyCliState):
|
|
|
234
247
|
return bool(shutil.which(cmd[0]))
|
|
235
248
|
return False
|
|
236
249
|
|
|
250
|
+
@rendering_output
|
|
237
251
|
def format_sqlresult(
|
|
238
252
|
self,
|
|
239
253
|
result,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
1
2
|
import functools
|
|
2
3
|
import logging
|
|
3
4
|
import re
|
|
@@ -19,6 +20,76 @@ SOURCE_OPTION_TERMINATOR_PATTERN = re.compile(r'(?<!\S)--(?=\s|$)')
|
|
|
19
20
|
HYBRID_OPERATOR_PATTERN = re.compile(r'\$(?:>>?|\|)')
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class ShellRedirect:
|
|
25
|
+
command: str | None
|
|
26
|
+
file_operator: str | None
|
|
27
|
+
filename: str | None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def tokenize_shell_suffix(suffix: str) -> list[sqlglot.Token]:
|
|
31
|
+
# Shell long options are not SQL comments; preserve offsets into the original text.
|
|
32
|
+
for match in re.finditer(r'\$\s*>', suffix):
|
|
33
|
+
prefix = re.sub(r'--(?=\S)', '__', suffix[: match.end()])
|
|
34
|
+
try:
|
|
35
|
+
tokens = sqlglot.tokenize(prefix)
|
|
36
|
+
except sqlglot.errors.TokenError:
|
|
37
|
+
continue
|
|
38
|
+
files = find_token_indices(tokens)['angle_bracket']
|
|
39
|
+
if files and tokens[files[-1]].end == match.end() - 1:
|
|
40
|
+
# Only an unquoted file operator switches back to SQL comment handling.
|
|
41
|
+
operand = re.sub(r'(?<=\S)--', '__', suffix[match.end() :])
|
|
42
|
+
return sqlglot.tokenize(prefix + operand)
|
|
43
|
+
return sqlglot.tokenize(re.sub(r'--(?=\S)', '__', suffix))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_shell_redirect(suffix: str) -> ShellRedirect:
|
|
47
|
+
"""Parse a final shell suffix independently of the SQL or transform preceding it."""
|
|
48
|
+
suffix = suffix.strip().removesuffix(delimiter_command.current).rstrip()
|
|
49
|
+
tokens = tokenize_shell_suffix(suffix)
|
|
50
|
+
indices = find_token_indices(tokens)
|
|
51
|
+
dollars = indices['true_dollar']
|
|
52
|
+
pipes = indices['pipe']
|
|
53
|
+
files = indices['angle_bracket']
|
|
54
|
+
if not dollars or tokens[dollars[0]].start != 0:
|
|
55
|
+
raise ValueError('Shell redirection requires an operator.')
|
|
56
|
+
if len(files) > 1 or (files and pipes and pipes[-1] > files[0]):
|
|
57
|
+
raise ValueError('Shell file redirection must be last and occur only once.')
|
|
58
|
+
if WIN and len(pipes) > 1:
|
|
59
|
+
raise ValueError('Multiple shell pipes are not supported on Windows.')
|
|
60
|
+
|
|
61
|
+
command_parts: list[str] = []
|
|
62
|
+
for position, dollar in enumerate(dollars):
|
|
63
|
+
operator = tokens[dollar + 1]
|
|
64
|
+
next_dollar = dollars[position + 1] if position + 1 < len(dollars) else len(tokens)
|
|
65
|
+
if operator.token_type == sqlglot.TokenType.PIPE:
|
|
66
|
+
end = len(suffix[: tokens[next_dollar - 1].end + 1].removesuffix(delimiter_command.current).rstrip())
|
|
67
|
+
last_token = next(token for token in reversed(tokens[:next_dollar]) if token.start < end)
|
|
68
|
+
part = suffix[operator.end + 1 : min(last_token.end + 1, end)].strip()
|
|
69
|
+
if not part:
|
|
70
|
+
raise ValueError('Shell pipes require a command.')
|
|
71
|
+
command_parts.append(part)
|
|
72
|
+
|
|
73
|
+
filename = None
|
|
74
|
+
file_operator = None
|
|
75
|
+
if files:
|
|
76
|
+
operator = tokens[files[0]]
|
|
77
|
+
end = len(suffix[: tokens[-1].end + 1].removesuffix(delimiter_command.current).rstrip())
|
|
78
|
+
last_token = next(token for token in reversed(tokens) if token.start < end)
|
|
79
|
+
operand = suffix[operator.end + 1 : min(last_token.end + 1, end)].strip()
|
|
80
|
+
file_operator = '>'
|
|
81
|
+
if operand.startswith('>'):
|
|
82
|
+
file_operator = '>>'
|
|
83
|
+
operand = operand[1:].strip()
|
|
84
|
+
filename = parse_redirect_filename(operand)
|
|
85
|
+
if not filename:
|
|
86
|
+
raise ValueError('Shell file redirection requires one filename; quote filenames containing spaces.')
|
|
87
|
+
command = ' | '.join(command_parts) or None
|
|
88
|
+
if invalid_shell_part(filename, command):
|
|
89
|
+
raise ValueError('Invalid shell redirection.')
|
|
90
|
+
return ShellRedirect(command, file_operator, filename)
|
|
91
|
+
|
|
92
|
+
|
|
22
93
|
def tokenize_command(command: str) -> list[sqlglot.Token]:
|
|
23
94
|
"""Tokenize a command without treating source options as SQL comments."""
|
|
24
95
|
if SOURCE_COMMAND_PATTERN.match(command):
|
|
@@ -200,34 +271,11 @@ def get_redirect_components(command: str) -> tuple[str | None, str | None, str |
|
|
|
200
271
|
if not sql_part:
|
|
201
272
|
return None, None, None, None
|
|
202
273
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
file_operator_part,
|
|
207
|
-
) = find_file_tokens(
|
|
208
|
-
tokens,
|
|
209
|
-
token_indices['angle_bracket'],
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
command_part_tokens = find_command_tokens(
|
|
213
|
-
tokens[0:file_part_index],
|
|
214
|
-
token_indices['true_dollar'],
|
|
215
|
-
)
|
|
216
|
-
|
|
217
|
-
if file_part_tokens:
|
|
218
|
-
file_part = parse_redirect_filename(assemble_tokens(file_part_tokens))
|
|
219
|
-
if file_part is None:
|
|
220
|
-
return None, None, None, None
|
|
221
|
-
else:
|
|
222
|
-
file_part = None
|
|
223
|
-
|
|
224
|
-
if command_part_tokens:
|
|
225
|
-
command_part = assemble_tokens(command_part_tokens)
|
|
226
|
-
else:
|
|
227
|
-
command_part = None
|
|
228
|
-
|
|
229
|
-
if invalid_shell_part(file_part, command_part):
|
|
274
|
+
try:
|
|
275
|
+
redirect = parse_shell_redirect(command[tokens[token_indices['true_dollar'][0]].start :])
|
|
276
|
+
except (ValueError, sqlglot.errors.TokenError):
|
|
230
277
|
return None, None, None, None
|
|
278
|
+
command_part, file_operator_part, file_part = redirect.command, redirect.file_operator, redirect.filename
|
|
231
279
|
|
|
232
280
|
logger.debug('redirect parse sql_part: "{}"'.format(sql_part))
|
|
233
281
|
logger.debug('redirect parse command_part: "{}"'.format(command_part))
|
|
@@ -14,6 +14,7 @@ from typing import Any, Union, cast, get_args, get_origin
|
|
|
14
14
|
|
|
15
15
|
import sqlglot
|
|
16
16
|
|
|
17
|
+
from mycli.packages.hybrid_redirection import find_token_indices
|
|
17
18
|
from mycli.packages.polars_transform import _pipeline_operator_indexes
|
|
18
19
|
|
|
19
20
|
_ATTRIBUTE_PATTERN = re.compile(r'(?s)(.*)\.([A-Za-z_][A-Za-z0-9_]*)?\s*$')
|
|
@@ -40,6 +41,8 @@ def complete_polars_transform(command: str) -> list[PolarsCompletion] | None:
|
|
|
40
41
|
return None
|
|
41
42
|
if pipe_index is None:
|
|
42
43
|
return None
|
|
44
|
+
if find_token_indices(tokens)['true_dollar']:
|
|
45
|
+
return []
|
|
43
46
|
if output_index is not None:
|
|
44
47
|
return []
|
|
45
48
|
|
|
@@ -11,6 +11,7 @@ from typing import Any, Iterable, Sequence
|
|
|
11
11
|
|
|
12
12
|
import sqlglot
|
|
13
13
|
|
|
14
|
+
from mycli.packages.hybrid_redirection import ShellRedirect, find_token_indices, parse_shell_redirect, tokenize_shell_suffix
|
|
14
15
|
from mycli.packages.special.delimitercommand import DelimiterCommand
|
|
15
16
|
from mycli.packages.sqlresult import SQLResult
|
|
16
17
|
from mycli.types import ImageProtocol, OutputMode
|
|
@@ -30,6 +31,7 @@ class PolarsPipeline:
|
|
|
30
31
|
expression: str | None
|
|
31
32
|
output_path: str | None
|
|
32
33
|
output_mode: OutputMode
|
|
34
|
+
shell_redirect: ShellRedirect | None = None
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
@dataclass(frozen=True, slots=True)
|
|
@@ -69,8 +71,29 @@ def parse_polars_transform(command: str) -> PolarsPipeline | None:
|
|
|
69
71
|
|
|
70
72
|
pipe_index, parquet_index = _pipeline_operator_indexes(command, tokens)
|
|
71
73
|
|
|
74
|
+
shell_indexes = find_token_indices(tokens)['true_dollar']
|
|
75
|
+
if shell_indexes:
|
|
76
|
+
suffix = command[tokens[shell_indexes[0]].start :]
|
|
77
|
+
try:
|
|
78
|
+
shell_tokens = tokenize_shell_suffix(suffix)
|
|
79
|
+
except sqlglot.errors.TokenError as exc:
|
|
80
|
+
raise PolarsTransformError(f'Unable to parse shell redirection: {exc}') from exc
|
|
81
|
+
shell_operators = _pipeline_operator_indexes(suffix, shell_tokens)
|
|
82
|
+
if any(index is not None for index in shell_operators):
|
|
83
|
+
raise PolarsTransformError('Shell redirections must follow all transform stages.')
|
|
72
84
|
if pipe_index is None and parquet_index is None:
|
|
73
85
|
return None
|
|
86
|
+
shell_redirect = None
|
|
87
|
+
if shell_indexes:
|
|
88
|
+
shell_index = shell_indexes[0]
|
|
89
|
+
if parquet_index is not None:
|
|
90
|
+
raise PolarsTransformError('The ".>" operator cannot be combined with shell redirection.')
|
|
91
|
+
shell_start = tokens[shell_index].start
|
|
92
|
+
try:
|
|
93
|
+
shell_redirect = parse_shell_redirect(command[shell_start:])
|
|
94
|
+
except (ValueError, sqlglot.errors.TokenError) as exc:
|
|
95
|
+
raise PolarsTransformError(str(exc)) from exc
|
|
96
|
+
command = command[:shell_start].rstrip()
|
|
74
97
|
if parquet_index is not None and pipe_index is not None and parquet_index < pipe_index:
|
|
75
98
|
raise PolarsTransformError('The ".>" operator must follow the ".|" operator.')
|
|
76
99
|
|
|
@@ -99,6 +122,9 @@ def parse_polars_transform(command: str) -> PolarsPipeline | None:
|
|
|
99
122
|
else:
|
|
100
123
|
output_mode = 'tabular'
|
|
101
124
|
|
|
125
|
+
if shell_redirect is not None and output_mode == 'explorer':
|
|
126
|
+
raise PolarsTransformError('Explorer output cannot be combined with shell redirection.')
|
|
127
|
+
|
|
102
128
|
for delimiter in (delimiter_command.current, r'\G', r'\g', r'\x'):
|
|
103
129
|
sql = sql.removesuffix(delimiter).rstrip()
|
|
104
130
|
if expression is not None:
|
|
@@ -113,7 +139,13 @@ def parse_polars_transform(command: str) -> PolarsPipeline | None:
|
|
|
113
139
|
raise PolarsTransformError('File saves require a destination path.')
|
|
114
140
|
output_path = _parse_output_path(output_path)
|
|
115
141
|
_validate_sql(sql)
|
|
116
|
-
return PolarsPipeline(
|
|
142
|
+
return PolarsPipeline(
|
|
143
|
+
sql=sql,
|
|
144
|
+
expression=expression,
|
|
145
|
+
output_path=output_path,
|
|
146
|
+
output_mode=output_mode,
|
|
147
|
+
shell_redirect=shell_redirect,
|
|
148
|
+
)
|
|
117
149
|
|
|
118
150
|
|
|
119
151
|
def _pipeline_operator_indexes(
|
|
@@ -229,6 +261,7 @@ def run_polars_transform(
|
|
|
229
261
|
plot_scale_factor: float = 1.0,
|
|
230
262
|
plot_ppi: int = 200,
|
|
231
263
|
plot_theme: str = 'carbong90',
|
|
264
|
+
allow_plots: bool = True,
|
|
232
265
|
) -> SQLResult:
|
|
233
266
|
iterator = iter(results)
|
|
234
267
|
try:
|
|
@@ -298,6 +331,8 @@ def run_polars_transform(
|
|
|
298
331
|
return SQLResult(status=f'Wrote {len(series_dataframe)} rows to {output_path}.')
|
|
299
332
|
return SQLResult(header=[column_name], rows=[(item,) for item in value])
|
|
300
333
|
elif transform.altair is not None and isinstance(value, transform.altair.TopLevelMixin):
|
|
334
|
+
if not allow_plots:
|
|
335
|
+
raise PolarsTransformError('Altair plots cannot use shell redirection. Use ".>" to save a plot file.')
|
|
301
336
|
plot_format = _plot_format_for_path(output_path) if output_path is not None else None
|
|
302
337
|
if output_path is not None and plot_format is None:
|
|
303
338
|
raise PolarsTransformError('Altair plots can only be written to ".png", ".pdf", ".svg", or ".html" files.')
|