mycli 2.0.0__tar.gz → 2.2.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.0.0 → mycli-2.2.0}/.github/workflows/ci.yml +4 -4
- {mycli-2.0.0 → mycli-2.2.0}/.github/workflows/lint.yml +2 -2
- {mycli-2.0.0 → mycli-2.2.0}/.github/workflows/publish.yml +4 -4
- {mycli-2.0.0 → mycli-2.2.0}/.github/workflows/typecheck.yml +1 -1
- {mycli-2.0.0 → mycli-2.2.0}/AGENTS.md +8 -4
- {mycli-2.0.0/mycli.egg-info → mycli-2.2.0}/PKG-INFO +1 -1
- {mycli-2.0.0 → mycli-2.2.0}/changelog.md +56 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/TIPS +5 -1
- {mycli-2.0.0 → mycli-2.2.0}/mycli/cli_runner.py +129 -47
- {mycli-2.0.0 → mycli-2.2.0}/mycli/client.py +16 -2
- {mycli-2.0.0 → mycli-2.2.0}/mycli/client_connection.py +79 -17
- {mycli-2.0.0 → mycli-2.2.0}/mycli/main.py +39 -11
- {mycli-2.0.0 → mycli-2.2.0}/mycli/myclirc +34 -2
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/cli_utils.py +2 -6
- mycli-2.2.0/mycli/packages/special/dsn_aliases.py +64 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/favoritequeries.py +4 -4
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/iocommands.py +47 -1
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/llm.py +6 -6
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/utils.py +46 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/sqlexecute.py +4 -0
- mycli-2.2.0/mycli/ssh_tunnel.py +212 -0
- mycli-2.2.0/mycli/vault.py +54 -0
- {mycli-2.0.0 → mycli-2.2.0/mycli.egg-info}/PKG-INFO +1 -1
- {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/SOURCES.txt +7 -1
- {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/scm_file_list.json +6 -0
- mycli-2.2.0/mycli.egg-info/scm_version.json +8 -0
- mycli-2.2.0/test/features/fixture_data/help_commands.txt +39 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/myclirc +34 -2
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_cli_runner.py +414 -2
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_cli_utils.py +2 -2
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_client.py +17 -1
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_client_connection.py +242 -1
- mycli-2.2.0/test/pytests/test_dsn_aliases.py +102 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_main.py +61 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_smart_completion_public_schema_only.py +4 -1
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_iocommands.py +106 -2
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_utils.py +75 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_sqlexecute.py +3 -0
- mycli-2.2.0/test/pytests/test_ssh_tunnel.py +512 -0
- mycli-2.2.0/test/pytests/test_vault.py +96 -0
- mycli-2.0.0/mycli.egg-info/scm_version.json +0 -8
- mycli-2.0.0/test/features/fixture_data/help_commands.txt +0 -38
- {mycli-2.0.0 → mycli-2.2.0}/.git-blame-ignore-revs +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/.github/dependabot.yml +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/.gitignore +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/AUTHORS.rst +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/CONTRIBUTING.md +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/LICENSE.txt +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/MANIFEST.in +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/README.md +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/SPONSORS.rst +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/doc/key_bindings.rst +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/doc/llm.md +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/doc/screenshots/main.gif +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/doc/screenshots/tables.png +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/AUTHORS +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/SPONSORS +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/__init__.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/app_state.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/clibuffer.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/client_commands.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/client_query.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/clistyle.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/clitoolbar.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/compat.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/completion_refresher.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/config.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/constants.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/key_bindings.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/lexer.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/main_modes/batch.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/main_modes/execute.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/main_modes/repl.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/output.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/__init__.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/batch_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/completion_engine.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/filepaths.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/hybrid_redirection.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/interactive_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/key_binding_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/ptoolkit/__init__.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/ptoolkit/fzf.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/ptoolkit/history.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/ptoolkit/utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/__init__.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/dbcommands.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/delimitercommand.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/main.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/sql_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/sqlresult.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/string_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/schema_prefetcher.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/sqlcompleter.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli/types.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/requires.txt +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/pyproject.toml +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/setup.cfg +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/__init__.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/__init__.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/auto_vertical.feature +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/basic_commands.feature +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/connection.feature +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/crud_database.feature +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/crud_table.feature +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/db_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/environment.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/fixture_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/iocommands.feature +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/named_queries.feature +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/specials.feature +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/__init__.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/auto_vertical.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/basic_commands.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/connection.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/crud_database.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/crud_table.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/iocommands.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/named_queries.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/specials.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/wrappers.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/features/wrappager.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/mylogin.cnf +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/conftest.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_app_state.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_batch_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_checkup.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_clibuffer.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_client_commands.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_client_query.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_clistyle.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_clitoolbar.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_completion_engine.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_completion_refresher.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_config.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_filepaths.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_hybrid_redirection.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_key_bindings.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_lexer.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_main_modes_batch.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_main_modes_execute.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_main_modes_repl.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_naive_completion.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_output.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_ptoolkit_history.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_ptoolkit_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_dbcommands.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_init.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_llm.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_main.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_sql_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_sqlcompleter.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_sqlresult.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_string_utils.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_tabular_output.py +0 -0
- {mycli-2.0.0 → mycli-2.2.0}/test/utils.py +0 -0
|
@@ -24,7 +24,7 @@ jobs:
|
|
|
24
24
|
steps:
|
|
25
25
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
26
26
|
|
|
27
|
-
- uses: astral-sh/setup-uv@
|
|
27
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
28
28
|
with:
|
|
29
29
|
version: "latest"
|
|
30
30
|
|
|
@@ -66,7 +66,7 @@ jobs:
|
|
|
66
66
|
steps:
|
|
67
67
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
68
68
|
|
|
69
|
-
- uses: astral-sh/setup-uv@
|
|
69
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
70
70
|
with:
|
|
71
71
|
version: "latest"
|
|
72
72
|
|
|
@@ -116,7 +116,7 @@ jobs:
|
|
|
116
116
|
steps:
|
|
117
117
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
118
118
|
|
|
119
|
-
- uses: astral-sh/setup-uv@
|
|
119
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
120
120
|
with:
|
|
121
121
|
version: 'latest'
|
|
122
122
|
|
|
@@ -219,7 +219,7 @@ jobs:
|
|
|
219
219
|
steps:
|
|
220
220
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
221
221
|
|
|
222
|
-
- uses: astral-sh/setup-uv@
|
|
222
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
223
223
|
with:
|
|
224
224
|
version: "latest"
|
|
225
225
|
|
|
@@ -21,9 +21,9 @@ jobs:
|
|
|
21
21
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
22
22
|
|
|
23
23
|
- name: Run ruff check
|
|
24
|
-
uses: astral-sh/ruff-action@
|
|
24
|
+
uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
|
|
25
25
|
|
|
26
26
|
- name: Run ruff format
|
|
27
|
-
uses: astral-sh/ruff-action@
|
|
27
|
+
uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
|
|
28
28
|
with:
|
|
29
29
|
args: 'format --check'
|
|
@@ -29,7 +29,7 @@ jobs:
|
|
|
29
29
|
|
|
30
30
|
steps:
|
|
31
31
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
32
|
-
- uses: astral-sh/setup-uv@
|
|
32
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
33
33
|
with:
|
|
34
34
|
version: "latest"
|
|
35
35
|
|
|
@@ -74,7 +74,7 @@ jobs:
|
|
|
74
74
|
steps:
|
|
75
75
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
76
76
|
|
|
77
|
-
- uses: astral-sh/setup-uv@
|
|
77
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
78
78
|
with:
|
|
79
79
|
version: "latest"
|
|
80
80
|
|
|
@@ -124,7 +124,7 @@ jobs:
|
|
|
124
124
|
steps:
|
|
125
125
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
126
126
|
|
|
127
|
-
- uses: astral-sh/setup-uv@
|
|
127
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
128
128
|
with:
|
|
129
129
|
version: 'latest'
|
|
130
130
|
|
|
@@ -226,7 +226,7 @@ jobs:
|
|
|
226
226
|
|
|
227
227
|
steps:
|
|
228
228
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
229
|
-
- uses: astral-sh/setup-uv@
|
|
229
|
+
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
230
230
|
with:
|
|
231
231
|
version: "latest"
|
|
232
232
|
|
|
@@ -27,8 +27,11 @@ A command line client for MySQL with auto-completion and syntax highlighting.
|
|
|
27
27
|
├── mycli/lexer.py # extends `MySqlLexer` from Pygments
|
|
28
28
|
├── mycli/main.py # processes CLI arguments
|
|
29
29
|
├── mycli/main_modes/ # main execution paths
|
|
30
|
-
├── mycli/main_modes/batch.py # batch mode
|
|
31
|
-
├── mycli/main_modes/checkup.py #
|
|
30
|
+
├── mycli/main_modes/batch.py # `--batch` mode
|
|
31
|
+
├── mycli/main_modes/checkup.py # `--checkup` mode
|
|
32
|
+
├── mycli/main_modes/execute.py # `--execute` mode
|
|
33
|
+
├── mycli/main_modes/list_dsn.py # `--list-dsn` mode
|
|
34
|
+
├── mycli/main_modes/repl.py # interactive REPL made
|
|
32
35
|
├── mycli/myclirc # project-level configuration file
|
|
33
36
|
├── mycli/output.py # `OutputMixin` mixin for feedback and output of query results
|
|
34
37
|
├── mycli/packages/ # application packages
|
|
@@ -48,13 +51,14 @@ A command line client for MySQL with auto-completion and syntax highlighting.
|
|
|
48
51
|
├── mycli/schema_prefetcher.py # background prefetcher for multi-schema auto-completion
|
|
49
52
|
├── mycli/sqlcompleter.py # offers SQL completions
|
|
50
53
|
├── mycli/sqlexecute.py # runs SQL queries
|
|
54
|
+
├── mycli/types.py # shared types
|
|
55
|
+
├── mycli/vault.py # Vault integration
|
|
51
56
|
├── test/conftest.py # pytest configuration
|
|
52
57
|
├── test/features/ # behave tests
|
|
53
58
|
├── test/myclirc # mycli configuration used for tests
|
|
54
59
|
├── test/mylogin.cnf # `mylogin.cnf` example used for tests
|
|
55
60
|
├── test/pytests/ # pytest tests
|
|
56
|
-
|
|
57
|
-
└── types.py # shared types
|
|
61
|
+
└── test/utils.py # shared utilities for tests
|
|
58
62
|
|
|
59
63
|
## Development
|
|
60
64
|
|
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
2.2.0 (2026/07/11)
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Breaking Changes
|
|
5
|
+
---------
|
|
6
|
+
* `ssl` is no longer supported as a DSN parameter. Use `ssl_mode` instead.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
Features
|
|
10
|
+
---------
|
|
11
|
+
* Configurable tunnel method for `--ssh-jump`, making UDS usually default.
|
|
12
|
+
* Add `--ssh-options` CLI argument to pass extra options with `--ssh-jump`.
|
|
13
|
+
* Make `ssh_jump` a DSN query parameter.
|
|
14
|
+
* Warn on unknown DSN query parameters.
|
|
15
|
+
* Experimental: reading credentials from Vault using the `vault kv get` CLI.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Bug Fixes
|
|
19
|
+
---------
|
|
20
|
+
* Allow `/dsn` command to show correct values with `--ssh-jump`.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
Documentation
|
|
24
|
+
---------
|
|
25
|
+
* Add `--ssh-jump` to TIPS.
|
|
26
|
+
* Better document `/dsn show`.
|
|
27
|
+
* Improve `ssh_options` commentary.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Internal
|
|
31
|
+
---------
|
|
32
|
+
* Make flaky `/watch` test more lenient for CI.
|
|
33
|
+
* Tweak `AGENTS.md` project structure.
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
2.1.1 (2026/07/08)
|
|
37
|
+
==============
|
|
38
|
+
|
|
39
|
+
Breaking Changes
|
|
40
|
+
---------
|
|
41
|
+
* Remove undocumented support for `tcp://` and `socket://` DSNs.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
Features
|
|
45
|
+
---------
|
|
46
|
+
* Add `--ssh-jump` CLI argument, restoring SSH jump functionality.
|
|
47
|
+
* Add a `/dsn` command to manage persisted DSN aliases.
|
|
48
|
+
* Add `auto` option to ignore keyring when inside an SSH session.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Documentation
|
|
52
|
+
---------
|
|
53
|
+
* Fix a typo in myclirc commentary.
|
|
54
|
+
* Advertise forward-slash command forms in more places.
|
|
55
|
+
|
|
56
|
+
|
|
1
57
|
2.0.0 (2026/07/03)
|
|
2
58
|
==============
|
|
3
59
|
|
|
@@ -20,7 +20,7 @@ the --character-set option sets the character set for a single session!
|
|
|
20
20
|
|
|
21
21
|
the --unbuffered flag can save memory when in batch mode!
|
|
22
22
|
|
|
23
|
-
--use-keyring=
|
|
23
|
+
--use-keyring=auto lets you access the system keyring for passwords!
|
|
24
24
|
|
|
25
25
|
--use-keyring=reset resets a password saved to the system keyring!
|
|
26
26
|
|
|
@@ -40,6 +40,8 @@ the --login-path option lets you work with login-path files!
|
|
|
40
40
|
|
|
41
41
|
--keepalive-ticks=<num> sets keepalive pings for a single session!
|
|
42
42
|
|
|
43
|
+
connect via an SSH tunnel with --ssh-jump!
|
|
44
|
+
|
|
43
45
|
###
|
|
44
46
|
### commands
|
|
45
47
|
###
|
|
@@ -108,6 +110,8 @@ the /watch command executes a query every N seconds!
|
|
|
108
110
|
|
|
109
111
|
use /bug to file a bug on GitHub!
|
|
110
112
|
|
|
113
|
+
use /dsn to manage saved DSNs!
|
|
114
|
+
|
|
111
115
|
###
|
|
112
116
|
### environment variables
|
|
113
117
|
###
|
|
@@ -9,18 +9,39 @@ from urllib.parse import parse_qs, unquote, urlparse
|
|
|
9
9
|
import click
|
|
10
10
|
|
|
11
11
|
from mycli.config import str_to_bool
|
|
12
|
-
from mycli.constants import EMPTY_PASSWORD_FLAG_SENTINEL
|
|
12
|
+
from mycli.constants import EMPTY_PASSWORD_FLAG_SENTINEL
|
|
13
13
|
from mycli.main_modes.batch import main_batch_from_stdin, main_batch_with_progress_bar, main_batch_without_progress_bar
|
|
14
14
|
from mycli.main_modes.checkup import main_checkup
|
|
15
15
|
from mycli.main_modes.execute import main_execute_from_cli
|
|
16
16
|
from mycli.main_modes.list_dsn import main_list_dsn
|
|
17
17
|
from mycli.packages.cli_utils import is_valid_connection_scheme
|
|
18
|
+
from mycli.vault import (
|
|
19
|
+
DEFAULT_VAULT_EXECUTABLE,
|
|
20
|
+
DEFAULT_VAULT_PASSWORD_FIELD,
|
|
21
|
+
DEFAULT_VAULT_USERNAME_FIELD,
|
|
22
|
+
VaultError,
|
|
23
|
+
get_field_from_vault,
|
|
24
|
+
)
|
|
18
25
|
|
|
19
26
|
if TYPE_CHECKING:
|
|
20
27
|
from mycli.main import CliArgs
|
|
21
28
|
|
|
22
29
|
ClientFactory = Callable[..., Any]
|
|
23
30
|
ENV_VAR_PATTERN = re.compile(r'^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$')
|
|
31
|
+
KNOWN_DSN_QUERY_PARAMS = {
|
|
32
|
+
'character_set',
|
|
33
|
+
'keepalive_ticks',
|
|
34
|
+
'socket',
|
|
35
|
+
'ssh_jump',
|
|
36
|
+
'ssl_ca',
|
|
37
|
+
'ssl_capath',
|
|
38
|
+
'ssl_cert',
|
|
39
|
+
'ssl_cipher',
|
|
40
|
+
'ssl_key',
|
|
41
|
+
'ssl_mode',
|
|
42
|
+
'ssl_verify_server_cert',
|
|
43
|
+
'tls_version',
|
|
44
|
+
}
|
|
24
45
|
|
|
25
46
|
|
|
26
47
|
class DsnAliasEnvVarError(ValueError):
|
|
@@ -79,7 +100,10 @@ def expand_dsn_alias_env_vars(
|
|
|
79
100
|
except ValueError as exc:
|
|
80
101
|
raise DsnAliasEnvVarError(f'Port in DSN alias {alias_name} must be an integer.') from exc
|
|
81
102
|
|
|
82
|
-
params = {
|
|
103
|
+
params = {
|
|
104
|
+
key: [expand_dsn_alias_env_var(value, alias_name) or '' for value in values]
|
|
105
|
+
for key, values in parse_qs(uri.query, keep_blank_values=True).items()
|
|
106
|
+
}
|
|
83
107
|
|
|
84
108
|
return (
|
|
85
109
|
expand_dsn_alias_env_var(unquote(username) if username is not None else None, alias_name),
|
|
@@ -200,7 +224,7 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
|
|
|
200
224
|
dsn_host = uri.hostname
|
|
201
225
|
dsn_port = uri.port
|
|
202
226
|
dsn_database = uri.path[1:]
|
|
203
|
-
dsn_params = parse_qs(uri.query) if uri.query else {}
|
|
227
|
+
dsn_params = parse_qs(uri.query, keep_blank_values=True) if uri.query else {}
|
|
204
228
|
|
|
205
229
|
if not database:
|
|
206
230
|
database = dsn_database
|
|
@@ -214,16 +238,13 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
|
|
|
214
238
|
if not cli_args.port:
|
|
215
239
|
cli_args.port = dsn_port
|
|
216
240
|
|
|
217
|
-
if
|
|
241
|
+
if unknown_dsn_params := sorted(set(dsn_params) - KNOWN_DSN_QUERY_PARAMS):
|
|
218
242
|
click.secho(
|
|
219
|
-
'Warning:
|
|
220
|
-
'Please use the "ssl_mode" parameter instead. '
|
|
221
|
-
f'See issue {ISSUES_URL}/1507',
|
|
243
|
+
f'Warning: Ignored unknown DSN URI query parameters: {", ".join(unknown_dsn_params)}.',
|
|
222
244
|
err=True,
|
|
223
245
|
fg='yellow',
|
|
224
246
|
)
|
|
225
|
-
|
|
226
|
-
cli_args.ssl_mode = 'on'
|
|
247
|
+
|
|
227
248
|
if params := dsn_params.get('ssl_mode'):
|
|
228
249
|
cli_args.ssl_mode = cli_args.ssl_mode or params[0]
|
|
229
250
|
if params := dsn_params.get('ssl_ca'):
|
|
@@ -254,6 +275,8 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
|
|
|
254
275
|
cli_args.keepalive_ticks = int(params[0])
|
|
255
276
|
if params := dsn_params.get('character_set'):
|
|
256
277
|
cli_args.character_set = cli_args.character_set or params[0]
|
|
278
|
+
if params := dsn_params.get('ssh_jump'):
|
|
279
|
+
cli_args.ssh_jump = cli_args.ssh_jump or params[0]
|
|
257
280
|
|
|
258
281
|
keepalive_ticks = cli_args.keepalive_ticks if cli_args.keepalive_ticks is not None else mycli.default_keepalive_ticks
|
|
259
282
|
ssl_mode = cli_args.ssl_mode or mycli.ssl_mode
|
|
@@ -304,52 +327,111 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
|
|
|
304
327
|
if cli_args.use_keyring is not None and cli_args.use_keyring.lower() == 'reset':
|
|
305
328
|
use_keyring = True
|
|
306
329
|
reset_keyring = True
|
|
330
|
+
elif cli_args.use_keyring is not None and cli_args.use_keyring.lower() == 'auto':
|
|
331
|
+
if os.environ.get('SSH_CONNECTION'):
|
|
332
|
+
use_keyring = False
|
|
333
|
+
reset_keyring = False
|
|
334
|
+
else:
|
|
335
|
+
use_keyring = True
|
|
336
|
+
reset_keyring = False
|
|
307
337
|
elif cli_args.use_keyring is None:
|
|
308
|
-
|
|
309
|
-
|
|
338
|
+
if mycli.config['main'].get('use_keyring', 'False').lower() == 'auto':
|
|
339
|
+
if os.environ.get('SSH_CONNECTION'):
|
|
340
|
+
use_keyring = False
|
|
341
|
+
reset_keyring = False
|
|
342
|
+
else:
|
|
343
|
+
use_keyring = True
|
|
344
|
+
reset_keyring = False
|
|
345
|
+
else:
|
|
346
|
+
use_keyring = str_to_bool(mycli.config['main'].get('use_keyring', 'False'))
|
|
347
|
+
reset_keyring = False
|
|
310
348
|
else:
|
|
311
349
|
use_keyring = str_to_bool(cli_args.use_keyring)
|
|
312
350
|
reset_keyring = False
|
|
313
351
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
352
|
+
if cli_args.password is None and cli_args.vault_secret:
|
|
353
|
+
vault_config = mycli.config.get('vault_beta', {})
|
|
354
|
+
vault_address = cli_args.vault_address or os.environ.get('VAULT_ADDR') or vault_config.get('address') or None
|
|
355
|
+
vault_mount = cli_args.vault_mount or vault_config.get('default_mount') or None
|
|
356
|
+
vault_field = cli_args.vault_password_field or vault_config.get('default_password_field') or DEFAULT_VAULT_PASSWORD_FIELD
|
|
357
|
+
vault_executable = vault_config.get('vault_executable') or DEFAULT_VAULT_EXECUTABLE
|
|
358
|
+
try:
|
|
359
|
+
vault_password: str | None = get_field_from_vault(
|
|
360
|
+
vault_field,
|
|
361
|
+
cli_args.vault_secret,
|
|
362
|
+
executable=vault_executable,
|
|
363
|
+
mount=vault_mount,
|
|
364
|
+
address=vault_address,
|
|
365
|
+
)
|
|
366
|
+
except VaultError as exc:
|
|
367
|
+
click.secho(f'Error reading password from Vault: {exc}', err=True, fg='red')
|
|
368
|
+
sys.exit(1)
|
|
369
|
+
else:
|
|
370
|
+
vault_password = None
|
|
371
|
+
|
|
372
|
+
if cli_args.user is None and cli_args.vault_secret:
|
|
373
|
+
vault_config = mycli.config.get('vault_beta', {})
|
|
374
|
+
vault_address = cli_args.vault_address or os.environ.get('VAULT_ADDR') or vault_config.get('address') or None
|
|
375
|
+
vault_mount = cli_args.vault_mount or vault_config.get('default_mount') or None
|
|
376
|
+
vault_field = cli_args.vault_username_field or vault_config.get('default_username_field') or DEFAULT_VAULT_USERNAME_FIELD
|
|
377
|
+
vault_executable = vault_config.get('vault_executable') or DEFAULT_VAULT_EXECUTABLE
|
|
378
|
+
try:
|
|
379
|
+
vault_username = get_field_from_vault(
|
|
380
|
+
vault_field,
|
|
381
|
+
cli_args.vault_secret,
|
|
382
|
+
executable=vault_executable,
|
|
383
|
+
mount=vault_mount,
|
|
384
|
+
address=vault_address,
|
|
385
|
+
)
|
|
386
|
+
except VaultError as exc:
|
|
387
|
+
click.secho(f'Error reading username from Vault: {exc}', err=True, fg='red')
|
|
388
|
+
sys.exit(1)
|
|
389
|
+
else:
|
|
390
|
+
vault_username = None
|
|
333
391
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
392
|
+
try:
|
|
393
|
+
mycli.connect(
|
|
394
|
+
database=database,
|
|
395
|
+
user=vault_username if cli_args.user is None else cli_args.user,
|
|
396
|
+
passwd=vault_password if cli_args.password is None else cli_args.password,
|
|
397
|
+
host=cli_args.host,
|
|
398
|
+
port=cli_args.port,
|
|
399
|
+
socket=cli_args.socket,
|
|
400
|
+
local_infile=cli_args.local_infile,
|
|
401
|
+
ssl=ssl,
|
|
402
|
+
init_command=combined_init_cmd,
|
|
403
|
+
unbuffered=cli_args.unbuffered,
|
|
404
|
+
character_set=cli_args.character_set,
|
|
405
|
+
use_keyring=use_keyring,
|
|
406
|
+
reset_keyring=reset_keyring,
|
|
407
|
+
keepalive_ticks=keepalive_ticks,
|
|
408
|
+
ssh_jump=cli_args.ssh_jump,
|
|
409
|
+
ssh_cli_options=cli_args.ssh_options,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if combined_init_cmd:
|
|
413
|
+
click.echo(f"Executing init-command: {combined_init_cmd}", err=True)
|
|
414
|
+
|
|
415
|
+
mycli.logger.debug(
|
|
416
|
+
"Launch Params: \n\tdatabase: %r\tuser: %r\thost: %r\tport: %r",
|
|
417
|
+
database,
|
|
418
|
+
cli_args.user,
|
|
419
|
+
cli_args.host,
|
|
420
|
+
cli_args.port,
|
|
421
|
+
)
|
|
341
422
|
|
|
342
|
-
|
|
343
|
-
|
|
423
|
+
if cli_args.execute is not None:
|
|
424
|
+
sys.exit(main_execute_from_cli(mycli, cli_args))
|
|
344
425
|
|
|
345
|
-
|
|
346
|
-
|
|
426
|
+
if cli_args.batch is not None and cli_args.batch != '-' and cli_args.progress and sys.stderr.isatty():
|
|
427
|
+
sys.exit(main_batch_with_progress_bar(mycli, cli_args))
|
|
347
428
|
|
|
348
|
-
|
|
349
|
-
|
|
429
|
+
if cli_args.batch is not None:
|
|
430
|
+
sys.exit(main_batch_without_progress_bar(mycli, cli_args))
|
|
350
431
|
|
|
351
|
-
|
|
352
|
-
|
|
432
|
+
if not sys.stdin.isatty():
|
|
433
|
+
sys.exit(main_batch_from_stdin(mycli, cli_args))
|
|
353
434
|
|
|
354
|
-
|
|
355
|
-
|
|
435
|
+
mycli.run_cli()
|
|
436
|
+
finally:
|
|
437
|
+
mycli.close()
|
|
@@ -35,11 +35,13 @@ from mycli.constants import DEFAULT_PROMPT
|
|
|
35
35
|
from mycli.main_modes import repl as repl_package
|
|
36
36
|
from mycli.output import OutputMixin
|
|
37
37
|
from mycli.packages import special
|
|
38
|
+
from mycli.packages.special.dsn_aliases import DsnAliases
|
|
38
39
|
from mycli.packages.special.favoritequeries import FavoriteQueries
|
|
39
40
|
from mycli.packages.tabular_output import sql_format
|
|
40
41
|
from mycli.schema_prefetcher import SchemaPrefetcher
|
|
41
42
|
from mycli.sqlcompleter import SQLCompleter
|
|
42
43
|
from mycli.sqlexecute import SQLExecute
|
|
44
|
+
from mycli.ssh_tunnel import SshTunnel
|
|
43
45
|
from mycli.types import Query
|
|
44
46
|
|
|
45
47
|
sqlparse.engine.grouping.MAX_GROUPING_DEPTH = None # type: ignore[assignment]
|
|
@@ -75,6 +77,7 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
|
|
|
75
77
|
cli_verbosity: int = 0,
|
|
76
78
|
) -> None:
|
|
77
79
|
self.sqlexecute = sqlexecute
|
|
80
|
+
self.ssh_tunnel: SshTunnel | None = None
|
|
78
81
|
self.logfile = logfile
|
|
79
82
|
self.login_path = login_path
|
|
80
83
|
self.toolbar_error_message: str | None = None
|
|
@@ -106,6 +109,7 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
|
|
|
106
109
|
self.default_keepalive_ticks = c['connection'].as_int('default_keepalive_ticks')
|
|
107
110
|
|
|
108
111
|
FavoriteQueries.instance = FavoriteQueries.from_config(self.config)
|
|
112
|
+
DsnAliases.instance = DsnAliases.from_config(self.config)
|
|
109
113
|
|
|
110
114
|
self.dsn_alias: str | None = None
|
|
111
115
|
self.main_formatter = TabularOutputFormatter(format_name=c["main"]["table_format"])
|
|
@@ -198,10 +202,20 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
|
|
|
198
202
|
special.set_destructive_keywords(self.destructive_keywords)
|
|
199
203
|
|
|
200
204
|
def close(self) -> None:
|
|
201
|
-
|
|
205
|
+
try:
|
|
202
206
|
self.schema_prefetcher.stop()
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
203
209
|
if self.sqlexecute is not None:
|
|
204
|
-
|
|
210
|
+
try:
|
|
211
|
+
self.sqlexecute.close()
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
if self.ssh_tunnel is not None:
|
|
215
|
+
try:
|
|
216
|
+
self.ssh_tunnel.close()
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
205
219
|
|
|
206
220
|
def run_cli(self) -> None:
|
|
207
221
|
repl_package.main_repl(self)
|
|
@@ -3,7 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
5
|
import traceback
|
|
6
|
-
from typing import TYPE_CHECKING, Any
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
7
|
+
from urllib.parse import quote as urlquote
|
|
7
8
|
|
|
8
9
|
import click
|
|
9
10
|
import keyring
|
|
@@ -21,7 +22,9 @@ from mycli.constants import (
|
|
|
21
22
|
ER_MUST_CHANGE_PASSWORD_LOGIN,
|
|
22
23
|
)
|
|
23
24
|
from mycli.packages.filepaths import guess_socket_location
|
|
25
|
+
from mycli.packages.special.utils import format_connection_dsn
|
|
24
26
|
from mycli.sqlexecute import SQLExecute
|
|
27
|
+
from mycli.ssh_tunnel import SshTunnel, SshTunnelError
|
|
25
28
|
|
|
26
29
|
try:
|
|
27
30
|
from pwd import getpwuid
|
|
@@ -58,6 +61,8 @@ class ClientConnectionMixin:
|
|
|
58
61
|
use_keyring: bool | None = None,
|
|
59
62
|
reset_keyring: bool | None = None,
|
|
60
63
|
keepalive_ticks: int | None = None,
|
|
64
|
+
ssh_jump: str | None = None,
|
|
65
|
+
ssh_cli_options: str | None = None,
|
|
61
66
|
) -> None:
|
|
62
67
|
mylogin_cnf: dict[str, Any] = self.read_mylogin_cnf(self.mylogin_cnf)
|
|
63
68
|
# Fall back to .mylogin.cnf values only if user did not specify a value.
|
|
@@ -67,6 +72,7 @@ class ClientConnectionMixin:
|
|
|
67
72
|
ssl_config: dict[str, Any] = ssl or {}
|
|
68
73
|
user_connection_config = self.config_without_package_defaults.get('connection', {})
|
|
69
74
|
self.keepalive_ticks = keepalive_ticks
|
|
75
|
+
self.ssh_tunnel = None
|
|
70
76
|
|
|
71
77
|
int_port = port and int(port)
|
|
72
78
|
if not int_port:
|
|
@@ -74,6 +80,34 @@ class ClientConnectionMixin:
|
|
|
74
80
|
if not host or host == DEFAULT_HOST:
|
|
75
81
|
socket = socket or user_connection_config.get("default_socket") or mylogin_cnf["socket"] or guess_socket_location()
|
|
76
82
|
|
|
83
|
+
if ssh_jump:
|
|
84
|
+
remote_host = host or DEFAULT_HOST
|
|
85
|
+
remote_port = int_port or DEFAULT_PORT
|
|
86
|
+
remote_socket = socket or None
|
|
87
|
+
ssh_executable = self.config.get('ssh', {}).get('ssh_executable', 'ssh') or 'ssh'
|
|
88
|
+
ssh_config_options = self.config.get('ssh', {}).get('ssh_options')
|
|
89
|
+
tunnel_method: Literal['auto', 'socket', 'port'] = self.config.get('ssh', {}).get('tunnel_method', 'auto').lower() or 'auto'
|
|
90
|
+
try:
|
|
91
|
+
self.ssh_tunnel = SshTunnel.from_target(
|
|
92
|
+
ssh_jump,
|
|
93
|
+
remote_host=remote_host,
|
|
94
|
+
remote_port=int(remote_port),
|
|
95
|
+
remote_socket=remote_socket,
|
|
96
|
+
ssh_executable=ssh_executable,
|
|
97
|
+
ssh_config_options=ssh_config_options,
|
|
98
|
+
ssh_cli_options=ssh_cli_options,
|
|
99
|
+
tunnel_method=tunnel_method,
|
|
100
|
+
)
|
|
101
|
+
self.ssh_tunnel.start()
|
|
102
|
+
except (OSError, ValueError, SshTunnelError) as exc:
|
|
103
|
+
click.secho(f'Error: Unable to start SSH tunnel: {exc}', err=True, fg='red')
|
|
104
|
+
try:
|
|
105
|
+
if self.ssh_tunnel:
|
|
106
|
+
self.ssh_tunnel.close()
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
77
111
|
passwd = passwd if isinstance(passwd, (str, int)) else mylogin_cnf["password"]
|
|
78
112
|
|
|
79
113
|
if not character_set:
|
|
@@ -132,10 +166,14 @@ class ClientConnectionMixin:
|
|
|
132
166
|
# 2. --password-file CLI option
|
|
133
167
|
# 3. envvar (MYSQL_PWD)
|
|
134
168
|
# 4. DSN (mysql://user:password)
|
|
135
|
-
# 5.
|
|
136
|
-
# 6.
|
|
137
|
-
|
|
138
|
-
|
|
169
|
+
# 5. Vault
|
|
170
|
+
# 6. .mylogin.cnf
|
|
171
|
+
# 7. keyring
|
|
172
|
+
|
|
173
|
+
ssh_tunnel_field = urlquote(ssh_jump or '')
|
|
174
|
+
if ssh_tunnel_field:
|
|
175
|
+
ssh_tunnel_field = ':' + ssh_tunnel_field
|
|
176
|
+
keyring_identifier = f'{user}@{host}:{"" if socket else int_port}:{socket or ""}{ssh_tunnel_field}'
|
|
139
177
|
keyring_domain = 'mycli.net'
|
|
140
178
|
keyring_retrieved_cleanly = False
|
|
141
179
|
|
|
@@ -152,19 +190,41 @@ class ClientConnectionMixin:
|
|
|
152
190
|
# should not fail, but will help the typechecker
|
|
153
191
|
assert not isinstance(passwd, int)
|
|
154
192
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
193
|
+
display_dsn = None
|
|
194
|
+
if self.ssh_tunnel:
|
|
195
|
+
display_dsn = format_connection_dsn(
|
|
196
|
+
user=user,
|
|
197
|
+
host=remote_host,
|
|
198
|
+
port=remote_port,
|
|
199
|
+
socket=remote_socket,
|
|
200
|
+
database=database,
|
|
201
|
+
character_set=character_set,
|
|
202
|
+
ssh_jump=ssh_jump,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
connection_info: dict[str, Any] = {
|
|
206
|
+
'database': database,
|
|
207
|
+
'user': user,
|
|
208
|
+
'password': passwd,
|
|
209
|
+
'character_set': character_set,
|
|
210
|
+
'local_infile': use_local_infile,
|
|
211
|
+
'ssl': ssl_config,
|
|
212
|
+
'init_command': init_command,
|
|
213
|
+
'unbuffered': unbuffered,
|
|
214
|
+
'display_dsn': display_dsn,
|
|
167
215
|
}
|
|
216
|
+
if self.ssh_tunnel and self.ssh_tunnel.local_socket:
|
|
217
|
+
connection_info['host'] = None
|
|
218
|
+
connection_info['port'] = None
|
|
219
|
+
connection_info['socket'] = self.ssh_tunnel.local_socket
|
|
220
|
+
elif self.ssh_tunnel:
|
|
221
|
+
connection_info['host'] = self.ssh_tunnel.local_host
|
|
222
|
+
connection_info['port'] = self.ssh_tunnel.local_port
|
|
223
|
+
connection_info['socket'] = None
|
|
224
|
+
else:
|
|
225
|
+
connection_info['host'] = host
|
|
226
|
+
connection_info['port'] = int_port
|
|
227
|
+
connection_info['socket'] = socket
|
|
168
228
|
|
|
169
229
|
def _update_keyring(password: str | None, keyring_retrieved_cleanly: bool):
|
|
170
230
|
if not password:
|
|
@@ -239,6 +299,8 @@ class ClientConnectionMixin:
|
|
|
239
299
|
socket_owner = getpwuid(os.stat(socket).st_uid).pw_name
|
|
240
300
|
except KeyError:
|
|
241
301
|
socket_owner = '<unknown>'
|
|
302
|
+
except FileNotFoundError:
|
|
303
|
+
socket_owner = '<unknown>'
|
|
242
304
|
self.echo(f"Connecting to socket {socket}, owned by user {socket_owner}", err=True)
|
|
243
305
|
try:
|
|
244
306
|
_connect(keyring_retrieved_cleanly=keyring_retrieved_cleanly)
|