mycli 2.0.0__tar.gz → 2.1.1__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 (173) hide show
  1. {mycli-2.0.0 → mycli-2.1.1}/.github/workflows/ci.yml +4 -4
  2. {mycli-2.0.0 → mycli-2.1.1}/.github/workflows/lint.yml +2 -2
  3. {mycli-2.0.0 → mycli-2.1.1}/.github/workflows/publish.yml +4 -4
  4. {mycli-2.0.0 → mycli-2.1.1}/.github/workflows/typecheck.yml +1 -1
  5. {mycli-2.0.0/mycli.egg-info → mycli-2.1.1}/PKG-INFO +1 -1
  6. {mycli-2.0.0 → mycli-2.1.1}/changelog.md +21 -0
  7. {mycli-2.0.0 → mycli-2.1.1}/mycli/TIPS +3 -1
  8. {mycli-2.0.0 → mycli-2.1.1}/mycli/cli_runner.py +57 -39
  9. {mycli-2.0.0 → mycli-2.1.1}/mycli/client.py +16 -2
  10. {mycli-2.0.0 → mycli-2.1.1}/mycli/client_connection.py +52 -13
  11. {mycli-2.0.0 → mycli-2.1.1}/mycli/main.py +15 -11
  12. {mycli-2.0.0 → mycli-2.1.1}/mycli/myclirc +13 -2
  13. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/cli_utils.py +2 -6
  14. mycli-2.1.1/mycli/packages/special/dsn_aliases.py +60 -0
  15. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/special/favoritequeries.py +4 -4
  16. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/special/iocommands.py +47 -1
  17. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/special/llm.py +6 -6
  18. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/special/utils.py +22 -0
  19. mycli-2.1.1/mycli/ssh_tunnel.py +155 -0
  20. {mycli-2.0.0 → mycli-2.1.1/mycli.egg-info}/PKG-INFO +1 -1
  21. {mycli-2.0.0 → mycli-2.1.1}/mycli.egg-info/SOURCES.txt +4 -0
  22. {mycli-2.0.0 → mycli-2.1.1}/mycli.egg-info/scm_file_list.json +4 -0
  23. mycli-2.1.1/mycli.egg-info/scm_version.json +8 -0
  24. mycli-2.1.1/test/features/fixture_data/help_commands.txt +39 -0
  25. {mycli-2.0.0 → mycli-2.1.1}/test/myclirc +13 -2
  26. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_cli_runner.py +56 -0
  27. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_cli_utils.py +2 -2
  28. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_client.py +17 -1
  29. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_client_connection.py +136 -0
  30. mycli-2.1.1/test/pytests/test_dsn_aliases.py +102 -0
  31. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_main.py +33 -0
  32. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_smart_completion_public_schema_only.py +4 -1
  33. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_special_iocommands.py +104 -0
  34. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_special_utils.py +44 -0
  35. mycli-2.1.1/test/pytests/test_ssh_tunnel.py +307 -0
  36. mycli-2.0.0/mycli.egg-info/scm_version.json +0 -8
  37. mycli-2.0.0/test/features/fixture_data/help_commands.txt +0 -38
  38. {mycli-2.0.0 → mycli-2.1.1}/.git-blame-ignore-revs +0 -0
  39. {mycli-2.0.0 → mycli-2.1.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  40. {mycli-2.0.0 → mycli-2.1.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  41. {mycli-2.0.0 → mycli-2.1.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  42. {mycli-2.0.0 → mycli-2.1.1}/.github/dependabot.yml +0 -0
  43. {mycli-2.0.0 → mycli-2.1.1}/.gitignore +0 -0
  44. {mycli-2.0.0 → mycli-2.1.1}/AGENTS.md +0 -0
  45. {mycli-2.0.0 → mycli-2.1.1}/AUTHORS.rst +0 -0
  46. {mycli-2.0.0 → mycli-2.1.1}/CONTRIBUTING.md +0 -0
  47. {mycli-2.0.0 → mycli-2.1.1}/LICENSE.txt +0 -0
  48. {mycli-2.0.0 → mycli-2.1.1}/MANIFEST.in +0 -0
  49. {mycli-2.0.0 → mycli-2.1.1}/README.md +0 -0
  50. {mycli-2.0.0 → mycli-2.1.1}/SPONSORS.rst +0 -0
  51. {mycli-2.0.0 → mycli-2.1.1}/doc/key_bindings.rst +0 -0
  52. {mycli-2.0.0 → mycli-2.1.1}/doc/llm.md +0 -0
  53. {mycli-2.0.0 → mycli-2.1.1}/doc/screenshots/main.gif +0 -0
  54. {mycli-2.0.0 → mycli-2.1.1}/doc/screenshots/tables.png +0 -0
  55. {mycli-2.0.0 → mycli-2.1.1}/mycli/AUTHORS +0 -0
  56. {mycli-2.0.0 → mycli-2.1.1}/mycli/SPONSORS +0 -0
  57. {mycli-2.0.0 → mycli-2.1.1}/mycli/__init__.py +0 -0
  58. {mycli-2.0.0 → mycli-2.1.1}/mycli/app_state.py +0 -0
  59. {mycli-2.0.0 → mycli-2.1.1}/mycli/clibuffer.py +0 -0
  60. {mycli-2.0.0 → mycli-2.1.1}/mycli/client_commands.py +0 -0
  61. {mycli-2.0.0 → mycli-2.1.1}/mycli/client_query.py +0 -0
  62. {mycli-2.0.0 → mycli-2.1.1}/mycli/clistyle.py +0 -0
  63. {mycli-2.0.0 → mycli-2.1.1}/mycli/clitoolbar.py +0 -0
  64. {mycli-2.0.0 → mycli-2.1.1}/mycli/compat.py +0 -0
  65. {mycli-2.0.0 → mycli-2.1.1}/mycli/completion_refresher.py +0 -0
  66. {mycli-2.0.0 → mycli-2.1.1}/mycli/config.py +0 -0
  67. {mycli-2.0.0 → mycli-2.1.1}/mycli/constants.py +0 -0
  68. {mycli-2.0.0 → mycli-2.1.1}/mycli/key_bindings.py +0 -0
  69. {mycli-2.0.0 → mycli-2.1.1}/mycli/lexer.py +0 -0
  70. {mycli-2.0.0 → mycli-2.1.1}/mycli/main_modes/batch.py +0 -0
  71. {mycli-2.0.0 → mycli-2.1.1}/mycli/main_modes/checkup.py +0 -0
  72. {mycli-2.0.0 → mycli-2.1.1}/mycli/main_modes/execute.py +0 -0
  73. {mycli-2.0.0 → mycli-2.1.1}/mycli/main_modes/list_dsn.py +0 -0
  74. {mycli-2.0.0 → mycli-2.1.1}/mycli/main_modes/repl.py +0 -0
  75. {mycli-2.0.0 → mycli-2.1.1}/mycli/output.py +0 -0
  76. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/__init__.py +0 -0
  77. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/batch_utils.py +0 -0
  78. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/completion_engine.py +0 -0
  79. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/filepaths.py +0 -0
  80. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/hybrid_redirection.py +0 -0
  81. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/interactive_utils.py +0 -0
  82. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/key_binding_utils.py +0 -0
  83. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/ptoolkit/__init__.py +0 -0
  84. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/ptoolkit/fzf.py +0 -0
  85. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/ptoolkit/history.py +0 -0
  86. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/ptoolkit/utils.py +0 -0
  87. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/special/__init__.py +0 -0
  88. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/special/dbcommands.py +0 -0
  89. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/special/delimitercommand.py +0 -0
  90. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/special/main.py +0 -0
  91. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/sql_utils.py +0 -0
  92. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/sqlresult.py +0 -0
  93. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/string_utils.py +0 -0
  94. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/tabular_output/__init__.py +0 -0
  95. {mycli-2.0.0 → mycli-2.1.1}/mycli/packages/tabular_output/sql_format.py +0 -0
  96. {mycli-2.0.0 → mycli-2.1.1}/mycli/schema_prefetcher.py +0 -0
  97. {mycli-2.0.0 → mycli-2.1.1}/mycli/sqlcompleter.py +0 -0
  98. {mycli-2.0.0 → mycli-2.1.1}/mycli/sqlexecute.py +0 -0
  99. {mycli-2.0.0 → mycli-2.1.1}/mycli/types.py +0 -0
  100. {mycli-2.0.0 → mycli-2.1.1}/mycli.egg-info/dependency_links.txt +0 -0
  101. {mycli-2.0.0 → mycli-2.1.1}/mycli.egg-info/entry_points.txt +0 -0
  102. {mycli-2.0.0 → mycli-2.1.1}/mycli.egg-info/requires.txt +0 -0
  103. {mycli-2.0.0 → mycli-2.1.1}/mycli.egg-info/top_level.txt +0 -0
  104. {mycli-2.0.0 → mycli-2.1.1}/pyproject.toml +0 -0
  105. {mycli-2.0.0 → mycli-2.1.1}/setup.cfg +0 -0
  106. {mycli-2.0.0 → mycli-2.1.1}/test/__init__.py +0 -0
  107. {mycli-2.0.0 → mycli-2.1.1}/test/features/__init__.py +0 -0
  108. {mycli-2.0.0 → mycli-2.1.1}/test/features/auto_vertical.feature +0 -0
  109. {mycli-2.0.0 → mycli-2.1.1}/test/features/basic_commands.feature +0 -0
  110. {mycli-2.0.0 → mycli-2.1.1}/test/features/connection.feature +0 -0
  111. {mycli-2.0.0 → mycli-2.1.1}/test/features/crud_database.feature +0 -0
  112. {mycli-2.0.0 → mycli-2.1.1}/test/features/crud_table.feature +0 -0
  113. {mycli-2.0.0 → mycli-2.1.1}/test/features/db_utils.py +0 -0
  114. {mycli-2.0.0 → mycli-2.1.1}/test/features/environment.py +0 -0
  115. {mycli-2.0.0 → mycli-2.1.1}/test/features/fixture_utils.py +0 -0
  116. {mycli-2.0.0 → mycli-2.1.1}/test/features/iocommands.feature +0 -0
  117. {mycli-2.0.0 → mycli-2.1.1}/test/features/named_queries.feature +0 -0
  118. {mycli-2.0.0 → mycli-2.1.1}/test/features/specials.feature +0 -0
  119. {mycli-2.0.0 → mycli-2.1.1}/test/features/steps/__init__.py +0 -0
  120. {mycli-2.0.0 → mycli-2.1.1}/test/features/steps/auto_vertical.py +0 -0
  121. {mycli-2.0.0 → mycli-2.1.1}/test/features/steps/basic_commands.py +0 -0
  122. {mycli-2.0.0 → mycli-2.1.1}/test/features/steps/connection.py +0 -0
  123. {mycli-2.0.0 → mycli-2.1.1}/test/features/steps/crud_database.py +0 -0
  124. {mycli-2.0.0 → mycli-2.1.1}/test/features/steps/crud_table.py +0 -0
  125. {mycli-2.0.0 → mycli-2.1.1}/test/features/steps/iocommands.py +0 -0
  126. {mycli-2.0.0 → mycli-2.1.1}/test/features/steps/named_queries.py +0 -0
  127. {mycli-2.0.0 → mycli-2.1.1}/test/features/steps/specials.py +0 -0
  128. {mycli-2.0.0 → mycli-2.1.1}/test/features/steps/utils.py +0 -0
  129. {mycli-2.0.0 → mycli-2.1.1}/test/features/steps/wrappers.py +0 -0
  130. {mycli-2.0.0 → mycli-2.1.1}/test/features/wrappager.py +0 -0
  131. {mycli-2.0.0 → mycli-2.1.1}/test/mylogin.cnf +0 -0
  132. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/conftest.py +0 -0
  133. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_app_state.py +0 -0
  134. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_batch_utils.py +0 -0
  135. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_checkup.py +0 -0
  136. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_clibuffer.py +0 -0
  137. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_client_commands.py +0 -0
  138. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_client_query.py +0 -0
  139. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_clistyle.py +0 -0
  140. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_clitoolbar.py +0 -0
  141. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_completer_use_switch.py +0 -0
  142. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_completion_engine.py +0 -0
  143. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_completion_refresher.py +0 -0
  144. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_config.py +0 -0
  145. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_delimitercommand.py +0 -0
  146. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_favoritequeries.py +0 -0
  147. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_filepaths.py +0 -0
  148. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_hybrid_redirection.py +0 -0
  149. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_interactive_utils.py +0 -0
  150. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_key_binding_utils.py +0 -0
  151. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_key_bindings.py +0 -0
  152. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_lexer.py +0 -0
  153. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_main_modes_batch.py +0 -0
  154. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_main_modes_execute.py +0 -0
  155. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_main_modes_list_dsn.py +0 -0
  156. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_main_modes_repl.py +0 -0
  157. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_naive_completion.py +0 -0
  158. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_output.py +0 -0
  159. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_ptoolkit_fzf.py +0 -0
  160. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_ptoolkit_history.py +0 -0
  161. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_ptoolkit_utils.py +0 -0
  162. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_schema_prefetcher.py +0 -0
  163. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_special_dbcommands.py +0 -0
  164. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_special_init.py +0 -0
  165. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_special_llm.py +0 -0
  166. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_special_main.py +0 -0
  167. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_sql_utils.py +0 -0
  168. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_sqlcompleter.py +0 -0
  169. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_sqlexecute.py +0 -0
  170. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_sqlresult.py +0 -0
  171. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_string_utils.py +0 -0
  172. {mycli-2.0.0 → mycli-2.1.1}/test/pytests/test_tabular_output.py +0 -0
  173. {mycli-2.0.0 → mycli-2.1.1}/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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
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@f98e06938123ccabd21905ea5d0069192241f9f1 # v8.3.1
29
29
  with:
30
30
  version: 'latest'
31
31
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.0.0
3
+ Version: 2.1.1
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,24 @@
1
+ 2.1.1 (2026/07/08)
2
+ ==============
3
+
4
+ Breaking Changes
5
+ ---------
6
+ * Remove undocumented support for `tcp://` and `socket://` DSNs.
7
+
8
+
9
+ Features
10
+ ---------
11
+ * Add `--ssh-jump` CLI argument, restoring SSH jump functionality.
12
+ * Add a `/dsn` command to manage persisted DSN aliases.
13
+ * Add `auto` option to ignore keyring when inside an SSH session.
14
+
15
+
16
+ Documentation
17
+ ---------
18
+ * Fix a typo in myclirc commentary.
19
+ * Advertise forward-slash command forms in more places.
20
+
21
+
1
22
  2.0.0 (2026/07/03)
2
23
  ==============
3
24
 
@@ -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
 
@@ -108,6 +108,8 @@ the /watch command executes a query every N seconds!
108
108
 
109
109
  use /bug to file a bug on GitHub!
110
110
 
111
+ use /dsn to manage saved DSNs!
112
+
111
113
  ###
112
114
  ### environment variables
113
115
  ###
@@ -304,52 +304,70 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
304
304
  if cli_args.use_keyring is not None and cli_args.use_keyring.lower() == 'reset':
305
305
  use_keyring = True
306
306
  reset_keyring = True
307
+ elif cli_args.use_keyring is not None and cli_args.use_keyring.lower() == 'auto':
308
+ if os.environ.get('SSH_CONNECTION'):
309
+ use_keyring = False
310
+ reset_keyring = False
311
+ else:
312
+ use_keyring = True
313
+ reset_keyring = False
307
314
  elif cli_args.use_keyring is None:
308
- use_keyring = str_to_bool(mycli.config['main'].get('use_keyring', 'False'))
309
- reset_keyring = False
315
+ if mycli.config['main'].get('use_keyring', 'False').lower() == 'auto':
316
+ if os.environ.get('SSH_CONNECTION'):
317
+ use_keyring = False
318
+ reset_keyring = False
319
+ else:
320
+ use_keyring = True
321
+ reset_keyring = False
322
+ else:
323
+ use_keyring = str_to_bool(mycli.config['main'].get('use_keyring', 'False'))
324
+ reset_keyring = False
310
325
  else:
311
326
  use_keyring = str_to_bool(cli_args.use_keyring)
312
327
  reset_keyring = False
313
328
 
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)
333
-
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
- )
329
+ try:
330
+ mycli.connect(
331
+ database=database,
332
+ user=cli_args.user,
333
+ passwd=cli_args.password,
334
+ host=cli_args.host,
335
+ port=cli_args.port,
336
+ socket=cli_args.socket,
337
+ local_infile=cli_args.local_infile,
338
+ ssl=ssl,
339
+ init_command=combined_init_cmd,
340
+ unbuffered=cli_args.unbuffered,
341
+ character_set=cli_args.character_set,
342
+ use_keyring=use_keyring,
343
+ reset_keyring=reset_keyring,
344
+ keepalive_ticks=keepalive_ticks,
345
+ ssh_jump=cli_args.ssh_jump,
346
+ )
347
+
348
+ if combined_init_cmd:
349
+ click.echo(f"Executing init-command: {combined_init_cmd}", err=True)
350
+
351
+ mycli.logger.debug(
352
+ "Launch Params: \n\tdatabase: %r\tuser: %r\thost: %r\tport: %r",
353
+ database,
354
+ cli_args.user,
355
+ cli_args.host,
356
+ cli_args.port,
357
+ )
341
358
 
342
- if cli_args.execute is not None:
343
- sys.exit(main_execute_from_cli(mycli, cli_args))
359
+ if cli_args.execute is not None:
360
+ sys.exit(main_execute_from_cli(mycli, cli_args))
344
361
 
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))
362
+ if cli_args.batch is not None and cli_args.batch != '-' and cli_args.progress and sys.stderr.isatty():
363
+ sys.exit(main_batch_with_progress_bar(mycli, cli_args))
347
364
 
