mycli 2.15.0__tar.gz → 2.17.0__tar.gz

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