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.
Files changed (175) hide show
  1. {mycli-2.0.0 → mycli-2.2.0}/.github/workflows/ci.yml +4 -4
  2. {mycli-2.0.0 → mycli-2.2.0}/.github/workflows/lint.yml +2 -2
  3. {mycli-2.0.0 → mycli-2.2.0}/.github/workflows/publish.yml +4 -4
  4. {mycli-2.0.0 → mycli-2.2.0}/.github/workflows/typecheck.yml +1 -1
  5. {mycli-2.0.0 → mycli-2.2.0}/AGENTS.md +8 -4
  6. {mycli-2.0.0/mycli.egg-info → mycli-2.2.0}/PKG-INFO +1 -1
  7. {mycli-2.0.0 → mycli-2.2.0}/changelog.md +56 -0
  8. {mycli-2.0.0 → mycli-2.2.0}/mycli/TIPS +5 -1
  9. {mycli-2.0.0 → mycli-2.2.0}/mycli/cli_runner.py +129 -47
  10. {mycli-2.0.0 → mycli-2.2.0}/mycli/client.py +16 -2
  11. {mycli-2.0.0 → mycli-2.2.0}/mycli/client_connection.py +79 -17
  12. {mycli-2.0.0 → mycli-2.2.0}/mycli/main.py +39 -11
  13. {mycli-2.0.0 → mycli-2.2.0}/mycli/myclirc +34 -2
  14. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/cli_utils.py +2 -6
  15. mycli-2.2.0/mycli/packages/special/dsn_aliases.py +64 -0
  16. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/favoritequeries.py +4 -4
  17. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/iocommands.py +47 -1
  18. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/llm.py +6 -6
  19. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/utils.py +46 -0
  20. {mycli-2.0.0 → mycli-2.2.0}/mycli/sqlexecute.py +4 -0
  21. mycli-2.2.0/mycli/ssh_tunnel.py +212 -0
  22. mycli-2.2.0/mycli/vault.py +54 -0
  23. {mycli-2.0.0 → mycli-2.2.0/mycli.egg-info}/PKG-INFO +1 -1
  24. {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/SOURCES.txt +7 -1
  25. {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/scm_file_list.json +6 -0
  26. mycli-2.2.0/mycli.egg-info/scm_version.json +8 -0
  27. mycli-2.2.0/test/features/fixture_data/help_commands.txt +39 -0
  28. {mycli-2.0.0 → mycli-2.2.0}/test/myclirc +34 -2
  29. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_cli_runner.py +414 -2
  30. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_cli_utils.py +2 -2
  31. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_client.py +17 -1
  32. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_client_connection.py +242 -1
  33. mycli-2.2.0/test/pytests/test_dsn_aliases.py +102 -0
  34. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_main.py +61 -0
  35. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_smart_completion_public_schema_only.py +4 -1
  36. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_iocommands.py +106 -2
  37. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_utils.py +75 -0
  38. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_sqlexecute.py +3 -0
  39. mycli-2.2.0/test/pytests/test_ssh_tunnel.py +512 -0
  40. mycli-2.2.0/test/pytests/test_vault.py +96 -0
  41. mycli-2.0.0/mycli.egg-info/scm_version.json +0 -8
  42. mycli-2.0.0/test/features/fixture_data/help_commands.txt +0 -38
  43. {mycli-2.0.0 → mycli-2.2.0}/.git-blame-ignore-revs +0 -0
  44. {mycli-2.0.0 → mycli-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  45. {mycli-2.0.0 → mycli-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  46. {mycli-2.0.0 → mycli-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  47. {mycli-2.0.0 → mycli-2.2.0}/.github/dependabot.yml +0 -0
  48. {mycli-2.0.0 → mycli-2.2.0}/.gitignore +0 -0
  49. {mycli-2.0.0 → mycli-2.2.0}/AUTHORS.rst +0 -0
  50. {mycli-2.0.0 → mycli-2.2.0}/CONTRIBUTING.md +0 -0
  51. {mycli-2.0.0 → mycli-2.2.0}/LICENSE.txt +0 -0
  52. {mycli-2.0.0 → mycli-2.2.0}/MANIFEST.in +0 -0
  53. {mycli-2.0.0 → mycli-2.2.0}/README.md +0 -0
  54. {mycli-2.0.0 → mycli-2.2.0}/SPONSORS.rst +0 -0
  55. {mycli-2.0.0 → mycli-2.2.0}/doc/key_bindings.rst +0 -0
  56. {mycli-2.0.0 → mycli-2.2.0}/doc/llm.md +0 -0
  57. {mycli-2.0.0 → mycli-2.2.0}/doc/screenshots/main.gif +0 -0
  58. {mycli-2.0.0 → mycli-2.2.0}/doc/screenshots/tables.png +0 -0
  59. {mycli-2.0.0 → mycli-2.2.0}/mycli/AUTHORS +0 -0
  60. {mycli-2.0.0 → mycli-2.2.0}/mycli/SPONSORS +0 -0
  61. {mycli-2.0.0 → mycli-2.2.0}/mycli/__init__.py +0 -0
  62. {mycli-2.0.0 → mycli-2.2.0}/mycli/app_state.py +0 -0
  63. {mycli-2.0.0 → mycli-2.2.0}/mycli/clibuffer.py +0 -0
  64. {mycli-2.0.0 → mycli-2.2.0}/mycli/client_commands.py +0 -0
  65. {mycli-2.0.0 → mycli-2.2.0}/mycli/client_query.py +0 -0
  66. {mycli-2.0.0 → mycli-2.2.0}/mycli/clistyle.py +0 -0
  67. {mycli-2.0.0 → mycli-2.2.0}/mycli/clitoolbar.py +0 -0
  68. {mycli-2.0.0 → mycli-2.2.0}/mycli/compat.py +0 -0
  69. {mycli-2.0.0 → mycli-2.2.0}/mycli/completion_refresher.py +0 -0
  70. {mycli-2.0.0 → mycli-2.2.0}/mycli/config.py +0 -0
  71. {mycli-2.0.0 → mycli-2.2.0}/mycli/constants.py +0 -0
  72. {mycli-2.0.0 → mycli-2.2.0}/mycli/key_bindings.py +0 -0
  73. {mycli-2.0.0 → mycli-2.2.0}/mycli/lexer.py +0 -0
  74. {mycli-2.0.0 → mycli-2.2.0}/mycli/main_modes/batch.py +0 -0
  75. {mycli-2.0.0 → mycli-2.2.0}/mycli/main_modes/checkup.py +0 -0
  76. {mycli-2.0.0 → mycli-2.2.0}/mycli/main_modes/execute.py +0 -0
  77. {mycli-2.0.0 → mycli-2.2.0}/mycli/main_modes/list_dsn.py +0 -0
  78. {mycli-2.0.0 → mycli-2.2.0}/mycli/main_modes/repl.py +0 -0
  79. {mycli-2.0.0 → mycli-2.2.0}/mycli/output.py +0 -0
  80. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/__init__.py +0 -0
  81. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/batch_utils.py +0 -0
  82. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/completion_engine.py +0 -0
  83. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/filepaths.py +0 -0
  84. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/hybrid_redirection.py +0 -0
  85. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/interactive_utils.py +0 -0
  86. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/key_binding_utils.py +0 -0
  87. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/ptoolkit/__init__.py +0 -0
  88. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/ptoolkit/fzf.py +0 -0
  89. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/ptoolkit/history.py +0 -0
  90. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/ptoolkit/utils.py +0 -0
  91. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/__init__.py +0 -0
  92. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/dbcommands.py +0 -0
  93. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/delimitercommand.py +0 -0
  94. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/special/main.py +0 -0
  95. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/sql_utils.py +0 -0
  96. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/sqlresult.py +0 -0
  97. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/string_utils.py +0 -0
  98. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/tabular_output/__init__.py +0 -0
  99. {mycli-2.0.0 → mycli-2.2.0}/mycli/packages/tabular_output/sql_format.py +0 -0
  100. {mycli-2.0.0 → mycli-2.2.0}/mycli/schema_prefetcher.py +0 -0
  101. {mycli-2.0.0 → mycli-2.2.0}/mycli/sqlcompleter.py +0 -0
  102. {mycli-2.0.0 → mycli-2.2.0}/mycli/types.py +0 -0
  103. {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/dependency_links.txt +0 -0
  104. {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/entry_points.txt +0 -0
  105. {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/requires.txt +0 -0
  106. {mycli-2.0.0 → mycli-2.2.0}/mycli.egg-info/top_level.txt +0 -0
  107. {mycli-2.0.0 → mycli-2.2.0}/pyproject.toml +0 -0
  108. {mycli-2.0.0 → mycli-2.2.0}/setup.cfg +0 -0
  109. {mycli-2.0.0 → mycli-2.2.0}/test/__init__.py +0 -0
  110. {mycli-2.0.0 → mycli-2.2.0}/test/features/__init__.py +0 -0
  111. {mycli-2.0.0 → mycli-2.2.0}/test/features/auto_vertical.feature +0 -0
  112. {mycli-2.0.0 → mycli-2.2.0}/test/features/basic_commands.feature +0 -0
  113. {mycli-2.0.0 → mycli-2.2.0}/test/features/connection.feature +0 -0
  114. {mycli-2.0.0 → mycli-2.2.0}/test/features/crud_database.feature +0 -0
  115. {mycli-2.0.0 → mycli-2.2.0}/test/features/crud_table.feature +0 -0
  116. {mycli-2.0.0 → mycli-2.2.0}/test/features/db_utils.py +0 -0
  117. {mycli-2.0.0 → mycli-2.2.0}/test/features/environment.py +0 -0
  118. {mycli-2.0.0 → mycli-2.2.0}/test/features/fixture_utils.py +0 -0
  119. {mycli-2.0.0 → mycli-2.2.0}/test/features/iocommands.feature +0 -0
  120. {mycli-2.0.0 → mycli-2.2.0}/test/features/named_queries.feature +0 -0
  121. {mycli-2.0.0 → mycli-2.2.0}/test/features/specials.feature +0 -0
  122. {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/__init__.py +0 -0
  123. {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/auto_vertical.py +0 -0
  124. {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/basic_commands.py +0 -0
  125. {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/connection.py +0 -0
  126. {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/crud_database.py +0 -0
  127. {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/crud_table.py +0 -0
  128. {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/iocommands.py +0 -0
  129. {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/named_queries.py +0 -0
  130. {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/specials.py +0 -0
  131. {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/utils.py +0 -0
  132. {mycli-2.0.0 → mycli-2.2.0}/test/features/steps/wrappers.py +0 -0
  133. {mycli-2.0.0 → mycli-2.2.0}/test/features/wrappager.py +0 -0
  134. {mycli-2.0.0 → mycli-2.2.0}/test/mylogin.cnf +0 -0
  135. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/conftest.py +0 -0
  136. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_app_state.py +0 -0
  137. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_batch_utils.py +0 -0
  138. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_checkup.py +0 -0
  139. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_clibuffer.py +0 -0
  140. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_client_commands.py +0 -0
  141. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_client_query.py +0 -0
  142. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_clistyle.py +0 -0
  143. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_clitoolbar.py +0 -0
  144. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_completer_use_switch.py +0 -0
  145. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_completion_engine.py +0 -0
  146. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_completion_refresher.py +0 -0
  147. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_config.py +0 -0
  148. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_delimitercommand.py +0 -0
  149. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_favoritequeries.py +0 -0
  150. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_filepaths.py +0 -0
  151. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_hybrid_redirection.py +0 -0
  152. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_interactive_utils.py +0 -0
  153. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_key_binding_utils.py +0 -0
  154. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_key_bindings.py +0 -0
  155. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_lexer.py +0 -0
  156. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_main_modes_batch.py +0 -0
  157. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_main_modes_execute.py +0 -0
  158. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
  159. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_main_modes_repl.py +0 -0
  160. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_naive_completion.py +0 -0
  161. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_output.py +0 -0
  162. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
  163. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_ptoolkit_history.py +0 -0
  164. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_ptoolkit_utils.py +0 -0
  165. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_schema_prefetcher.py +0 -0
  166. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_dbcommands.py +0 -0
  167. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_init.py +0 -0
  168. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_llm.py +0 -0
  169. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_special_main.py +0 -0
  170. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_sql_utils.py +0 -0
  171. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_sqlcompleter.py +0 -0
  172. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_sqlresult.py +0 -0
  173. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_string_utils.py +0 -0
  174. {mycli-2.0.0 → mycli-2.2.0}/test/pytests/test_tabular_output.py +0 -0
  175. {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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
24
+ uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v4.1.0
25
25
 
26
26
  - name: Run ruff format
27
- uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
229
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
230
230
  with:
231
231
  version: "latest"
232
232
 
@@ -25,7 +25,7 @@ jobs:
25
25
  with:
26
26
  python-version: '3.14'
27
27
 
28
- - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
28
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
29
29
  with:
30
30
  version: 'latest'
31
31
 
@@ -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 execution path
31
- ├── mycli/main_modes/checkup.py # implementation of `--checkup` mode
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
- ├── test/utils.py # shared utilities for tests
57
- └── types.py # shared types
61
+ └── test/utils.py # shared utilities for tests
58
62
 
59
63
  ## Development
60
64
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.0.0
3
+ Version: 2.2.0
4
4
  Summary: CLI for MySQL Database. With auto-completion and syntax highlighting.
5
5
  Author: Mycli Core Team
6
6
  License-Expression: BSD-3-Clause
@@ -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=true lets you access the system keyring for passwords!
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, ISSUES_URL
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 = {key: [expand_dsn_alias_env_var(value, alias_name) or '' for value in values] for key, values in parse_qs(uri.query).items()}
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 params := dsn_params.get('ssl'):
241
+ if unknown_dsn_params := sorted(set(dsn_params) - KNOWN_DSN_QUERY_PARAMS):
218
242
  click.secho(
219
- 'Warning: The "ssl" DSN URI parameter is deprecated and will be removed in a future release. '
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
- if params[0].lower() == 'true':
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
- use_keyring = str_to_bool(mycli.config['main'].get('use_keyring', 'False'))
309
- reset_keyring = False
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
- mycli.connect(
315
- database=database,
316
- user=cli_args.user,
317
- passwd=cli_args.password,
318
- host=cli_args.host,
319
- port=cli_args.port,
320
- socket=cli_args.socket,
321
- local_infile=cli_args.local_infile,
322
- ssl=ssl,
323
- init_command=combined_init_cmd,
324
- unbuffered=cli_args.unbuffered,
325
- character_set=cli_args.character_set,
326
- use_keyring=use_keyring,
327
- reset_keyring=reset_keyring,
328
- keepalive_ticks=keepalive_ticks,
329
- )
330
-
331
- if combined_init_cmd:
332
- click.echo(f"Executing init-command: {combined_init_cmd}", err=True)
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
- mycli.logger.debug(
335
- "Launch Params: \n\tdatabase: %r\tuser: %r\thost: %r\tport: %r",
336
- database,
337
- cli_args.user,
338
- cli_args.host,
339
- cli_args.port,
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
- if cli_args.execute is not None:
343
- sys.exit(main_execute_from_cli(mycli, cli_args))
423
+ if cli_args.execute is not None:
424
+ sys.exit(main_execute_from_cli(mycli, cli_args))
344
425
 
345
- if cli_args.batch is not None and cli_args.batch != '-' and cli_args.progress and sys.stderr.isatty():
346
- sys.exit(main_batch_with_progress_bar(mycli, cli_args))
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
- if cli_args.batch is not None:
349
- sys.exit(main_batch_without_progress_bar(mycli, cli_args))
429
+ if cli_args.batch is not None:
430
+ sys.exit(main_batch_without_progress_bar(mycli, cli_args))
350
431
 
351
- if not sys.stdin.isatty():
352
- sys.exit(main_batch_from_stdin(mycli, cli_args))
432
+ if not sys.stdin.isatty():
433
+ sys.exit(main_batch_from_stdin(mycli, cli_args))
353
434
 
354
- mycli.run_cli()
355
- mycli.close()
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
- if hasattr(self, 'schema_prefetcher'):
205
+ try:
202
206
  self.schema_prefetcher.stop()
207
+ except Exception:
208
+ pass
203
209
  if self.sqlexecute is not None:
204
- self.sqlexecute.close()
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. .mylogin.cnf
136
- # 6. keyring
137
-
138
- keyring_identifier = f'{user}@{host}:{"" if socket else int_port}:{socket or ""}'
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
- connection_info: dict[Any, Any] = {
156
- "database": database,
157
- "user": user,
158
- "password": passwd,
159
- "host": host,
160
- "port": int_port,
161
- "socket": socket,
162
- "character_set": character_set,
163
- "local_infile": use_local_infile,
164
- "ssl": ssl_config,
165
- "init_command": init_command,
166
- "unbuffered": unbuffered,
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)