348
- if cli_args.batch is not None:
349
- sys.exit(main_batch_without_progress_bar(mycli, cli_args))
365
+ if cli_args.batch is not None:
366
+ sys.exit(main_batch_without_progress_bar(mycli, cli_args))
350
367
 
351
- if not sys.stdin.isatty():
352
- sys.exit(main_batch_from_stdin(mycli, cli_args))
368
+ if not sys.stdin.isatty():
369
+ sys.exit(main_batch_from_stdin(mycli, cli_args))
353
370
 
354
- mycli.run_cli()
355
- mycli.close()
371
+ mycli.run_cli()
372
+ finally:
373
+ 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)
@@ -4,6 +4,7 @@ import os
4
4
  import sys
5
5
  import traceback
6
6
  from typing import TYPE_CHECKING, Any
7
+ from urllib.parse import quote as urlquote
7
8
 
8
9
  import click
9
10
  import keyring
@@ -22,6 +23,7 @@ from mycli.constants import (
22
23
  )
23
24
  from mycli.packages.filepaths import guess_socket_location
24
25
  from mycli.sqlexecute import SQLExecute
26
+ from mycli.ssh_tunnel import SshTunnel, SshTunnelError
25
27
 
26
28
  try:
27
29
  from pwd import getpwuid
@@ -58,6 +60,7 @@ class ClientConnectionMixin:
58
60
  use_keyring: bool | None = None,
