mycli 2.19.0__tar.gz → 2.20.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 (193) hide show
  1. {mycli-2.19.0/mycli.egg-info → mycli-2.20.0}/PKG-INFO +8 -8
  2. {mycli-2.19.0 → mycli-2.20.0}/README.md +1 -1
  3. {mycli-2.19.0 → mycli-2.20.0}/changelog.md +30 -0
  4. {mycli-2.19.0 → mycli-2.20.0}/mycli/client_commands.py +28 -141
  5. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/completion_engine.py +59 -13
  6. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/hybrid_redirection.py +31 -1
  7. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/special/__init__.py +6 -3
  8. mycli-2.20.0/mycli/packages/special/source.py +169 -0
  9. {mycli-2.19.0 → mycli-2.20.0/mycli.egg-info}/PKG-INFO +8 -8
  10. {mycli-2.19.0 → mycli-2.20.0}/mycli.egg-info/SOURCES.txt +2 -0
  11. {mycli-2.19.0 → mycli-2.20.0}/mycli.egg-info/requires.txt +6 -6
  12. {mycli-2.19.0 → mycli-2.20.0}/mycli.egg-info/scm_file_list.json +2 -0
  13. mycli-2.20.0/mycli.egg-info/scm_version.json +8 -0
  14. {mycli-2.19.0 → mycli-2.20.0}/pyproject.toml +6 -6
  15. mycli-2.20.0/test/features/fixture_data/help_commands.txt +43 -0
  16. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_client_commands.py +91 -169
  17. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_completion_engine.py +118 -9
  18. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_hybrid_redirection.py +51 -0
  19. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_smart_completion_public_schema_only.py +50 -5
  20. mycli-2.20.0/test/pytests/test_special_source.py +187 -0
  21. mycli-2.19.0/mycli.egg-info/scm_version.json +0 -8
  22. mycli-2.19.0/test/features/fixture_data/help_commands.txt +0 -43
  23. {mycli-2.19.0 → mycli-2.20.0}/.git-blame-ignore-revs +0 -0
  24. {mycli-2.19.0 → mycli-2.20.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  25. {mycli-2.19.0 → mycli-2.20.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  26. {mycli-2.19.0 → mycli-2.20.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  27. {mycli-2.19.0 → mycli-2.20.0}/.github/dependabot.yml +0 -0
  28. {mycli-2.19.0 → mycli-2.20.0}/.github/workflows/ci.yml +0 -0
  29. {mycli-2.19.0 → mycli-2.20.0}/.github/workflows/lint.yml +0 -0
  30. {mycli-2.19.0 → mycli-2.20.0}/.github/workflows/publish.yml +0 -0
  31. {mycli-2.19.0 → mycli-2.20.0}/.github/workflows/typecheck.yml +0 -0
  32. {mycli-2.19.0 → mycli-2.20.0}/.gitignore +0 -0
  33. {mycli-2.19.0 → mycli-2.20.0}/AGENTS.md +0 -0
  34. {mycli-2.19.0 → mycli-2.20.0}/AUTHORS.rst +0 -0
  35. {mycli-2.19.0 → mycli-2.20.0}/CONTRIBUTING.md +0 -0
  36. {mycli-2.19.0 → mycli-2.20.0}/LICENSE.txt +0 -0
  37. {mycli-2.19.0 → mycli-2.20.0}/MANIFEST.in +0 -0
  38. {mycli-2.19.0 → mycli-2.20.0}/SPONSORS.rst +0 -0
  39. {mycli-2.19.0 → mycli-2.20.0}/doc/key_bindings.rst +0 -0
  40. {mycli-2.19.0 → mycli-2.20.0}/doc/llm.md +0 -0
  41. {mycli-2.19.0 → mycli-2.20.0}/doc/screenshots/main.gif +0 -0
  42. {mycli-2.19.0 → mycli-2.20.0}/doc/screenshots/tables.png +0 -0
  43. {mycli-2.19.0 → mycli-2.20.0}/doc/screenshots/total_histogram.png +0 -0
  44. {mycli-2.19.0 → mycli-2.20.0}/doc/transforms.md +0 -0
  45. {mycli-2.19.0 → mycli-2.20.0}/mycli/AUTHORS +0 -0
  46. {mycli-2.19.0 → mycli-2.20.0}/mycli/SPONSORS +0 -0
  47. {mycli-2.19.0 → mycli-2.20.0}/mycli/TIPS +0 -0
  48. {mycli-2.19.0 → mycli-2.20.0}/mycli/__init__.py +0 -0
  49. {mycli-2.19.0 → mycli-2.20.0}/mycli/app_state.py +0 -0
  50. {mycli-2.19.0 → mycli-2.20.0}/mycli/boundary_tunnel.py +0 -0
  51. {mycli-2.19.0 → mycli-2.20.0}/mycli/cli_runner.py +0 -0
  52. {mycli-2.19.0 → mycli-2.20.0}/mycli/clibuffer.py +0 -0
  53. {mycli-2.19.0 → mycli-2.20.0}/mycli/client.py +0 -0
  54. {mycli-2.19.0 → mycli-2.20.0}/mycli/client_connection.py +0 -0
  55. {mycli-2.19.0 → mycli-2.20.0}/mycli/client_query.py +0 -0
  56. {mycli-2.19.0 → mycli-2.20.0}/mycli/clistyle.py +0 -0
  57. {mycli-2.19.0 → mycli-2.20.0}/mycli/clitoolbar.py +0 -0
  58. {mycli-2.19.0 → mycli-2.20.0}/mycli/compat.py +0 -0
  59. {mycli-2.19.0 → mycli-2.20.0}/mycli/completion_refresher.py +0 -0
  60. {mycli-2.19.0 → mycli-2.20.0}/mycli/config.py +0 -0
  61. {mycli-2.19.0 → mycli-2.20.0}/mycli/constants.py +0 -0
  62. {mycli-2.19.0 → mycli-2.20.0}/mycli/key_bindings.py +0 -0
  63. {mycli-2.19.0 → mycli-2.20.0}/mycli/lexer.py +0 -0
  64. {mycli-2.19.0 → mycli-2.20.0}/mycli/main.py +0 -0
  65. {mycli-2.19.0 → mycli-2.20.0}/mycli/main_modes/batch.py +0 -0
  66. {mycli-2.19.0 → mycli-2.20.0}/mycli/main_modes/checkup.py +0 -0
  67. {mycli-2.19.0 → mycli-2.20.0}/mycli/main_modes/completions.py +0 -0
  68. {mycli-2.19.0 → mycli-2.20.0}/mycli/main_modes/execute.py +0 -0
  69. {mycli-2.19.0 → mycli-2.20.0}/mycli/main_modes/list_dsn.py +0 -0
  70. {mycli-2.19.0 → mycli-2.20.0}/mycli/main_modes/repl.py +0 -0
  71. {mycli-2.19.0 → mycli-2.20.0}/mycli/myclirc +0 -0
  72. {mycli-2.19.0 → mycli-2.20.0}/mycli/output.py +0 -0
  73. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/__init__.py +0 -0
  74. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/batch_utils.py +0 -0
  75. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/cli_utils.py +0 -0
  76. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/filepaths.py +0 -0
  77. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/interactive_utils.py +0 -0
  78. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/key_binding_utils.py +0 -0
  79. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/polars_completion.py +0 -0
  80. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/polars_transform.py +0 -0
  81. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/ptoolkit/__init__.py +0 -0
  82. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/ptoolkit/fzf.py +0 -0
  83. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/ptoolkit/history.py +0 -0
  84. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/ptoolkit/utils.py +0 -0
  85. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/special/dbcommands.py +0 -0
  86. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/special/delimitercommand.py +0 -0
  87. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/special/dsn_aliases.py +0 -0
  88. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/special/favoritequeries.py +0 -0
  89. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/special/iocommands.py +0 -0
  90. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/special/llm.py +0 -0
  91. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/special/main.py +0 -0
  92. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/special/utils.py +0 -0
  93. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/sql_utils.py +0 -0
  94. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/sqlresult.py +0 -0
  95. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/string_utils.py +0 -0
  96. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/tabular_output/__init__.py +0 -0
  97. {mycli-2.19.0 → mycli-2.20.0}/mycli/packages/tabular_output/sql_format.py +0 -0
  98. {mycli-2.19.0 → mycli-2.20.0}/mycli/password_sources.py +0 -0
  99. {mycli-2.19.0 → mycli-2.20.0}/mycli/resources/completions/bash/mycli +0 -0
  100. {mycli-2.19.0 → mycli-2.20.0}/mycli/resources/completions/fish/mycli.fish +0 -0
  101. {mycli-2.19.0 → mycli-2.20.0}/mycli/resources/completions/zsh/_mycli +0 -0
  102. {mycli-2.19.0 → mycli-2.20.0}/mycli/schema_prefetcher.py +0 -0
  103. {mycli-2.19.0 → mycli-2.20.0}/mycli/sqlcompleter.py +0 -0
  104. {mycli-2.19.0 → mycli-2.20.0}/mycli/sqlexecute.py +0 -0
  105. {mycli-2.19.0 → mycli-2.20.0}/mycli/ssh_tunnel.py +0 -0
  106. {mycli-2.19.0 → mycli-2.20.0}/mycli/types.py +0 -0
  107. {mycli-2.19.0 → mycli-2.20.0}/mycli/vault.py +0 -0
  108. {mycli-2.19.0 → mycli-2.20.0}/mycli.egg-info/dependency_links.txt +0 -0
  109. {mycli-2.19.0 → mycli-2.20.0}/mycli.egg-info/entry_points.txt +0 -0
  110. {mycli-2.19.0 → mycli-2.20.0}/mycli.egg-info/top_level.txt +0 -0
  111. {mycli-2.19.0 → mycli-2.20.0}/setup.cfg +0 -0
  112. {mycli-2.19.0 → mycli-2.20.0}/test/__init__.py +0 -0
  113. {mycli-2.19.0 → mycli-2.20.0}/test/features/__init__.py +0 -0
  114. {mycli-2.19.0 → mycli-2.20.0}/test/features/auto_vertical.feature +0 -0
  115. {mycli-2.19.0 → mycli-2.20.0}/test/features/basic_commands.feature +0 -0
  116. {mycli-2.19.0 → mycli-2.20.0}/test/features/connection.feature +0 -0
  117. {mycli-2.19.0 → mycli-2.20.0}/test/features/crud_database.feature +0 -0
  118. {mycli-2.19.0 → mycli-2.20.0}/test/features/crud_table.feature +0 -0
  119. {mycli-2.19.0 → mycli-2.20.0}/test/features/db_utils.py +0 -0
  120. {mycli-2.19.0 → mycli-2.20.0}/test/features/environment.py +0 -0
  121. {mycli-2.19.0 → mycli-2.20.0}/test/features/fixture_utils.py +0 -0
  122. {mycli-2.19.0 → mycli-2.20.0}/test/features/iocommands.feature +0 -0
  123. {mycli-2.19.0 → mycli-2.20.0}/test/features/named_queries.feature +0 -0
  124. {mycli-2.19.0 → mycli-2.20.0}/test/features/specials.feature +0 -0
  125. {mycli-2.19.0 → mycli-2.20.0}/test/features/steps/__init__.py +0 -0
  126. {mycli-2.19.0 → mycli-2.20.0}/test/features/steps/auto_vertical.py +0 -0
  127. {mycli-2.19.0 → mycli-2.20.0}/test/features/steps/basic_commands.py +0 -0
  128. {mycli-2.19.0 → mycli-2.20.0}/test/features/steps/connection.py +0 -0
  129. {mycli-2.19.0 → mycli-2.20.0}/test/features/steps/crud_database.py +0 -0
  130. {mycli-2.19.0 → mycli-2.20.0}/test/features/steps/crud_table.py +0 -0
  131. {mycli-2.19.0 → mycli-2.20.0}/test/features/steps/iocommands.py +0 -0
  132. {mycli-2.19.0 → mycli-2.20.0}/test/features/steps/named_queries.py +0 -0
  133. {mycli-2.19.0 → mycli-2.20.0}/test/features/steps/specials.py +0 -0
  134. {mycli-2.19.0 → mycli-2.20.0}/test/features/steps/utils.py +0 -0
  135. {mycli-2.19.0 → mycli-2.20.0}/test/features/steps/wrappers.py +0 -0
  136. {mycli-2.19.0 → mycli-2.20.0}/test/features/wrappager.py +0 -0
  137. {mycli-2.19.0 → mycli-2.20.0}/test/myclirc +0 -0
  138. {mycli-2.19.0 → mycli-2.20.0}/test/mylogin.cnf +0 -0
  139. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/conftest.py +0 -0
  140. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_app_state.py +0 -0
  141. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_bash_completion.py +0 -0
  142. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_batch_utils.py +0 -0
  143. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_boundary_tunnel.py +0 -0
  144. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_checkup.py +0 -0
  145. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_cli_runner.py +0 -0
  146. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_cli_utils.py +0 -0
  147. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_clibuffer.py +0 -0
  148. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_client.py +0 -0
  149. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_client_connection.py +0 -0
  150. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_client_query.py +0 -0
  151. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_clistyle.py +0 -0
  152. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_clitoolbar.py +0 -0
  153. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_completer_use_switch.py +0 -0
  154. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_completion_refresher.py +0 -0
  155. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_config.py +0 -0
  156. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_delimitercommand.py +0 -0
  157. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_dsn_aliases.py +0 -0
  158. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_favoritequeries.py +0 -0
  159. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_filepaths.py +0 -0
  160. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_fish_completion.py +0 -0
  161. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_interactive_utils.py +0 -0
  162. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_key_binding_utils.py +0 -0
  163. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_key_bindings.py +0 -0
  164. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_main.py +0 -0
  165. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_main_modes_batch.py +0 -0
  166. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_main_modes_execute.py +0 -0
  167. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
  168. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_main_modes_repl.py +0 -0
  169. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_naive_completion.py +0 -0
  170. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_output.py +0 -0
  171. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_password_sources.py +0 -0
  172. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_polars_completion.py +0 -0
  173. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_polars_transform.py +0 -0
  174. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
  175. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_ptoolkit_history.py +0 -0
  176. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_ptoolkit_utils.py +0 -0
  177. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_schema_prefetcher.py +0 -0
  178. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_special_dbcommands.py +0 -0
  179. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_special_init.py +0 -0
  180. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_special_iocommands.py +0 -0
  181. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_special_llm.py +0 -0
  182. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_special_main.py +0 -0
  183. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_special_utils.py +0 -0
  184. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_sql_utils.py +0 -0
  185. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_sqlcompleter.py +0 -0
  186. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_sqlexecute.py +0 -0
  187. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_sqlresult.py +0 -0
  188. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_ssh_tunnel.py +0 -0
  189. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_string_utils.py +0 -0
  190. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_tabular_output.py +0 -0
  191. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_vault.py +0 -0
  192. {mycli-2.19.0 → mycli-2.20.0}/test/pytests/test_zsh_completion.py +0 -0
  193. {mycli-2.19.0 → mycli-2.20.0}/test/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.19.0
3
+ Version: 2.20.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
@@ -30,7 +30,7 @@ License-File: LICENSE.txt
30
30
  License-File: AUTHORS.rst
31
31
  Requires-Dist: click~=8.5.0
32
32
  Requires-Dist: clickdc~=0.1.1
33
- Requires-Dist: cryptography~=50.0.0
33
+ Requires-Dist: cryptography~=50.0.1
34
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
@@ -39,11 +39,11 @@ Requires-Dist: sqlglot~=30.17.0
39
39
  Requires-Dist: sqlglotc~=30.17.0
40
40
  Requires-Dist: configobj~=5.0.9
41
41
  Requires-Dist: cli_helpers[styles]~=2.15.1
42
- Requires-Dist: wcwidth~=0.8.2
42
+ Requires-Dist: wcwidth~=0.8.3
43
43
  Requires-Dist: pyperclip~=1.11.0
44
44
  Requires-Dist: pycryptodomex~=3.23.0
45
45
  Requires-Dist: pyfzf~=0.3.1
46
- Requires-Dist: rapidfuzz~=3.14.3
46
+ Requires-Dist: rapidfuzz~=3.14.5
47
47
  Requires-Dist: keyring~=25.7.0
48
48
  Requires-Dist: jinja2~=3.1.6
49
49
  Requires-Dist: yaspin~=3.4.0
@@ -59,21 +59,21 @@ Provides-Extra: all
59
59
  Requires-Dist: mycli[dataframe,llm]; extra == "all"
60
60
  Provides-Extra: dev
61
61
  Requires-Dist: behave~=1.3.3; extra == "dev"
62
- Requires-Dist: coverage~=7.13.4; extra == "dev"
62
+ Requires-Dist: coverage~=7.15.4; extra == "dev"
63
63
  Requires-Dist: mypy~=2.3.1; extra == "dev"
64
64
  Requires-Dist: pexpect~=4.9.0; extra == "dev"
65
65
  Requires-Dist: pytest~=9.1.1; extra == "dev"
66
- Requires-Dist: pytest-cov~=7.0.0; extra == "dev"
66
+ Requires-Dist: pytest-cov~=7.1.0; extra == "dev"
67
67
  Requires-Dist: pytest-random-order~=1.2.0; extra == "dev"
68
68
  Requires-Dist: tox~=4.35.0; extra == "dev"
69
69
  Requires-Dist: pdbpp~=0.11.7; extra == "dev"
70
- Requires-Dist: ruff~=0.16.3; extra == "dev"
70
+ Requires-Dist: ruff~=0.16.5; extra == "dev"
71
71
  Dynamic: license-file
72
72
 
73
73
  # mycli
74
74
 
75
75
  [![Build Status](https://github.com/dbcli/mycli/workflows/mycli/badge.svg)](https://github.com/dbcli/mycli/actions?query=workflow%3Amycli)
76
- [![License](https://img.shields.io/github/license/dbcli/mycli.svg)](https://github.com/dbcli/mycli/blob/main/LICENSE.txt)
76
+ [![License](https://img.shields.io/github/license/dbcli/mycli.svg?color=2FC745)](https://github.com/dbcli/mycli/blob/main/LICENSE.txt)
77
77
 
78
78
  Rich MySQL terminal client with auto-completion, syntax highlighting, and dataframes.
79
79
 
@@ -1,7 +1,7 @@
1
1
  # mycli
2
2
 
3
3
  [![Build Status](https://github.com/dbcli/mycli/workflows/mycli/badge.svg)](https://github.com/dbcli/mycli/actions?query=workflow%3Amycli)
4
- [![License](https://img.shields.io/github/license/dbcli/mycli.svg)](https://github.com/dbcli/mycli/blob/main/LICENSE.txt)
4
+ [![License](https://img.shields.io/github/license/dbcli/mycli.svg?color=2FC745)](https://github.com/dbcli/mycli/blob/main/LICENSE.txt)
5
5
 
6
6
  Rich MySQL terminal client with auto-completion, syntax highlighting, and dataframes.
7
7
 
@@ -1,3 +1,33 @@
1
+ 2.20.0 (2026/09/05)
2
+ ==============
3
+
4
+ Features
5
+ --------
6
+ * Add a `/source --throttle` option to pause between sourced statements.
7
+
8
+
9
+ Bug Fixes
10
+ --------
11
+ * Allow shell-style redirects with `/source` when the filename is unquoted.
12
+
13
+
14
+ Documentation
15
+ --------
16
+ * Badge color nit in `README.md`.
17
+ * Add a `/source --help` option.
18
+
19
+
20
+ Internal
21
+ ---------
22
+ * Upgrade `cryptography` dependency to v50.0.1.
23
+ * Upgrade `rapidfuzz` dependency to v3.14.5.
24
+ * Upgrade `ruff` dev dependency to v0.16.5.
25
+ * Upgrade `coverage` dev dependency to v7.15.4.
26
+ * Upgrade `pytest-cov` dev dependency to v7.1.0.
27
+ * Upgrade `wcwidth` dependency to v0.8.3.
28
+ * Use `argparse` to process arguments to `/source`.
29
+
30
+
1
31
  2.19.0 (2026/09/02)
2
32
  ==============
3
33
 
@@ -4,13 +4,11 @@ from collections.abc import Generator, Mapping
4
4
  import logging
5
5
  import os
6
6
  import re
7
- import shlex
7
+ import time
8
8
  from typing import TYPE_CHECKING, Any, cast
9
9
 
10
10
  import click
11
- import sqlparse
12
11
 
13
- from mycli.compat import WIN
14
12
  from mycli.config import write_default_config
15
13
  from mycli.main_modes.repl import set_all_external_titles
16
14
  from mycli.packages import special
@@ -18,9 +16,12 @@ from mycli.packages.batch_utils import statements_from_filehandle
18
16
  from mycli.packages.filepaths import dir_path_exists
19
17
  from mycli.packages.interactive_utils import confirm_destructive_query
20
18
  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
23
19
  from mycli.packages.special.main import ArgType, SpecialCommandAlias
20
+ from mycli.packages.special.source import (
21
+ SOURCE_HELP_ROWS,
22
+ parse_source_arguments,
23
+ source_special_command_is_safe,
24
+ )
24
25
  from mycli.packages.sqlresult import SQLResult
25
26
  from mycli.sqlexecute import SQLExecute
26
27
 
@@ -36,30 +37,6 @@ MISSING_CONFIG_VALUE = object()
36
37
  DSN_CONFIG_VALUE = object()
37
38
  FAVORITES_CONFIG_VALUE = object()
38
39
  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
- 'ping',
48
- 'prompt',
49
- 'redirectformat',
50
- 'rehash',
51
- 'status',
52
- 'tableformat',
53
- 'timing',
54
- 'use',
55
- 'warnings',
56
- 'dt',
57
- })
58
- SOURCE_SAFE_SUBCOMMANDS = {
59
- 'config': frozenset({'help', 'get', 'search'}),
60
- 'dsn': frozenset({'help', 'list', 'show', 'save', 'delete'}),
61
- 'favorite': frozenset({'help', 'list', 'reload', 'run', 'save', 'delete'}),
62
- }
63
40
 
64
41
 
65
42
  def _render_config_value(value: Any) -> str:
@@ -68,106 +45,6 @@ def _render_config_value(value: Any) -> str:
68
45
  return str(value)
69
46
 
70
47
 
71
- def _parse_source_arguments(arg: str) -> tuple[str, bool, bool, bool]:
72
- allow_special = False
73
- show_queries = False
74
- page_output = False
75
- filename = arg
76
- while arguments := filename.split(maxsplit=1):
77
- if arguments[0] == '--special':
78
- allow_special = True
79
- elif arguments[0] == '--show':
80
- show_queries = True
81
- elif arguments[0] == '--page':
82
- page_output = True
83
- else:
84
- break
85
- filename = arguments[1] if len(arguments) == 2 else ''
86
- return filename, allow_special, show_queries, page_output
87
-
88
-
89
- def _has_unquoted_whitespace(value: str) -> bool:
90
- quote: str | None = None
91
- escaped = False
92
- for character in value:
93
- if escaped:
94
- if quote is None and character.isspace():
95
- return True
96
- escaped = False
97
- continue
98
- if not WIN and character == '\\' and quote != "'":
99
- escaped = True
100
- continue
101
- if character in ("'", '"'):
102
- if quote is None:
103
- quote = character
104
- elif quote == character:
105
- quote = None
106
- elif quote is None and character.isspace():
107
- return True
108
- return False
109
-
110
-
111
- def _parse_source_filename(filename: str) -> str:
112
- if not filename:
113
- return ''
114
- if _has_unquoted_whitespace(filename):
115
- raise ValueError(INVALID_SOURCE_FILENAME)
116
- try:
117
- arguments = shlex.split(filename, posix=not WIN)
118
- except ValueError as error:
119
- raise ValueError(f'Invalid source filename: {error}.') from None
120
- if len(arguments) != 1:
121
- raise ValueError(INVALID_SOURCE_FILENAME)
122
- parsed_filename = arguments[0]
123
- if WIN and len(parsed_filename) >= 2 and parsed_filename[0] == parsed_filename[-1] and parsed_filename[0] in ("'", '"'):
124
- parsed_filename = parsed_filename[1:-1]
125
- return parsed_filename
126
-
127
-
128
- def _registered_special_command(query: str) -> tuple[str, str] | None:
129
- command, _verbosity, arg = special.parse_special_command(query)
130
- registered = special_main.COMMANDS.get(command)
131
- if registered is None:
132
- registered = special_main.COMMANDS.get(command.lower())
133
- if registered is None:
134
- return None
135
- return registered.command.removeprefix('\\').removeprefix('/').lower(), arg
136
-
137
-
138
- def _favorite_source_command_is_safe(arg: str) -> bool:
139
- query, _error = expand_favorite_query(arg)
140
- if query is None:
141
- return True
142
- return not any(special.is_special_command(statement.rstrip(';')) for statement in sqlparse.split(query))
143
-
144
-
145
- def _source_special_command_is_safe(query: str) -> bool:
146
- parsed = _registered_special_command(query)
147
- if parsed is None:
148
- return False
149
-
150
- command, arg = parsed
151
- if command == 'f':
152
- return not arg or _favorite_source_command_is_safe(arg)
153
- if command in ('fd', 'fs'):
154
- return True
155
- if command in SOURCE_SAFE_SPECIAL_COMMANDS:
156
- return True
157
-
158
- subcommands = SOURCE_SAFE_SUBCOMMANDS.get(command)
159
- if subcommands is None:
160
- return False
161
- arguments = arg.split(maxsplit=1)
162
- subcommand = arguments[0].lower() if arguments else 'help'
163
- if subcommand not in subcommands:
164
- return False
165
- if command == 'favorite' and subcommand == 'run':
166
- run_arg = arguments[1] if len(arguments) == 2 else ''
167
- return not run_arg or _favorite_source_command_is_safe(run_arg)
168
- return True
169
-
170
-
171
48
  def _iter_config_values(
172
49
  config: Mapping[str, Any],
173
50
  prefix: str = '',
@@ -279,7 +156,7 @@ class ClientCommandsMixin:
279
156
  special.register_special_command(
280
157
  self.execute_from_file,
281
158
  "source",
282
- "/source [--special|--show|--page] <file>",
159
+ "/source [options] <file>",
283
160
  "Execute queries from a file.",
284
161
  aliases=[SpecialCommandAlias("\\.", case_sensitive=False)],
285
162
  completion_snippet='execute queries from file',
@@ -412,16 +289,19 @@ class ClientCommandsMixin:
412
289
  yield SQLResult(status=msg)
413
290
 
414
291
  def execute_from_file(self, arg: str, **_) -> Generator[SQLResult, None, None]:
415
- filename, allow_special, show_queries, page_output = _parse_source_arguments(arg)
416
- if page_output:
417
- yield SQLResult(command={'name': 'source_page'})
418
292
  try:
419
- filename = _parse_source_filename(filename)
293
+ source_arguments = parse_source_arguments(arg)
420
294
  except ValueError as error:
421
295
  yield SQLResult(status=str(error), is_error=True)
422
296
  return
297
+ if source_arguments.show_help:
298
+ yield SQLResult(header=['Argument', 'Description'], rows=SOURCE_HELP_ROWS)
299
+ return
300
+ if source_arguments.page_output:
301
+ yield SQLResult(command={'name': 'source_page'})
302
+ filename = source_arguments.filename
423
303
  if not filename:
424
- yield SQLResult(status="Missing required argument: filename.", is_error=True)
304
+ yield SQLResult(status="Missing required argument: filename. See /source --help.", is_error=True)
425
305
  return
426
306
 
427
307
  try:
@@ -431,6 +311,7 @@ class ClientCommandsMixin:
431
311
  return
432
312
 
433
313
  assert isinstance(self.sqlexecute, SQLExecute)
314
+ executed_statement = False
434
315
  with file_h:
435
316
  statements = statements_from_filehandle(file_h)
436
317
  while True:
@@ -444,35 +325,41 @@ class ClientCommandsMixin:
444
325
 
445
326
  special_query = query.rstrip(';')
446
327
  if special.is_special_command(special_query):
447
- if not allow_special:
328
+ if not source_arguments.allow_special:
448
329
  yield SQLResult(
449
330
  status='Special commands are not supported without /source --special.',
450
331
  is_error=True,
451
332
  )
452
333
  return
453
- if not _source_special_command_is_safe(special_query):
334
+ if not source_special_command_is_safe(special_query):
454
335
  command, _verbosity, _arg = special.parse_special_command(special_query)
455
336
  yield SQLResult(
456
337
  status=f'Special command is never permitted in source files: {command}.',
457
338
  is_error=True,
458
339
  )
459
340
  return
460
- if show_queries:
461
- if page_output:
341
+ if executed_statement and source_arguments.throttle > 0:
342
+ time.sleep(source_arguments.throttle)
343
+ if source_arguments.show_queries:
344
+ if source_arguments.page_output:
462
345
  yield SQLResult(command={'name': 'source_show', 'text': special_query})
463
346
  else:
464
347
  click.secho(f'> {special_query}')
465
348
  yield from self.sqlexecute.run(special_query)
349
+ executed_statement = True
466
350
  continue
467
351
 
468
352
  if self.destructive_warning and confirm_destructive_query(self.destructive_keywords, query) is False:
469
353
  continue
470
- if show_queries:
471
- if page_output:
354
+ if executed_statement and source_arguments.throttle > 0:
355
+ time.sleep(source_arguments.throttle)
356
+ if source_arguments.show_queries:
357
+ if source_arguments.page_output:
472
358
  yield SQLResult(command={'name': 'source_show', 'text': query})
473
359
  else:
474
360
  click.secho(f'> {query}')
475
361
  yield from self.sqlexecute.run(query)
362
+ executed_statement = True
476
363
 
477
364
  def change_prompt_format(self, arg: str, **_) -> list[SQLResult]:
478
365
  """
@@ -11,6 +11,7 @@ from mycli.packages.special.dsn_aliases import DSN_SUBCOMMANDS
11
11
  from mycli.packages.special.favoritequeries import FAVORITE_SUBCOMMANDS
12
12
  from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS
13
13
  from mycli.packages.special.main import parse_special_command
14
+ from mycli.packages.special.source import SOURCE_BOOLEAN_OPTIONS, SOURCE_OPTIONS
14
15
  from mycli.packages.sql_utils import extract_tables, find_prev_keyword, last_word
15
16
 
16
17
  sqlparse.engine.grouping.MAX_GROUPING_DEPTH = None # type: ignore[assignment]
@@ -816,8 +817,24 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
816
817
  'source',
817
818
  '/source',
818
819
  ]:
819
- source_options = ['--special', '--show', '--page']
820
- source_arguments = _arg.split()
820
+ source_options = list(SOURCE_OPTIONS)
821
+ try:
822
+ source_arguments = shlex.split(_arg, posix=False)
823
+ except ValueError:
824
+ source_filename = _arg
825
+ while leading_arguments := source_filename.split(maxsplit=1):
826
+ if leading_arguments[0] in SOURCE_BOOLEAN_OPTIONS:
827
+ source_filename = leading_arguments[1] if len(leading_arguments) == 2 else ''
828
+ continue
829
+ if leading_arguments[0] == '--throttle' and len(leading_arguments) == 2:
830
+ throttle_arguments = leading_arguments[1].split(maxsplit=1)
831
+ source_filename = throttle_arguments[1] if len(throttle_arguments) == 2 else ''
832
+ continue
833
+ if leading_arguments[0].startswith('--throttle='):
834
+ source_filename = leading_arguments[1] if len(leading_arguments) == 2 else ''
835
+ continue
836
+ break
837
+ return [{'type': 'file_name', 'quote_spaces': True, 'source_filename': source_filename}]
821
838
  if not source_arguments:
822
839
  return [
823
840
  {'type': 'special_subcommand', 'subcommands': source_options},
@@ -826,26 +843,55 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
826
843
 
827
844
  used_options: set[str] = set()
828
845
  argument_index = 0
829
- while argument_index < len(source_arguments) and source_arguments[argument_index] in source_options:
830
- used_options.add(source_arguments[argument_index])
846
+ filename_index: int | None = None
847
+ options_ended = False
848
+ while argument_index < len(source_arguments):
849
+ argument = source_arguments[argument_index]
850
+ if not options_ended and argument == '--':
851
+ options_ended = True
852
+ used_options.update(source_options)
853
+ argument_index += 1
854
+ continue
855
+ if not options_ended and argument in SOURCE_BOOLEAN_OPTIONS:
856
+ used_options.add(argument)
857
+ argument_index += 1
858
+ continue
859
+ if not options_ended and argument == '--help':
860
+ return []
861
+ if not options_ended and argument == '--throttle':
862
+ used_options.add(argument)
863
+ argument_index += 1
864
+ if argument_index >= len(source_arguments):
865
+ return []
866
+ if argument_index == len(source_arguments) - 1 and not text[-1].isspace():
867
+ return []
868
+ argument_index += 1
869
+ continue
870
+ if not options_ended and argument.startswith('--throttle='):
871
+ used_options.add('--throttle')
872
+ if argument == '--throttle=' or not text[-1].isspace():
873
+ return []
874
+ argument_index += 1
875
+ continue
876
+ if not options_ended and argument.startswith('-'):
877
+ break
878
+ if filename_index is not None:
879
+ return []
880
+ filename_index = argument_index
831
881
  argument_index += 1
832
882
  remaining_options = [option for option in source_options if option not in used_options]
833
- source_filename = _arg
834
- for _index in range(argument_index):
835
- parsed_argument = source_filename.split(maxsplit=1)
836
- source_filename = parsed_argument[1] if len(parsed_argument) == 2 else ''
883
+ source_filename = source_arguments[filename_index] if filename_index is not None else ''
837
884
  file_suggestion = {'type': 'file_name', 'quote_spaces': True, 'source_filename': source_filename}
838
885
 
839
886
  if argument_index < len(source_arguments):
840
- if source_arguments[argument_index].startswith('-'):
841
- return [{'type': 'special_subcommand', 'subcommands': remaining_options}]
842
- return [file_suggestion]
887
+ return [{'type': 'special_subcommand', 'subcommands': remaining_options}]
843
888
  if not text[-1].isspace():
844
- return []
889
+ return [file_suggestion] if filename_index == len(source_arguments) - 1 else []
845
890
  suggestions: list[dict[str, Any]] = []
846
891
  if remaining_options:
847
892
  suggestions.append({'type': 'special_subcommand', 'subcommands': remaining_options})
848
- suggestions.append(file_suggestion)
893
+ if filename_index is None:
894
+ suggestions.append(file_suggestion)
849
895
  return suggestions
850
896
 
851
897
  if cmd.lower() in [
@@ -1,14 +1,35 @@
1
1
  import functools
2
2
  import logging
3
+ import re
3
4
  import shlex
4
5
 
5
6
  import sqlglot
6
7
 
7
8
  from mycli.compat import WIN
8
9
  from mycli.packages.special.delimitercommand import DelimiterCommand
10
+ from mycli.packages.special.source import (
11
+ parse_source_arguments,
12
+ )
9
13
 
10
14
  logger = logging.getLogger(__name__)
11
15
  delimiter_command = DelimiterCommand()
16
+ SOURCE_COMMAND_PATTERN = re.compile(r'^([/]?source|[/\\]\.)\s+', re.IGNORECASE)
17
+ SOURCE_OPTION_PATTERN = re.compile(r'(?<!\S)--(?:special|show|page|help|throttle)(?==|\s|$)', re.IGNORECASE)
18
+ SOURCE_OPTION_TERMINATOR_PATTERN = re.compile(r'(?<!\S)--(?=\s|$)')
19
+ HYBRID_OPERATOR_PATTERN = re.compile(r'\$(?:>>?|\|)')
20
+
21
+
22
+ def tokenize_command(command: str) -> list[sqlglot.Token]:
23
+ """Tokenize a command without treating source options as SQL comments."""
24
+ if SOURCE_COMMAND_PATTERN.match(command):
25
+ operator_match = HYBRID_OPERATOR_PATTERN.search(command)
26
+ source_end = operator_match.start() if operator_match is not None else len(command)
27
+ source_part = command[:source_end]
28
+ terminator_match = SOURCE_OPTION_TERMINATOR_PATTERN.search(source_part)
29
+ options_end = terminator_match.start() if terminator_match is not None else len(source_part)
30
+ masked_options = SOURCE_OPTION_PATTERN.sub(lambda match: match.group().replace('-', '_'), source_part[:options_end])
31
+ command = masked_options + source_part[options_end:] + command[source_end:]
32
+ return sqlglot.tokenize(command)
12
33
 
13
34
 
14
35
  def find_token_indices(tokens: list[sqlglot.Token]) -> dict[str, list[int]]:
@@ -44,6 +65,15 @@ def find_sql_part(
44
65
  ):
45
66
  leftmost_dollar_pos = tokens[true_dollar_indices[0]].start
46
67
  sql_part = command[0:leftmost_dollar_pos].strip().removesuffix(delimiter_command.current).rstrip()
68
+ if SOURCE_COMMAND_PATTERN.match(sql_part):
69
+ source_arg_str = SOURCE_COMMAND_PATTERN.sub('', sql_part)
70
+ try:
71
+ filename = parse_source_arguments(source_arg_str).filename
72
+ except ValueError:
73
+ return ''
74
+ if not filename:
75
+ return ''
76
+ return sql_part
47
77
  try:
48
78
  statements = sqlglot.parse(sql_part, read='mysql')
49
79
  except sqlglot.errors.ParseError:
@@ -142,7 +172,7 @@ def get_redirect_components(command: str) -> tuple[str | None, str | None, str |
142
172
  """Get the parts of a hybrid shell-style redirect command."""
143
173
 
144
174
  try:
145
- tokens = sqlglot.tokenize(command)
175
+ tokens = tokenize_command(command)
146
176
  except sqlglot.errors.TokenError:
147
177
  return None, None, None, None
148
178
 
@@ -3,6 +3,7 @@ __lazy_modules__ = [
3
3
  'mycli.packages.special.iocommands',
4
4
  'mycli.packages.special.llm',
5
5
  'mycli.packages.special.main',
6
+ 'mycli.packages.special.source',
6
7
  ]
7
8
 
8
9
  import os
@@ -54,6 +55,7 @@ from mycli.packages.special.iocommands import (
54
55
  write_pipe_once,
55
56
  write_tee,
56
57
  )
58
+ from mycli.packages.special.source import parse_source_arguments
57
59
 
58
60
  if not os.environ.get('MYCLI_LLM_OFF'):
59
61
  from mycli.packages.special.llm import (
@@ -112,12 +114,14 @@ __all__: list[str] = [
112
114
  'is_llm_command',
113
115
  'is_pager_enabled',
114
116
  'is_redirected',
115
- 'is_special_command',
117
+ 'is_show_favorite_query',
116
118
  'is_show_warnings_enabled',
119
+ 'is_special_command',
117
120
  'is_timing_enabled',
118
121
  'list_databases',
119
122
  'list_tables',
120
123
  'open_external_editor',
124
+ 'parse_source_arguments',
121
125
  'parse_special_command',
122
126
  'ping',
123
127
  'register_special_command',
@@ -131,10 +135,9 @@ __all__: list[str] = [
131
135
  'set_pager',
132
136
  'set_pager_enabled',
133
137
  'set_redirect',
138
+ 'set_show_favorite_query',
134
139
  'set_show_warnings_enabled',
135
140
  'set_timing_enabled',
136
- 'set_show_favorite_query',
137
- 'is_show_favorite_query',
138
141
  'special_command',
139
142
  'split_queries',
140
143
  'sql_using_llm',