mycli 2.1.1__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.1.1 → mycli-2.2.0}/.github/workflows/ci.yml +4 -4
  2. {mycli-2.1.1 → mycli-2.2.0}/.github/workflows/publish.yml +4 -4
  3. {mycli-2.1.1 → mycli-2.2.0}/.github/workflows/typecheck.yml +1 -1
  4. {mycli-2.1.1 → mycli-2.2.0}/AGENTS.md +8 -4
  5. {mycli-2.1.1/mycli.egg-info → mycli-2.2.0}/PKG-INFO +1 -1
  6. {mycli-2.1.1 → mycli-2.2.0}/changelog.md +35 -0
  7. {mycli-2.1.1 → mycli-2.2.0}/mycli/TIPS +2 -0
  8. {mycli-2.1.1 → mycli-2.2.0}/mycli/cli_runner.py +75 -11
  9. {mycli-2.1.1 → mycli-2.2.0}/mycli/client_connection.py +29 -6
  10. {mycli-2.1.1 → mycli-2.2.0}/mycli/main.py +24 -0
  11. {mycli-2.1.1 → mycli-2.2.0}/mycli/myclirc +28 -7
  12. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/special/dsn_aliases.py +4 -0
  13. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/special/utils.py +36 -12
  14. {mycli-2.1.1 → mycli-2.2.0}/mycli/sqlexecute.py +4 -0
  15. {mycli-2.1.1 → mycli-2.2.0}/mycli/ssh_tunnel.py +74 -17
  16. mycli-2.2.0/mycli/vault.py +54 -0
  17. {mycli-2.1.1 → mycli-2.2.0/mycli.egg-info}/PKG-INFO +1 -1
  18. {mycli-2.1.1 → mycli-2.2.0}/mycli.egg-info/SOURCES.txt +3 -1
  19. {mycli-2.1.1 → mycli-2.2.0}/mycli.egg-info/scm_file_list.json +2 -0
  20. mycli-2.2.0/mycli.egg-info/scm_version.json +8 -0
  21. {mycli-2.1.1 → mycli-2.2.0}/test/myclirc +28 -7
  22. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_cli_runner.py +358 -2
  23. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_client_connection.py +119 -14
  24. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_main.py +28 -0
  25. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_special_iocommands.py +2 -2
  26. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_special_utils.py +31 -0
  27. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_sqlexecute.py +3 -0
  28. mycli-2.2.0/test/pytests/test_ssh_tunnel.py +512 -0
  29. mycli-2.2.0/test/pytests/test_vault.py +96 -0
  30. mycli-2.1.1/mycli.egg-info/scm_version.json +0 -8
  31. mycli-2.1.1/test/pytests/test_ssh_tunnel.py +0 -307
  32. {mycli-2.1.1 → mycli-2.2.0}/.git-blame-ignore-revs +0 -0
  33. {mycli-2.1.1 → mycli-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  34. {mycli-2.1.1 → mycli-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  35. {mycli-2.1.1 → mycli-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  36. {mycli-2.1.1 → mycli-2.2.0}/.github/dependabot.yml +0 -0
  37. {mycli-2.1.1 → mycli-2.2.0}/.github/workflows/lint.yml +0 -0
  38. {mycli-2.1.1 → mycli-2.2.0}/.gitignore +0 -0
  39. {mycli-2.1.1 → mycli-2.2.0}/AUTHORS.rst +0 -0
  40. {mycli-2.1.1 → mycli-2.2.0}/CONTRIBUTING.md +0 -0
  41. {mycli-2.1.1 → mycli-2.2.0}/LICENSE.txt +0 -0
  42. {mycli-2.1.1 → mycli-2.2.0}/MANIFEST.in +0 -0
  43. {mycli-2.1.1 → mycli-2.2.0}/README.md +0 -0
  44. {mycli-2.1.1 → mycli-2.2.0}/SPONSORS.rst +0 -0
  45. {mycli-2.1.1 → mycli-2.2.0}/doc/key_bindings.rst +0 -0
  46. {mycli-2.1.1 → mycli-2.2.0}/doc/llm.md +0 -0
  47. {mycli-2.1.1 → mycli-2.2.0}/doc/screenshots/main.gif +0 -0
  48. {mycli-2.1.1 → mycli-2.2.0}/doc/screenshots/tables.png +0 -0
  49. {mycli-2.1.1 → mycli-2.2.0}/mycli/AUTHORS +0 -0
  50. {mycli-2.1.1 → mycli-2.2.0}/mycli/SPONSORS +0 -0
  51. {mycli-2.1.1 → mycli-2.2.0}/mycli/__init__.py +0 -0
  52. {mycli-2.1.1 → mycli-2.2.0}/mycli/app_state.py +0 -0
  53. {mycli-2.1.1 → mycli-2.2.0}/mycli/clibuffer.py +0 -0
  54. {mycli-2.1.1 → mycli-2.2.0}/mycli/client.py +0 -0
  55. {mycli-2.1.1 → mycli-2.2.0}/mycli/client_commands.py +0 -0
  56. {mycli-2.1.1 → mycli-2.2.0}/mycli/client_query.py +0 -0
  57. {mycli-2.1.1 → mycli-2.2.0}/mycli/clistyle.py +0 -0
  58. {mycli-2.1.1 → mycli-2.2.0}/mycli/clitoolbar.py +0 -0
  59. {mycli-2.1.1 → mycli-2.2.0}/mycli/compat.py +0 -0
  60. {mycli-2.1.1 → mycli-2.2.0}/mycli/completion_refresher.py +0 -0
  61. {mycli-2.1.1 → mycli-2.2.0}/mycli/config.py +0 -0
  62. {mycli-2.1.1 → mycli-2.2.0}/mycli/constants.py +0 -0
  63. {mycli-2.1.1 → mycli-2.2.0}/mycli/key_bindings.py +0 -0
  64. {mycli-2.1.1 → mycli-2.2.0}/mycli/lexer.py +0 -0
  65. {mycli-2.1.1 → mycli-2.2.0}/mycli/main_modes/batch.py +0 -0
  66. {mycli-2.1.1 → mycli-2.2.0}/mycli/main_modes/checkup.py +0 -0
  67. {mycli-2.1.1 → mycli-2.2.0}/mycli/main_modes/execute.py +0 -0
  68. {mycli-2.1.1 → mycli-2.2.0}/mycli/main_modes/list_dsn.py +0 -0
  69. {mycli-2.1.1 → mycli-2.2.0}/mycli/main_modes/repl.py +0 -0
  70. {mycli-2.1.1 → mycli-2.2.0}/mycli/output.py +0 -0
  71. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/__init__.py +0 -0
  72. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/batch_utils.py +0 -0
  73. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/cli_utils.py +0 -0
  74. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/completion_engine.py +0 -0
  75. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/filepaths.py +0 -0
  76. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/hybrid_redirection.py +0 -0
  77. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/interactive_utils.py +0 -0
  78. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/key_binding_utils.py +0 -0
  79. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/ptoolkit/__init__.py +0 -0
  80. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/ptoolkit/fzf.py +0 -0
  81. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/ptoolkit/history.py +0 -0
  82. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/ptoolkit/utils.py +0 -0
  83. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/special/__init__.py +0 -0
  84. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/special/dbcommands.py +0 -0
  85. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/special/delimitercommand.py +0 -0
  86. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/special/favoritequeries.py +0 -0
  87. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/special/iocommands.py +0 -0
  88. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/special/llm.py +0 -0
  89. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/special/main.py +0 -0
  90. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/sql_utils.py +0 -0
  91. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/sqlresult.py +0 -0
  92. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/string_utils.py +0 -0
  93. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/tabular_output/__init__.py +0 -0
  94. {mycli-2.1.1 → mycli-2.2.0}/mycli/packages/tabular_output/sql_format.py +0 -0
  95. {mycli-2.1.1 → mycli-2.2.0}/mycli/schema_prefetcher.py +0 -0
  96. {mycli-2.1.1 → mycli-2.2.0}/mycli/sqlcompleter.py +0 -0
  97. {mycli-2.1.1 → mycli-2.2.0}/mycli/types.py +0 -0
  98. {mycli-2.1.1 → mycli-2.2.0}/mycli.egg-info/dependency_links.txt +0 -0
  99. {mycli-2.1.1 → mycli-2.2.0}/mycli.egg-info/entry_points.txt +0 -0
  100. {mycli-2.1.1 → mycli-2.2.0}/mycli.egg-info/requires.txt +0 -0
  101. {mycli-2.1.1 → mycli-2.2.0}/mycli.egg-info/top_level.txt +0 -0
  102. {mycli-2.1.1 → mycli-2.2.0}/pyproject.toml +0 -0
  103. {mycli-2.1.1 → mycli-2.2.0}/setup.cfg +0 -0
  104. {mycli-2.1.1 → mycli-2.2.0}/test/__init__.py +0 -0
  105. {mycli-2.1.1 → mycli-2.2.0}/test/features/__init__.py +0 -0
  106. {mycli-2.1.1 → mycli-2.2.0}/test/features/auto_vertical.feature +0 -0
  107. {mycli-2.1.1 → mycli-2.2.0}/test/features/basic_commands.feature +0 -0
  108. {mycli-2.1.1 → mycli-2.2.0}/test/features/connection.feature +0 -0
  109. {mycli-2.1.1 → mycli-2.2.0}/test/features/crud_database.feature +0 -0
  110. {mycli-2.1.1 → mycli-2.2.0}/test/features/crud_table.feature +0 -0
  111. {mycli-2.1.1 → mycli-2.2.0}/test/features/db_utils.py +0 -0
  112. {mycli-2.1.1 → mycli-2.2.0}/test/features/environment.py +0 -0
  113. {mycli-2.1.1 → mycli-2.2.0}/test/features/fixture_data/help_commands.txt +0 -0
  114. {mycli-2.1.1 → mycli-2.2.0}/test/features/fixture_utils.py +0 -0
  115. {mycli-2.1.1 → mycli-2.2.0}/test/features/iocommands.feature +0 -0
  116. {mycli-2.1.1 → mycli-2.2.0}/test/features/named_queries.feature +0 -0
  117. {mycli-2.1.1 → mycli-2.2.0}/test/features/specials.feature +0 -0
  118. {mycli-2.1.1 → mycli-2.2.0}/test/features/steps/__init__.py +0 -0
  119. {mycli-2.1.1 → mycli-2.2.0}/test/features/steps/auto_vertical.py +0 -0
  120. {mycli-2.1.1 → mycli-2.2.0}/test/features/steps/basic_commands.py +0 -0
  121. {mycli-2.1.1 → mycli-2.2.0}/test/features/steps/connection.py +0 -0
  122. {mycli-2.1.1 → mycli-2.2.0}/test/features/steps/crud_database.py +0 -0
  123. {mycli-2.1.1 → mycli-2.2.0}/test/features/steps/crud_table.py +0 -0
  124. {mycli-2.1.1 → mycli-2.2.0}/test/features/steps/iocommands.py +0 -0
  125. {mycli-2.1.1 → mycli-2.2.0}/test/features/steps/named_queries.py +0 -0
  126. {mycli-2.1.1 → mycli-2.2.0}/test/features/steps/specials.py +0 -0
  127. {mycli-2.1.1 → mycli-2.2.0}/test/features/steps/utils.py +0 -0
  128. {mycli-2.1.1 → mycli-2.2.0}/test/features/steps/wrappers.py +0 -0
  129. {mycli-2.1.1 → mycli-2.2.0}/test/features/wrappager.py +0 -0
  130. {mycli-2.1.1 → mycli-2.2.0}/test/mylogin.cnf +0 -0
  131. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/conftest.py +0 -0
  132. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_app_state.py +0 -0
  133. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_batch_utils.py +0 -0
  134. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_checkup.py +0 -0
  135. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_cli_utils.py +0 -0
  136. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_clibuffer.py +0 -0
  137. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_client.py +0 -0
  138. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_client_commands.py +0 -0
  139. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_client_query.py +0 -0
  140. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_clistyle.py +0 -0
  141. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_clitoolbar.py +0 -0
  142. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_completer_use_switch.py +0 -0
  143. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_completion_engine.py +0 -0
  144. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_completion_refresher.py +0 -0
  145. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_config.py +0 -0
  146. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_delimitercommand.py +0 -0
  147. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_dsn_aliases.py +0 -0
  148. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_favoritequeries.py +0 -0
  149. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_filepaths.py +0 -0
  150. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_hybrid_redirection.py +0 -0
  151. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_interactive_utils.py +0 -0
  152. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_key_binding_utils.py +0 -0
  153. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_key_bindings.py +0 -0
  154. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_lexer.py +0 -0
  155. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_main_modes_batch.py +0 -0
  156. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_main_modes_execute.py +0 -0
  157. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
  158. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_main_modes_repl.py +0 -0
  159. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_naive_completion.py +0 -0
  160. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_output.py +0 -0
  161. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
  162. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_ptoolkit_history.py +0 -0
  163. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_ptoolkit_utils.py +0 -0
  164. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_schema_prefetcher.py +0 -0
  165. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
  166. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_special_dbcommands.py +0 -0
  167. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_special_init.py +0 -0
  168. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_special_llm.py +0 -0
  169. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_special_main.py +0 -0
  170. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_sql_utils.py +0 -0
  171. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_sqlcompleter.py +0 -0
  172. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_sqlresult.py +0 -0
  173. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_string_utils.py +0 -0
  174. {mycli-2.1.1 → mycli-2.2.0}/test/pytests/test_tabular_output.py +0 -0
  175. {mycli-2.1.1 → 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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
222
+ - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
223
223
  with:
224
224
  version: "latest"
225
225
 
@@ -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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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.1.1
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,38 @@
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
+
1
36
  2.1.1 (2026/07/08)
2
37
  ==============
3
38
 
@@ -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
  ###
@@ -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
@@ -326,11 +349,51 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
326
349
  use_keyring = str_to_bool(cli_args.use_keyring)
327
350
  reset_keyring = False
328
351
 
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
391
+
329
392
  try:
330
393
  mycli.connect(
331
394
  database=database,
332
- user=cli_args.user,
333
- passwd=cli_args.password,
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,
334
397
  host=cli_args.host,
335
398
  port=cli_args.port,
336
399
  socket=cli_args.socket,
@@ -343,6 +406,7 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
343
406
  reset_keyring=reset_keyring,
344
407
  keepalive_ticks=keepalive_ticks,
345
408
  ssh_jump=cli_args.ssh_jump,
409
+ ssh_cli_options=cli_args.ssh_options,
346
410
  )
347
411
 
348
412
  if combined_init_cmd:
@@ -3,7 +3,7 @@ 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
7
  from urllib.parse import quote as urlquote
8
8
 
9
9
  import click
@@ -22,6 +22,7 @@ from mycli.constants import (
22
22
  ER_MUST_CHANGE_PASSWORD_LOGIN,
23
23
  )
24
24
  from mycli.packages.filepaths import guess_socket_location
25
+ from mycli.packages.special.utils import format_connection_dsn
25
26
  from mycli.sqlexecute import SQLExecute
26
27
  from mycli.ssh_tunnel import SshTunnel, SshTunnelError
27
28
 
@@ -61,6 +62,7 @@ class ClientConnectionMixin:
61
62
  reset_keyring: bool | None = None,
62
63
  keepalive_ticks: int | None = None,
63
64
  ssh_jump: str | None = None,
65
+ ssh_cli_options: str | None = None,
64
66
  ) -> None:
65
67
  mylogin_cnf: dict[str, Any] = self.read_mylogin_cnf(self.mylogin_cnf)
66
68
  # Fall back to .mylogin.cnf values only if user did not specify a value.
@@ -83,7 +85,8 @@ class ClientConnectionMixin:
83
85
  remote_port = int_port or DEFAULT_PORT
84
86
  remote_socket = socket or None
85
87
  ssh_executable = self.config.get('ssh', {}).get('ssh_executable', 'ssh') or 'ssh'
86
- ssh_options = self.config.get('ssh', {}).get('ssh_options')
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'
87
90
  try:
88
91
  self.ssh_tunnel = SshTunnel.from_target(
89
92
  ssh_jump,
@@ -91,7 +94,9 @@ class ClientConnectionMixin:
91
94
  remote_port=int(remote_port),
92
95
  remote_socket=remote_socket,
93
96
  ssh_executable=ssh_executable,
94
- ssh_options=ssh_options,
97
+ ssh_config_options=ssh_config_options,
98
+ ssh_cli_options=ssh_cli_options,
99
+ tunnel_method=tunnel_method,
95
100
  )
96
101
  self.ssh_tunnel.start()
97
102
  except (OSError, ValueError, SshTunnelError) as exc:
@@ -161,8 +166,9 @@ class ClientConnectionMixin:
161
166
  # 2. --password-file CLI option
162
167
  # 3. envvar (MYSQL_PWD)
163
168
  # 4. DSN (mysql://user:password)
164
- # 5. .mylogin.cnf
165
- # 6. keyring
169
+ # 5. Vault
170
+ # 6. .mylogin.cnf
171
+ # 7. keyring
166
172
 
167
173
  ssh_tunnel_field = urlquote(ssh_jump or '')
168
174
  if ssh_tunnel_field:
@@ -184,6 +190,18 @@ class ClientConnectionMixin:
184
190
  # should not fail, but will help the typechecker
185
191
  assert not isinstance(passwd, int)
186
192
 
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
+
187
205
  connection_info: dict[str, Any] = {
188
206
  'database': database,
189
207
  'user': user,
@@ -193,8 +211,13 @@ class ClientConnectionMixin:
193
211
  'ssl': ssl_config,
194
212
  'init_command': init_command,
195
213
  'unbuffered': unbuffered,
214
+ 'display_dsn': display_dsn,
196
215
  }
197
- if self.ssh_tunnel:
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:
198
221
  connection_info['host'] = self.ssh_tunnel.local_host
199
222
  connection_info['port'] = self.ssh_tunnel.local_port
200
223
  connection_info['socket'] = None
@@ -89,6 +89,26 @@ class CliArgs:
89
89
  type=click.Path(),
90
90
  help='File or FIFO path containing the password to connect to the db if not specified otherwise.',
91
91
  )
92
+ vault_address: str | None = clickdc.option(
93
+ type=str,
94
+ help='EXPERIMENTAL "vault kv get" integration: value for $VAULT_ADDR if unset in the environment or ~/.myclirc.',
95
+ )
96
+ vault_mount: str | None = clickdc.option(
97
+ type=str,
98
+ help='EXPERIMENTAL "vault kv get" integration: value for -mount if unset in ~/.myclirc.',
99
+ )
100
+ vault_secret: str | None = clickdc.option(
101
+ type=str,
102
+ help='EXPERIMENTAL "vault kv get" integration: secret name.',
103
+ )
104
+ vault_password_field: str | None = clickdc.option(
105
+ type=str,
106
+ help='EXPERIMENTAL "vault kv get" integration: field containing the password.',
107
+ )
108
+ vault_username_field: str | None = clickdc.option(
109
+ type=str,
110
+ help='EXPERIMENTAL "vault kv get" integration: field containing the username.',
111
+ )
92
112
  ssl_mode: str = clickdc.option(
93
113
  type=click.Choice(['auto', 'on', 'off']),
94
114
  help='Set desired SSL behavior. auto=preferred if TCP/IP, on=required, off=off.',
@@ -275,6 +295,10 @@ class CliArgs:
275
295
  type=str,
276
296
  help='Open an SSH tunnel via [user@]host[:port] and connect to MySQL through it.',
277
297
  )
298
+ ssh_options: str | None = clickdc.option(
299
+ type=str,
300
+ help='Extra CLI arguments for SSH with --ssh-jump, placed after options from myclirc.',
301
+ )
278
302
  checkup: bool = clickdc.option(
279
303
  is_flag=True,
280
304
  help='Run a checkup on your configuration.',
@@ -305,6 +305,34 @@ vi_ttimeoutlen = 0.1
305
305
  # How long to wait for an Escape key sequence in Emacs mode.
306
306
  emacs_ttimeoutlen = 0.5
307
307
 
308
+ [ssh]
309
+ # Path to the ssh executable used by --ssh-jump.
310
+ ssh_executable = ssh
311
+
312
+ # Options to pass to the ssh executable when making a tunnel.
313
+ # The defaults may need to be changed if not using OpenSSH.
314
+ ssh_options = -a -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -o IPQoS=af13 -o LogLevel=FATAL
315
+
316
+ # Method for establishing the local side of the SSH tunnel: auto, port, socket.
317
+ # auto means: port if on Windows, socket otherwise.
318
+ tunnel_method = auto
319
+
320
+ [vault_beta]
321
+ # Path to the vault executable used for Vault integration.
322
+ vault_executable = vault
323
+
324
+ # URL for VAULT_ADDR, if the environment variable is unset.
325
+ address =
326
+
327
+ # Default mount, which can be overridden by --vault-mount at the CLI.
328
+ default_mount =
329
+
330
+ # Field/property containing the password, if --vault-password-field is not provided.
331
+ default_password_field = password
332
+
333
+ # Field/property containing the username, if --vault-username-field is not provided.
334
+ default_username_field = username
335
+
308
336
  # Custom colors for the completion menu, toolbar, etc, with actual support
309
337
  # depending on the terminal, and the property being set.
310
338
  # Colors: #ffffff, bg:#ffffff, border:#ffffff.
@@ -407,10 +435,3 @@ matching-bracket.other = '#000000 bg:#aacccc'
407
435
  [alias_dsn.init-commands]
408
436
  # Define one or more SQL statements per alias (semicolon-separated).
409
437
  # example_dsn = "SET sql_select_limit=1000; SET time_zone='+00:00'"
410
-
411
- [ssh]
412
- # Path to the ssh executable used by --ssh-jump.
413
- ssh_executable = ssh
414
-
415
- # options to pass to the ssh executable when making a tunnel
416
- ssh_options = -a -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -o IPQoS=af13 -o LogLevel=FATAL
@@ -11,6 +11,10 @@ directly or by using this command.
11
11
 
12
12
  Examples:
13
13
 
14
+ # Show a DSN for the current connection
15
+ mysql> /dsn show
16
+ mysql://mycli@localhost/mysql
17
+
14
18
  # List all DSN aliases
15
19
  mysql> /dsn list
16
20
  ┌───────┬───────────────────────────────┐
@@ -147,21 +147,45 @@ def get_local_timezone() -> str:
147
147
  return datetime.datetime.now().astimezone().tzname() or ''
148
148
 
149
149
 
150
- def compute_current_dsn(cur: Cursor) -> str:
151
- conn = cur.connection
152
- user = urlquote(conn.user or '')
153
- host = conn.host or 'localhost'
154
- port = f':{conn.port}'
155
- db = urlquote(conn.db.decode() if isinstance(conn.db, bytes) else (conn.db or ''))
150
+ def format_connection_dsn(
151
+ *,
152
+ user: str | None,
153
+ host: str | None,
154
+ port: int | None,
155
+ database: str | None,
156
+ socket: str | None,
157
+ character_set: str | None,
158
+ ssh_jump: str | None = None,
159
+ ) -> str:
160
+ user = urlquote(user or '')
161
+ host = host or 'localhost'
162
+ port_part = f':{port}' if port is not None else ''
163
+ db = urlquote(database or '')
156
164
  if db:
157
165
  db = f'/{db}'
158
166
  query_part = {}
159
- if getattr(conn, 'unix_socket', None):
160
- query_part['socket'] = conn.unix_socket
161
- port = ''
162
- if getattr(conn, 'charset', None) and conn.charset != 'utf8mb4':
163
- query_part['character_set'] = conn.charset
164
- dsn = f'mysql://{user}@{host}{port}{db}'
167
+ if socket:
168
+ query_part['socket'] = socket
169
+ port_part = ''
170
+ if character_set and character_set != 'utf8mb4':
171
+ query_part['character_set'] = character_set
172
+ if ssh_jump:
173
+ query_part['ssh_jump'] = ssh_jump
174
+ dsn = f'mysql://{user}@{host}{port_part}{db}'
165
175
  if query_part:
166
176
  dsn += '?' + urlencode(query_part)
167
177
  return dsn
178
+
179
+
180
+ def compute_current_dsn(cur: Cursor) -> str:
181
+ conn = cur.connection
182
+ if display_dsn := getattr(conn, '_mycli_display_dsn', None):
183
+ return display_dsn
184
+ return format_connection_dsn(
185
+ user=conn.user.decode() if isinstance(conn.user, bytes) else conn.user,
186
+ host=conn.host,
187
+ port=conn.port,
188
+ database=conn.db.decode() if isinstance(conn.db, bytes) else conn.db,
189
+ socket=getattr(conn, 'unix_socket', None),
190
+ character_set=getattr(conn, 'charset', None),
191
+ )
@@ -167,6 +167,7 @@ class SQLExecute:
167
167
  ssl: dict[str, Any] | None,
168
168
  init_command: str | None = None,
169
169
  unbuffered: bool | None = None,
170
+ display_dsn: str | None = None,
170
171
  ) -> None:
171
172
  self.dbname = database
172
173
  self.user = user
@@ -177,6 +178,7 @@ class SQLExecute:
177
178
  self.character_set = character_set
178
179
  self.local_infile = local_infile
179
180
  self.ssl = ssl
181
+ self.display_dsn = display_dsn
180
182
  self.server_info: ServerInfo | None = None
181
183
  self.connection_id: int | None = None
182
184
  self.init_command = init_command
@@ -295,6 +297,8 @@ class SQLExecute:
295
297
  except pymysql.err.Error:
296
298
  pass
297
299
  self.conn = conn
300
+ if self.display_dsn is not None:
301
+ self.conn._mycli_display_dsn = self.display_dsn # type: ignore[attr-defined]
298
302
  # Update them after the connection is made to ensure that it was a
299
303
  # successful connection.
300
304
  self.dbname = db