59
61
  reset_keyring: bool | None = None,
60
62
  keepalive_ticks: int | None = None,
63
+ ssh_jump: str | None = None,
61
64
  ) -> None:
62
65
  mylogin_cnf: dict[str, Any] = self.read_mylogin_cnf(self.mylogin_cnf)
63
66
  # Fall back to .mylogin.cnf values only if user did not specify a value.
@@ -67,6 +70,7 @@ class ClientConnectionMixin:
67
70
  ssl_config: dict[str, Any] = ssl or {}
68
71
  user_connection_config = self.config_without_package_defaults.get('connection', {})
69
72
  self.keepalive_ticks = keepalive_ticks
73
+ self.ssh_tunnel = None
70
74
 
71
75
  int_port = port and int(port)
72
76
  if not int_port:
@@ -74,6 +78,31 @@ class ClientConnectionMixin:
74
78
  if not host or host == DEFAULT_HOST:
75
79
  socket = socket or user_connection_config.get("default_socket") or mylogin_cnf["socket"] or guess_socket_location()
76
80
 
81
+ if ssh_jump:
82
+ remote_host = host or DEFAULT_HOST
83
+ remote_port = int_port or DEFAULT_PORT
84
+ remote_socket = socket or None
85
+ ssh_executable = self.config.get('ssh', {}).get('ssh_executable', 'ssh') or 'ssh'
86
+ ssh_options = self.config.get('ssh', {}).get('ssh_options')
87
+ try:
88
+ self.ssh_tunnel = SshTunnel.from_target(
89
+ ssh_jump,
90
+ remote_host=remote_host,
91
+ remote_port=int(remote_port),
92
+ remote_socket=remote_socket,
93
+ ssh_executable=ssh_executable,
94
+ ssh_options=ssh_options,
95
+ )
96
+ self.ssh_tunnel.start()
97
+ except (OSError, ValueError, SshTunnelError) as exc:
98
+ click.secho(f'Error: Unable to start SSH tunnel: {exc}', err=True, fg='red')
99
+ try:
100
+ if self.ssh_tunnel:
101
+ self.ssh_tunnel.close()
102
+ except Exception:
103
+ pass
104
+ sys.exit(1)
105
+
77
106
  passwd = passwd if isinstance(passwd, (str, int)) else mylogin_cnf["password"]
