mycli 2.15.0__tar.gz → 2.17.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.15.0/mycli.egg-info → mycli-2.17.0}/PKG-INFO +5 -6
- {mycli-2.15.0 → mycli-2.17.0}/changelog.md +36 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/TIPS +4 -4
- {mycli-2.15.0 → mycli-2.17.0}/mycli/client_commands.py +185 -8
- {mycli-2.15.0 → mycli-2.17.0}/mycli/client_query.py +10 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/completion_refresher.py +1 -1
- {mycli-2.15.0 → mycli-2.17.0}/mycli/lexer.py +1 -3
- {mycli-2.15.0 → mycli-2.17.0}/mycli/main.py +0 -1
- {mycli-2.15.0 → mycli-2.17.0}/mycli/main_modes/execute.py +1 -1
- {mycli-2.15.0 → mycli-2.17.0}/mycli/main_modes/repl.py +157 -4
- {mycli-2.15.0 → mycli-2.17.0}/mycli/myclirc +8 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/completion_engine.py +38 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/filepaths.py +1 -1
- mycli-2.17.0/mycli/packages/polars_completion.py +293 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/polars_transform.py +36 -24
- mycli-2.17.0/mycli/packages/ptoolkit/history.py +198 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/special/dbcommands.py +3 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/special/iocommands.py +19 -2
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/special/main.py +22 -2
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/sqlresult.py +3 -1
- {mycli-2.15.0 → mycli-2.17.0}/mycli/resources/completions/bash/mycli +1 -1
- {mycli-2.15.0 → mycli-2.17.0}/mycli/resources/completions/fish/mycli.fish +1 -1
- {mycli-2.15.0 → mycli-2.17.0}/mycli/resources/completions/zsh/_mycli +1 -1
- {mycli-2.15.0 → mycli-2.17.0}/mycli/sqlcompleter.py +83 -15
- {mycli-2.15.0 → mycli-2.17.0/mycli.egg-info}/PKG-INFO +5 -6
- {mycli-2.15.0 → mycli-2.17.0}/mycli.egg-info/SOURCES.txt +2 -1
- {mycli-2.15.0 → mycli-2.17.0}/mycli.egg-info/requires.txt +4 -5
- {mycli-2.15.0 → mycli-2.17.0}/mycli.egg-info/scm_file_list.json +2 -1
- mycli-2.17.0/mycli.egg-info/scm_version.json +8 -0
- {mycli-2.15.0 → mycli-2.17.0}/pyproject.toml +10 -8
- mycli-2.17.0/test/features/fixture_data/help_commands.txt +42 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/myclirc +8 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_client_commands.py +371 -4
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_client_query.py +45 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_completion_engine.py +37 -4
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_completion_refresher.py +6 -2
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_filepaths.py +1 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_main.py +2 -2
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_main_modes_execute.py +6 -6
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_main_modes_repl.py +384 -4
- mycli-2.17.0/test/pytests/test_polars_completion.py +259 -0
- mycli-2.17.0/test/pytests/test_ptoolkit_history.py +410 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_smart_completion_public_schema_only.py +90 -17
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_special_main.py +15 -2
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_sqlcompleter.py +86 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_sqlresult.py +2 -0
- mycli-2.15.0/mycli/packages/ptoolkit/history.py +0 -59
- mycli-2.15.0/mycli.egg-info/scm_version.json +0 -8
- mycli-2.15.0/test/features/fixture_data/help_commands.txt +0 -42
- mycli-2.15.0/test/pytests/test_lexer.py +0 -10
- mycli-2.15.0/test/pytests/test_ptoolkit_history.py +0 -72
- {mycli-2.15.0 → mycli-2.17.0}/.git-blame-ignore-revs +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/.github/dependabot.yml +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/.github/workflows/ci.yml +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/.github/workflows/lint.yml +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/.github/workflows/publish.yml +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/.github/workflows/typecheck.yml +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/.gitignore +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/AGENTS.md +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/AUTHORS.rst +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/CONTRIBUTING.md +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/LICENSE.txt +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/MANIFEST.in +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/README.md +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/SPONSORS.rst +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/doc/key_bindings.rst +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/doc/llm.md +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/doc/screenshots/main.gif +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/doc/screenshots/tables.png +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/doc/screenshots/total_histogram.png +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/doc/transforms.md +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/AUTHORS +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/SPONSORS +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/app_state.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/boundary_tunnel.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/cli_runner.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/clibuffer.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/client.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/client_connection.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/clistyle.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/clitoolbar.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/compat.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/config.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/constants.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/key_bindings.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/main_modes/batch.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/main_modes/completions.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/output.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/batch_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/cli_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/hybrid_redirection.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/interactive_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/key_binding_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/ptoolkit/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/ptoolkit/fzf.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/ptoolkit/utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/special/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/special/delimitercommand.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/special/dsn_aliases.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/special/favoritequeries.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/special/llm.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/special/utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/sql_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/string_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/password_sources.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/schema_prefetcher.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/sqlexecute.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/ssh_tunnel.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/types.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli/vault.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/setup.cfg +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/auto_vertical.feature +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/basic_commands.feature +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/connection.feature +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/crud_database.feature +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/crud_table.feature +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/db_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/environment.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/fixture_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/iocommands.feature +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/named_queries.feature +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/specials.feature +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/steps/__init__.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/steps/auto_vertical.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/steps/basic_commands.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/steps/connection.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/steps/crud_database.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/steps/crud_table.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/steps/iocommands.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/steps/named_queries.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/steps/specials.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/steps/utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/steps/wrappers.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/features/wrappager.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/mylogin.cnf +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/conftest.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_app_state.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_bash_completion.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_batch_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_boundary_tunnel.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_checkup.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_cli_runner.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_cli_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_clibuffer.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_client.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_client_connection.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_clistyle.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_clitoolbar.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_config.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_dsn_aliases.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_fish_completion.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_hybrid_redirection.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_key_bindings.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_main_modes_batch.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_naive_completion.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_output.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_password_sources.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_polars_transform.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_ptoolkit_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_special_dbcommands.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_special_init.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_special_iocommands.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_special_llm.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_special_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_sql_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_sqlexecute.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_ssh_tunnel.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_string_utils.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_tabular_output.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_vault.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/pytests/test_zsh_completion.py +0 -0
- {mycli-2.15.0 → mycli-2.17.0}/test/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mycli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.17.0
|
|
4
4
|
Summary: Rich MySQL terminal client with autocompletion, syntax highlighting, and dataframes
|
|
5
5
|
Author: Mycli Core Team
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -31,7 +31,7 @@ License-File: AUTHORS.rst
|
|
|
31
31
|
Requires-Dist: click~=8.3.3
|
|
32
32
|
Requires-Dist: clickdc~=0.1.1
|
|
33
33
|
Requires-Dist: cryptography~=50.0.0
|
|
34
|
-
Requires-Dist: Pygments~=2.
|
|
34
|
+
Requires-Dist: Pygments~=2.21.0
|
|
35
35
|
Requires-Dist: prompt_toolkit<4.0.0,>=3.0.41
|
|
36
36
|
Requires-Dist: PyMySQL~=1.1.2
|
|
37
37
|
Requires-Dist: sqlparse<0.6.0,>=0.3.0
|
|
@@ -48,10 +48,9 @@ Requires-Dist: keyring~=25.7.0
|
|
|
48
48
|
Requires-Dist: jinja2~=3.1.6
|
|
49
49
|
Requires-Dist: yaspin~=3.4.0
|
|
50
50
|
Provides-Extra: llm
|
|
51
|
-
Requires-Dist: llm~=0.
|
|
51
|
+
Requires-Dist: llm~=0.33; extra == "llm"
|
|
52
52
|
Requires-Dist: setuptools==83.*; extra == "llm"
|
|
53
53
|
Requires-Dist: pip~=26.1.2; extra == "llm"
|
|
54
|
-
Requires-Dist: openai~=2.48.0; extra == "llm"
|
|
55
54
|
Provides-Extra: dataframe
|
|
56
55
|
Requires-Dist: polars~=1.42.1; extra == "dataframe"
|
|
57
56
|
Requires-Dist: altair~=6.2.2; extra == "dataframe"
|
|
@@ -61,14 +60,14 @@ Requires-Dist: mycli[dataframe,llm]; extra == "all"
|
|
|
61
60
|
Provides-Extra: dev
|
|
62
61
|
Requires-Dist: behave~=1.3.3; extra == "dev"
|
|
63
62
|
Requires-Dist: coverage~=7.13.4; extra == "dev"
|
|
64
|
-
Requires-Dist: mypy~=2.3.
|
|
63
|
+
Requires-Dist: mypy~=2.3.1; extra == "dev"
|
|
65
64
|
Requires-Dist: pexpect~=4.9.0; extra == "dev"
|
|
66
65
|
Requires-Dist: pytest~=9.1.1; extra == "dev"
|
|
67
66
|
Requires-Dist: pytest-cov~=7.0.0; extra == "dev"
|
|
68
67
|
Requires-Dist: pytest-random-order~=1.2.0; extra == "dev"
|
|
69
68
|
Requires-Dist: tox~=4.35.0; extra == "dev"
|
|
70
69
|
Requires-Dist: pdbpp~=0.11.7; extra == "dev"
|
|
71
|
-
Requires-Dist: ruff~=0.
|
|
70
|
+
Requires-Dist: ruff~=0.16.3; extra == "dev"
|
|
72
71
|
Dynamic: license-file
|
|
73
72
|
|
|
74
73
|
# mycli
|
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
2.17.0 (2026/08/25)
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Features
|
|
5
|
+
--------
|
|
6
|
+
* Sort completion candidates by frecency from history.
|
|
7
|
+
* Add help snippets in special /command completions.
|
|
8
|
+
* Add completions on Polars methods in dataframe transforms.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Internal
|
|
12
|
+
---------
|
|
13
|
+
* Upgrade `pygments` to v2.21.0, removing hacks for `set*` identifiers.
|
|
14
|
+
* Upgrade `llm` dependency to v3.33, removing pin for `openai`.
|
|
15
|
+
* Remove `pygments` cooldown exception as no longer needed.
|
|
16
|
+
* Upgrade `mypy` dev dependency to v2.3.1.
|
|
17
|
+
* Upgrade `ruff` dev dependency to v0.16.3.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
2.16.0 (2026/08/22)
|
|
21
|
+
==============
|
|
22
|
+
|
|
23
|
+
Features
|
|
24
|
+
---------
|
|
25
|
+
* Advertise beta `--boundary-id` option in helpdoc and completions.
|
|
26
|
+
* Add `/source --special` to allow executing some special commands.
|
|
27
|
+
* Add `/source --show` to display each query before executing it.
|
|
28
|
+
* Add `/source --page` to display all output in one pager session.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
Bug Fixes
|
|
32
|
+
---------
|
|
33
|
+
* Show CLI error on invalid `--execute` containing `/source`.
|
|
34
|
+
* Require `/source` filenames containing spaces to be quoted.
|
|
35
|
+
|
|
36
|
+
|
|
1
37
|
2.15.0 (2026/08/20)
|
|
2
38
|
==============
|
|
3
39
|
|
|
@@ -54,7 +54,7 @@ copy the previous query to the clipboard with /clip!
|
|
|
54
54
|
|
|
55
55
|
edit a query in an external editor using <query>\edit!
|
|
56
56
|
|
|
57
|
-
edit a query in an external editor using /edit <
|
|
57
|
+
edit a query in an external editor using /edit <file>!
|
|
58
58
|
|
|
59
59
|
/f lists favorite queries; /f <name> executes a favorite!
|
|
60
60
|
|
|
@@ -68,7 +68,7 @@ Manage favorite queries with /favorite!
|
|
|
68
68
|
|
|
69
69
|
/l lists databases!
|
|
70
70
|
|
|
71
|
-
/once <
|
|
71
|
+
/once <file> appends the next result to <file>!
|
|
72
72
|
|
|
73
73
|
/| <command> sends the next result to a subprocess!
|
|
74
74
|
|
|
@@ -306,9 +306,9 @@ customize password sources/precedence with "password_sources" in ~/.myclirc!
|
|
|
306
306
|
|
|
307
307
|
redirect query output to a shell command with "$| <command>"!
|
|
308
308
|
|
|
309
|
-
redirect query output to a CSV file with "$> <
|
|
309
|
+
redirect query output to a CSV file with "$> <file>"!
|
|
310
310
|
|
|
311
|
-
append query output to a CSV file with "$>> <
|
|
311
|
+
append query output to a CSV file with "$>> <file>"!
|
|
312
312
|
|
|
313
313
|
run a command after shell redirects with "post_redirect_command" in ~/.myclirc!
|
|
314
314
|
|
|
@@ -4,16 +4,22 @@ from collections.abc import Generator, Mapping
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
+
import shlex
|
|
7
8
|
from typing import TYPE_CHECKING, Any, cast
|
|
8
9
|
|
|
9
10
|
import click
|
|
11
|
+
import sqlparse
|
|
10
12
|
|
|
13
|
+
from mycli.compat import WIN
|
|
11
14
|
from mycli.config import write_default_config
|
|
12
15
|
from mycli.main_modes.repl import set_all_external_titles
|
|
13
16
|
from mycli.packages import special
|
|
14
17
|
from mycli.packages.batch_utils import statements_from_filehandle
|
|
15
18
|
from mycli.packages.filepaths import dir_path_exists
|
|
16
19
|
from mycli.packages.interactive_utils import confirm_destructive_query
|
|
20
|
+
from mycli.packages.ptoolkit.history import FileHistoryWithTimestamp
|
|
21
|
+
from mycli.packages.special import main as special_main
|
|
22
|
+
from mycli.packages.special.iocommands import expand_favorite_query
|
|
17
23
|
from mycli.packages.special.main import ArgType, SpecialCommandAlias
|
|
18
24
|
from mycli.packages.sqlresult import SQLResult
|
|
19
25
|
from mycli.sqlexecute import SQLExecute
|
|
@@ -30,6 +36,29 @@ MISSING_CONFIG_VALUE = object()
|
|
|
30
36
|
DSN_CONFIG_VALUE = object()
|
|
31
37
|
FAVORITES_CONFIG_VALUE = object()
|
|
32
38
|
HIDDEN_CONFIG_SECTIONS = frozenset({'alias_dsn', 'favorite_queries'})
|
|
39
|
+
INVALID_SOURCE_FILENAME = 'Source accepts exactly one filename; filenames containing spaces must be quoted.'
|
|
40
|
+
SOURCE_SAFE_SPECIAL_COMMANDS = frozenset({
|
|
41
|
+
'connect',
|
|
42
|
+
'fd',
|
|
43
|
+
'fs',
|
|
44
|
+
'help',
|
|
45
|
+
'l',
|
|
46
|
+
'nowarnings',
|
|
47
|
+
'prompt',
|
|
48
|
+
'redirectformat',
|
|
49
|
+
'rehash',
|
|
50
|
+
'status',
|
|
51
|
+
'tableformat',
|
|
52
|
+
'timing',
|
|
53
|
+
'use',
|
|
54
|
+
'warnings',
|
|
55
|
+
'dt',
|
|
56
|
+
})
|
|
57
|
+
SOURCE_SAFE_SUBCOMMANDS = {
|
|
58
|
+
'config': frozenset({'help', 'get', 'search'}),
|
|
59
|
+
'dsn': frozenset({'help', 'list', 'show', 'save', 'delete'}),
|
|
60
|
+
'favorite': frozenset({'help', 'list', 'reload', 'run', 'save', 'delete'}),
|
|
61
|
+
}
|
|
33
62
|
|
|
34
63
|
|
|
35
64
|
def _render_config_value(value: Any) -> str:
|
|
@@ -38,6 +67,106 @@ def _render_config_value(value: Any) -> str:
|
|
|
38
67
|
return str(value)
|
|
39
68
|
|
|
40
69
|
|
|
70
|
+
def _parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool]:
|
|
71
|
+
allow_special = False
|
|
72
|
+
show_queries = False
|
|
73
|
+
page_output = False
|
|
74
|
+
filename = arg
|
|
75
|
+
while arguments := filename.split(maxsplit=1):
|
|
76
|
+
if arguments[0] == '--special':
|
|
77
|
+
allow_special = True
|
|
78
|
+
elif arguments[0] == '--show':
|
|
79
|
+
show_queries = True
|
|
80
|
+
elif arguments[0] == '--page':
|
|
81
|
+
page_output = True
|
|
82
|
+
else:
|
|
83
|
+
break
|
|
84
|
+
filename = arguments[1] if len(arguments) == 2 else ''
|
|
85
|
+
return filename, allow_special, show_queries, page_output
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _has_unquoted_whitespace(value: str) -> bool:
|
|
89
|
+
quote: str | None = None
|
|
90
|
+
escaped = False
|
|
91
|
+
for character in value:
|
|
92
|
+
if escaped:
|
|
93
|
+
if quote is None and character.isspace():
|
|
94
|
+
return True
|
|
95
|
+
escaped = False
|
|
96
|
+
continue
|
|
97
|
+
if not WIN and character == '\\' and quote != "'":
|
|
98
|
+
escaped = True
|
|
99
|
+
continue
|
|
100
|
+
if character in ("'", '"'):
|
|
101
|
+
if quote is None:
|
|
102
|
+
quote = character
|
|
103
|
+
elif quote == character:
|
|
104
|
+
quote = None
|
|
105
|
+
elif quote is None and character.isspace():
|
|
106
|
+
return True
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _parse_source_filename(filename: str) -> str:
|
|
111
|
+
if not filename:
|
|
112
|
+
return ''
|
|
113
|
+
if _has_unquoted_whitespace(filename):
|
|
114
|
+
raise ValueError(INVALID_SOURCE_FILENAME)
|
|
115
|
+
try:
|
|
116
|
+
arguments = shlex.split(filename, posix=not WIN)
|
|
117
|
+
except ValueError as error:
|
|
118
|
+
raise ValueError(f'Invalid source filename: {error}.') from None
|
|
119
|
+
if len(arguments) != 1:
|
|
120
|
+
raise ValueError(INVALID_SOURCE_FILENAME)
|
|
121
|
+
parsed_filename = arguments[0]
|
|
122
|
+
if WIN and len(parsed_filename) >= 2 and parsed_filename[0] == parsed_filename[-1] and parsed_filename[0] in ("'", '"'):
|
|
123
|
+
parsed_filename = parsed_filename[1:-1]
|
|
124
|
+
return parsed_filename
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _registered_special_command(query: str) -> tuple[str, str] | None:
|
|
128
|
+
command, _verbosity, arg = special.parse_special_command(query)
|
|
129
|
+
registered = special_main.COMMANDS.get(command)
|
|
130
|
+
if registered is None:
|
|
131
|
+
registered = special_main.COMMANDS.get(command.lower())
|
|
132
|
+
if registered is None:
|
|
133
|
+
return None
|
|
134
|
+
return registered.command.removeprefix('\\').removeprefix('/').lower(), arg
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _favorite_source_command_is_safe(arg: str) -> bool:
|
|
138
|
+
query, _error = expand_favorite_query(arg)
|
|
139
|
+
if query is None:
|
|
140
|
+
return True
|
|
141
|
+
return not any(special.is_special_command(statement.rstrip(';')) for statement in sqlparse.split(query))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _source_special_command_is_safe(query: str) -> bool:
|
|
145
|
+
parsed = _registered_special_command(query)
|
|
146
|
+
if parsed is None:
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
command, arg = parsed
|
|
150
|
+
if command == 'f':
|
|
151
|
+
return not arg or _favorite_source_command_is_safe(arg)
|
|
152
|
+
if command in ('fd', 'fs'):
|
|
153
|
+
return True
|
|
154
|
+
if command in SOURCE_SAFE_SPECIAL_COMMANDS:
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
subcommands = SOURCE_SAFE_SUBCOMMANDS.get(command)
|
|
158
|
+
if subcommands is None:
|
|
159
|
+
return False
|
|
160
|
+
arguments = arg.split(maxsplit=1)
|
|
161
|
+
subcommand = arguments[0].lower() if arguments else 'help'
|
|
162
|
+
if subcommand not in subcommands:
|
|
163
|
+
return False
|
|
164
|
+
if command == 'favorite' and subcommand == 'run':
|
|
165
|
+
run_arg = arguments[1] if len(arguments) == 2 else ''
|
|
166
|
+
return not run_arg or _favorite_source_command_is_safe(run_arg)
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
|
|
41
170
|
def _iter_config_values(
|
|
42
171
|
config: Mapping[str, Any],
|
|
43
172
|
prefix: str = '',
|
|
@@ -94,6 +223,7 @@ class ClientCommandsMixin:
|
|
|
94
223
|
destructive_keywords: Any
|
|
95
224
|
config: Any
|
|
96
225
|
myclirc_path: str
|
|
226
|
+
prompt_session: Any
|
|
97
227
|
prompt_format: str
|
|
98
228
|
|
|
99
229
|
def refresh_completions(self, reset: bool = False) -> list[SQLResult]: ...
|
|
@@ -107,6 +237,7 @@ class ClientCommandsMixin:
|
|
|
107
237
|
"/use <database>",
|
|
108
238
|
"Change to a new database.",
|
|
109
239
|
aliases=[SpecialCommandAlias("\\u", case_sensitive=False)],
|
|
240
|
+
completion_snippet='change databases',
|
|
110
241
|
)
|
|
111
242
|
special.register_special_command(
|
|
112
243
|
self.manual_reconnect,
|
|
@@ -115,14 +246,16 @@ class ClientCommandsMixin:
|
|
|
115
246
|
"Reconnect to the server, optionally switching databases.",
|
|
116
247
|
case_sensitive=True,
|
|
117
248
|
aliases=[SpecialCommandAlias("\\r", case_sensitive=True)],
|
|
249
|
+
completion_snippet='reconnect to server',
|
|
118
250
|
)
|
|
119
251
|
special.register_special_command(
|
|
120
|
-
self.
|
|
252
|
+
self.rehash,
|
|
121
253
|
"rehash",
|
|
122
254
|
"/rehash",
|
|
123
255
|
"Refresh auto-completions.",
|
|
124
256
|
arg_type=ArgType.NO_ARGUMENT,
|
|
125
257
|
aliases=[SpecialCommandAlias("\\#", case_sensitive=False)],
|
|
258
|
+
completion_snippet='refresh completions',
|
|
126
259
|
)
|
|
127
260
|
special.register_special_command(
|
|
128
261
|
self.change_table_format,
|
|
@@ -131,6 +264,7 @@ class ClientCommandsMixin:
|
|
|
131
264
|
"Change the table format used to output interactive results.",
|
|
132
265
|
case_sensitive=True,
|
|
133
266
|
aliases=[SpecialCommandAlias("\\T", case_sensitive=True)],
|
|
267
|
+
completion_snippet='change interactive output format',
|
|
134
268
|
)
|
|
135
269
|
special.register_special_command(
|
|
136
270
|
self.change_redirect_format,
|
|
@@ -139,13 +273,15 @@ class ClientCommandsMixin:
|
|
|
139
273
|
"Change the table format used to output redirected results.",
|
|
140
274
|
case_sensitive=True,
|
|
141
275
|
aliases=[SpecialCommandAlias("\\Tr", case_sensitive=True)],
|
|
276
|
+
completion_snippet='change redirected output format',
|
|
142
277
|
)
|
|
143
278
|
special.register_special_command(
|
|
144
279
|
self.execute_from_file,
|
|
145
280
|
"source",
|
|
146
|
-
"/source <
|
|
281
|
+
"/source [--special|--show|--page] <file>",
|
|
147
282
|
"Execute queries from a file.",
|
|
148
283
|
aliases=[SpecialCommandAlias("\\.", case_sensitive=False)],
|
|
284
|
+
completion_snippet='execute queries from file',
|
|
149
285
|
)
|
|
150
286
|
special.register_special_command(
|
|
151
287
|
self.change_prompt_format,
|
|
@@ -154,12 +290,14 @@ class ClientCommandsMixin:
|
|
|
154
290
|
"Show or change prompt format.",
|
|
155
291
|
case_sensitive=True,
|
|
156
292
|
aliases=[SpecialCommandAlias("\\R", case_sensitive=True)],
|
|
293
|
+
completion_snippet='show or change prompt format',
|
|
157
294
|
)
|
|
158
295
|
special.register_special_command(
|
|
159
296
|
self.config_command,
|
|
160
297
|
r'\config',
|
|
161
298
|
'/config <help|get|search|edit> [key]',
|
|
162
299
|
'Inspect settings from config files.',
|
|
300
|
+
completion_snippet='inspect config file settings',
|
|
163
301
|
)
|
|
164
302
|
|
|
165
303
|
def manual_reconnect(self, arg: str = "", **_) -> Generator[SQLResult, None, None]:
|
|
@@ -174,6 +312,13 @@ class ClientCommandsMixin:
|
|
|
174
312
|
else:
|
|
175
313
|
yield self.change_db(arg).send(None)
|
|
176
314
|
|
|
315
|
+
def rehash(self) -> list[SQLResult]:
|
|
316
|
+
prompt_session = getattr(self, 'prompt_session', None)
|
|
317
|
+
history = getattr(prompt_session, 'history', None)
|
|
318
|
+
if isinstance(history, FileHistoryWithTimestamp):
|
|
319
|
+
history.refresh_frecency()
|
|
320
|
+
return self.refresh_completions()
|
|
321
|
+
|
|
177
322
|
def change_table_format(self, arg: str, **_) -> Generator[SQLResult, None, None]:
|
|
178
323
|
try:
|
|
179
324
|
self.main_formatter.format_name = arg
|
|
@@ -266,12 +411,20 @@ class ClientCommandsMixin:
|
|
|
266
411
|
yield SQLResult(status=msg)
|
|
267
412
|
|
|
268
413
|
def execute_from_file(self, arg: str, **_) -> Generator[SQLResult, None, None]:
|
|
269
|
-
|
|
270
|
-
|
|
414
|
+
filename, allow_special, show_queries, page_output = _parse_source_arguments(arg)
|
|
415
|
+
if page_output:
|
|
416
|
+
yield SQLResult(command={'name': 'source_page'})
|
|
417
|
+
try:
|
|
418
|
+
filename = _parse_source_filename(filename)
|
|
419
|
+
except ValueError as error:
|
|
420
|
+
yield SQLResult(status=str(error), is_error=True)
|
|
421
|
+
return
|
|
422
|
+
if not filename:
|
|
423
|
+
yield SQLResult(status="Missing required argument: filename.", is_error=True)
|
|
271
424
|
return
|
|
272
425
|
|
|
273
426
|
try:
|
|
274
|
-
file_h = open(os.path.expanduser(
|
|
427
|
+
file_h = open(os.path.expanduser(filename))
|
|
275
428
|
except OSError as error:
|
|
276
429
|
yield SQLResult(status=str(error))
|
|
277
430
|
return
|
|
@@ -288,12 +441,36 @@ class ClientCommandsMixin:
|
|
|
288
441
|
yield SQLResult(status=str(error))
|
|
289
442
|
return
|
|
290
443
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
444
|
+
special_query = query.rstrip(';')
|
|
445
|
+
if special.is_special_command(special_query):
|
|
446
|
+
if not allow_special:
|
|
447
|
+
yield SQLResult(
|
|
448
|
+
status='Special commands are not supported without /source --special.',
|
|
449
|
+
is_error=True,
|
|
450
|
+
)
|
|
451
|
+
return
|
|
452
|
+
if not _source_special_command_is_safe(special_query):
|
|
453
|
+
command, _verbosity, _arg = special.parse_special_command(special_query)
|
|
454
|
+
yield SQLResult(
|
|
455
|
+
status=f'Special command is never permitted in source files: {command}.',
|
|
456
|
+
is_error=True,
|
|
457
|
+
)
|
|
458
|
+
return
|
|
459
|
+
if show_queries:
|
|
460
|
+
if page_output:
|
|
461
|
+
yield SQLResult(command={'name': 'source_show', 'text': special_query})
|
|
462
|
+
else:
|
|
463
|
+
click.secho(f'> {special_query}')
|
|
464
|
+
yield from self.sqlexecute.run(special_query)
|
|
465
|
+
continue
|
|
294
466
|
|
|
295
467
|
if self.destructive_warning and confirm_destructive_query(self.destructive_keywords, query) is False:
|
|
296
468
|
continue
|
|
469
|
+
if show_queries:
|
|
470
|
+
if page_output:
|
|
471
|
+
yield SQLResult(command={'name': 'source_show', 'text': query})
|
|
472
|
+
else:
|
|
473
|
+
click.secho(f'> {query}')
|
|
297
474
|
yield from self.sqlexecute.run(query)
|
|
298
475
|
|
|
299
476
|
def change_prompt_format(self, arg: str, **_) -> list[SQLResult]:
|
|
@@ -10,6 +10,10 @@ from mycli.packages.sqlresult import SQLResult
|
|
|
10
10
|
from mycli.sqlcompleter import SQLCompleter
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class QueryError(Exception):
|
|
14
|
+
"""An error reported while consuming query results."""
|
|
15
|
+
|
|
16
|
+
|
|
13
17
|
class ClientQueryMixin:
|
|
14
18
|
if TYPE_CHECKING:
|
|
15
19
|
schema_prefetcher: Any
|
|
@@ -55,6 +59,7 @@ class ClientQueryMixin:
|
|
|
55
59
|
"keyword_casing": self.completer.keyword_casing,
|
|
56
60
|
"indexed_column_suffix": self.completer.indexed_column_suffix,
|
|
57
61
|
"config_property_names": self.completer.config_property_names,
|
|
62
|
+
'frecency_provider': self.completer.frecency_provider,
|
|
58
63
|
},
|
|
59
64
|
)
|
|
60
65
|
|
|
@@ -80,6 +85,7 @@ class ClientQueryMixin:
|
|
|
80
85
|
query: str,
|
|
81
86
|
checkpoint: str | None = None,
|
|
82
87
|
new_line: bool = True,
|
|
88
|
+
raise_on_error: bool = False,
|
|
83
89
|
) -> None:
|
|
84
90
|
"""Runs *query*."""
|
|
85
91
|
assert self.sqlexecute is not None
|
|
@@ -91,6 +97,10 @@ class ClientQueryMixin:
|
|
|
91
97
|
self.main_formatter.query = query
|
|
92
98
|
self.redirect_formatter.query = query
|
|
93
99
|
self.explorer_formatter.query = query
|
|
100
|
+
if result.is_error and raise_on_error:
|
|
101
|
+
message = result.status_plain or 'Query failed.'
|
|
102
|
+
self.log_output(message)
|
|
103
|
+
raise QueryError(message)
|
|
94
104
|
output = self.format_sqlresult(
|
|
95
105
|
result,
|
|
96
106
|
is_expanded=special.is_expanded_output(),
|
|
@@ -267,7 +267,7 @@ def refresh_collations(completer: SQLCompleter, executor: SQLExecute) -> None:
|
|
|
267
267
|
|
|
268
268
|
@refresher("special_commands")
|
|
269
269
|
def refresh_special(completer: SQLCompleter, executor: SQLExecute) -> None:
|
|
270
|
-
completer.extend_special_commands(
|
|
270
|
+
completer.extend_special_commands({command: details.completion_snippet or details.description for command, details in COMMANDS.items()})
|
|
271
271
|
|
|
272
272
|
|
|
273
273
|
@refresher("show_commands")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from pygments.lexer import inherit
|
|
2
2
|
from pygments.lexers.sql import MySqlLexer
|
|
3
|
-
from pygments.token import Keyword
|
|
3
|
+
from pygments.token import Keyword
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class MyCliLexer(MySqlLexer):
|
|
@@ -8,8 +8,6 @@ class MyCliLexer(MySqlLexer):
|
|
|
8
8
|
|
|
9
9
|
tokens = {
|
|
10
10
|
"root": [
|
|
11
|
-
# TODO: Remove once Pygments is upgraded above v2.20.0.
|
|
12
|
-
(r"\bset[\w$]+\b", Name),
|
|
13
11
|
(r"\brepair\b", Keyword),
|
|
14
12
|
(r"\boffset\b", Keyword),
|
|
15
13
|
inherit,
|
|
@@ -46,7 +46,7 @@ def main_execute_from_cli(mycli: 'MyCli', cli_args: 'CliArgs') -> int:
|
|
|
46
46
|
mycli.logger.warning('Unable to open TTY as stdin.')
|
|
47
47
|
raise e
|
|
48
48
|
if execution_confirmed:
|
|
49
|
-
mycli.run_query(execute_sql, checkpoint=cli_args.checkpoint)
|
|
49
|
+
mycli.run_query(execute_sql, checkpoint=cli_args.checkpoint, raise_on_error=True)
|
|
50
50
|
return 0
|
|
51
51
|
else:
|
|
52
52
|
return 1
|