78
107
 
79
108
  if not character_set:
@@ -135,7 +164,10 @@ class ClientConnectionMixin:
135
164
  # 5. .mylogin.cnf
136
165
  # 6. keyring
137
166
 
138
- keyring_identifier = f'{user}@{host}:{"" if socket else int_port}:{socket or ""}'
167
+ ssh_tunnel_field = urlquote(ssh_jump or '')
168
+ if ssh_tunnel_field:
169
+ ssh_tunnel_field = ':' + ssh_tunnel_field
170
+ keyring_identifier = f'{user}@{host}:{"" if socket else int_port}:{socket or ""}{ssh_tunnel_field}'
139
171
  keyring_domain = 'mycli.net'
140
172
  keyring_retrieved_cleanly = False
141
173
 
@@ -152,19 +184,24 @@ class ClientConnectionMixin:
152
184
  # should not fail, but will help the typechecker
153
185
  assert not isinstance(passwd, int)
154
186
 
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,
187
+ connection_info: dict[str, Any] = {
188
+ 'database': database,
189
+ 'user': user,
190
+ 'password': passwd,
191
+ 'character_set': character_set,
192
+ 'local_infile': use_local_infile,
193
+ 'ssl': ssl_config,
194
+ 'init_command': init_command,
195
+ 'unbuffered': unbuffered,
167
196
  }
197
+ if self.ssh_tunnel:
198
+ connection_info['host'] = self.ssh_tunnel.local_host
199
+ connection_info['port'] = self.ssh_tunnel.local_port
200
+ connection_info['socket'] = None
201
+ else:
202
+ connection_info['host'] = host
203
+ connection_info['port'] = int_port
204
+ connection_info['socket'] = socket
168
205
 
169
206
  def _update_keyring(password: str | None, keyring_retrieved_cleanly: bool):
170
207
  if not password:
@@ -239,6 +276,8 @@ class ClientConnectionMixin:
239
276
  socket_owner = getpwuid(os.stat(socket).st_uid).pw_name
240
277
  except KeyError:
241
278
  socket_owner = '<unknown>'
279
+ except FileNotFoundError:
280
+ socket_owner = '<unknown>'
242
281
  self.echo(f"Connecting to socket {socket}, owned by user {socket_owner}", err=True)
243
282
  try:
244
283
  _connect(keyring_retrieved_cleanly=keyring_retrieved_cleanly)
@@ -263,14 +263,18 @@ class CliArgs:
263
263
  help='Show progress on the standard error with --batch.',
264
264
  )
265
265
  use_keyring: str | None = clickdc.option(
266
- type=click.Choice(['true', 'false', 'reset']),
266
+ type=click.Choice(['auto', 'true', 'false', 'reset']),
267
267
  default=None,
268
- help='Store and retrieve passwords from the system keyring: true/false/reset.',
268
+ help='Store and retrieve passwords from the system keyring. auto means true, unless within an SSH connection.',
269
269
  )
270
270
  keepalive_ticks: int | None = clickdc.option(
271
271
  type=int,
272
272
  help='Send regular keepalive pings to the connection, roughly every <int> seconds.',
273
273
  )
274
+ ssh_jump: str | None = clickdc.option(
275
+ type=str,
276
+ help='Open an SSH tunnel via [user@]host[:port] and connect to MySQL through it.',
277
+ )
274
278
  checkup: bool = clickdc.option(
275
279
  is_flag=True,
276
280
  help='Run a checkup on your configuration.',
@@ -287,47 +291,47 @@ class CliArgs:
287
291
  ssh_user: str | None = clickdc.option(
288
292
  type=str,
289
293
  hidden=True,
290
- deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
294
+ deprecated='No effect. See --ssh-jump.',
291
295
  )
292
296
  ssh_host: str | None = clickdc.option(
293
297
  type=str,
294
298
  hidden=True,
295
- deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
299
+ deprecated='No effect. See --ssh-jump.',
296
300
  )
297
301
  ssh_port: int = clickdc.option(
298
302
  type=int,
299
303
  hidden=True,
300
- deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
304
+ deprecated='No effect. See --ssh-jump.',
301
305
  )
302
306
  ssh_password: str | None = clickdc.option(
303
307
  type=str,
304
308
  hidden=True,
305
- deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
309
+ deprecated='No effect. See --ssh-jump.',
306
310
  )
307
311
  ssh_key_filename: str | None = clickdc.option(
308
312
  type=str,
309
313
  hidden=True,
310
- deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
314
+ deprecated='No effect. See --ssh-jump.',
311
315
  )
312
316
  ssh_config_path: str = clickdc.option(
313
317
  type=str,
314
318
  hidden=True,
315
- deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
319
+ deprecated='No effect. See --ssh-jump.',
316
320
  )
317
321
  ssh_config_host: str | None = clickdc.option(
318
322
  type=str,
319
323
  hidden=True,
320
- deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
324
+ deprecated='No effect. See --ssh-jump.',
321
325
  )
322
326
  list_ssh_config: bool = clickdc.option(
323
327
  is_flag=True,
324
328
  hidden=True,
325
- deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
329
+ deprecated='No effect. See --ssh-jump.',
326
330
  )
327
331
  ssh_warning_off: bool = clickdc.option(
328
332
  is_flag=True,
329
333
  hidden=True,
330
- deprecated='No effect. See https://github.com/dbcli/mycli/issues/1960 .',
334
+ deprecated='No effect. See --ssh-jump.',
331
335
  )
332
336
 
333
337
 
@@ -5,7 +5,7 @@
5
5
  # after executing a SQL statement when applicable.
6
6
  show_warnings = False
7
7
 
8
- # Enables context sensitive auto-completion. If this is disabled the all
8
+ # Enables context sensitive auto-completion. If this is disabled then all
9
9
  # possible completions will be listed.
10
10
  smart_completion = True
11
11
 
@@ -206,10 +206,14 @@ enable_pager = True
206
206
  pager = 'less'
207
207
 
208
208
  # Whether to store and retrieve passwords from the system keyring.
209
+ # * False - never use keyring
210
+ # * True - always use keyring
211
+ # * auto - use keyring unless currently in an SSH connection
209
212
  # See the documentation for https://pypi.org/project/keyring/ for your OS.
210
213
  # Note that the hostname is considered to be different if short or qualified.
211
214
  # This can be overridden with --use-keyring= at the CLI.
212
215
  # A password can be reset with --use-keyring=reset at the CLI.
216
+ # Recommanded: auto
213
217
  use_keyring = False
214
218
 
215
219
  [search]
@@ -385,7 +389,7 @@ matching-bracket.other = '#000000 bg:#aacccc'
385
389
 
386
390
  # Favorite queries.
387
391
  # You can add your favorite queries here. They will be available in the
388
- # REPL when you type `\f` or `\f <query_name>`.
392
+ # REPL when you type `/f` or `/f <query_name>`.
389
393
  [favorite_queries]
390
394
  # example = "SELECT * FROM example_table WHERE id = 1"
391
395
 
@@ -403,3 +407,10 @@ matching-bracket.other = '#000000 bg:#aacccc'
403
407
  [alias_dsn.init-commands]
404
408
  # Define one or more SQL statements per alias (semicolon-separated).
405
409
  # 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
@@ -23,11 +23,7 @@ def is_valid_connection_scheme(text: str) -> tuple[bool, str | None]:
23
23
  # exit early if the text does not resemble a DSN URI
24
24
  if "://" not in text:
25
25
  return False, None
26
- scheme = text.split("://")[0]
27
- if scheme.startswith('mysql+'):
28
- return True, None
29
- if scheme.startswith('mysqlx+'):
30
- return True, None
31
- if scheme in ("mysql", "mysqlx", "tcp", "socket"):
26
+ scheme = text.split("://")[0].split("+")[0]
27
+ if scheme in ("mysql", "mysqlx"):
32
28
  return True, None
33
29
  return False, scheme
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class DsnAliases:
5
+ section_name: str = 'alias_dsn'
6
+
7
+ usage = """
8
+ DSN aliases are a way to save frequently used connections
9
+ with a short alias. You can manage them by editing ~/.myclirc
10
+ directly or by using this command.
11
+
12
+ Examples:
13
+
14
+ # List all DSN aliases
15
+ mysql> /dsn list
16
+ ┌───────┬───────────────────────────────┐
17
+ │ Alias │ DSN │
18
+ ├───────┼───────────────────────────────┤
19
+ │ rocks │ mysql://mycli@localhost/mysql │
20
+ └───────┴───────────────────────────────┘
21
+
22
+ # Save a new DSN alias based on the current connection.
23
+ # The password will not be included!
24
+ mysql> /dsn save connection_1
25
+
26
+ # Delete a DSN alias.
27
+ mysql> /dsn delete connection_1
28
+ """
29
+
30
+ # Class-level variable, for convenience to use as a singleton.
31
+ instance: DsnAliases
32
+
33
+ def __init__(self, config) -> None:
34
+ self.config = config
35
+
36
+ @classmethod
37
+ def from_config(cls, config):
38
+ return DsnAliases(config)
39
+
40
+ def list(self) -> list[str]:
41
+ return list(self.config.get(self.section_name, {}))
42
+
43
+ def get(self, alias: str) -> str | None:
44
+ return self.config.get(self.section_name, {}).get(alias, None)
45
+
46
+ def save(self, alias: str, dsn: str) -> str:
47
+ self.config.encoding = 'utf-8'
48
+ if self.section_name not in self.config:
49
+ self.config[self.section_name] = {}
50
+ self.config[self.section_name][alias] = dsn
51
+ self.config.write()
52
+ return f'Saved: {alias}'
53
+
54
+ def delete(self, alias: str) -> str:
55
+ try:
56
+ del self.config[self.section_name][alias]
57
+ except KeyError:
58
+ return f'Not Found: {alias}'
59
+ self.config.write()
60
+ return f'Deleted: {alias}'
@@ -10,10 +10,10 @@ with a short name.
10
10
  Examples:
11
11
 
12
12
  # Save a new favorite query.
13
- > \\fs simple select * from abc where a is not Null;
13
+ > /fs simple select * from abc where a is not Null;
14
14
 
15
15
  # List all favorite queries.
16
- > \\f
16
+ > /f
17
17
  ╒════════╤═══════════════════════════════════════╕
18
18
  │ Name │ Query │
19
19
  ╞════════╪═══════════════════════════════════════╡
@@ -21,7 +21,7 @@ Examples:
21
21
  ╘════════╧═══════════════════════════════════════╛
22
22
 
23
23
  # Run a favorite query.
24
- > \\f simple
24
+ > /f simple
25
25
  ╒════════╤════════╕
26
26
  │ a │ b │
27
27
  ╞════════╪════════╡
@@ -29,7 +29,7 @@ Examples:
29
29
  ╘════════╧════════╛
30
30
 
31
31
  # Delete a favorite query.
32
- > \\fd simple
32
+ > /fd simple
33
33
  simple: Deleted.
34
34
  """
35
35