mycli 2.5.0__tar.gz → 2.6.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 (183) hide show
  1. {mycli-2.5.0/mycli.egg-info → mycli-2.6.1}/PKG-INFO +7 -4
  2. {mycli-2.5.0 → mycli-2.6.1}/README.md +4 -3
  3. {mycli-2.5.0 → mycli-2.6.1}/changelog.md +37 -0
  4. mycli-2.6.1/doc/screenshots/total_histogram.png +0 -0
  5. mycli-2.6.1/doc/transforms.md +128 -0
  6. {mycli-2.5.0 → mycli-2.6.1}/mycli/TIPS +2 -2
  7. {mycli-2.5.0 → mycli-2.6.1}/mycli/app_state.py +11 -0
  8. {mycli-2.5.0 → mycli-2.6.1}/mycli/cli_runner.py +4 -4
  9. {mycli-2.5.0 → mycli-2.6.1}/mycli/client.py +7 -0
  10. mycli-2.6.1/mycli/compat.py +27 -0
  11. {mycli-2.5.0 → mycli-2.6.1}/mycli/main_modes/repl.py +21 -6
  12. {mycli-2.5.0 → mycli-2.6.1}/mycli/myclirc +21 -0
  13. {mycli-2.5.0 → mycli-2.6.1}/mycli/output.py +26 -1
  14. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/completion_engine.py +12 -0
  15. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/hybrid_redirection.py +17 -4
  16. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/polars_transform.py +144 -35
  17. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/dsn_aliases.py +2 -0
  18. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/sqlresult.py +8 -1
  19. {mycli-2.5.0 → mycli-2.6.1}/mycli/sqlcompleter.py +22 -0
  20. {mycli-2.5.0 → mycli-2.6.1}/mycli/types.py +6 -0
  21. {mycli-2.5.0 → mycli-2.6.1/mycli.egg-info}/PKG-INFO +7 -4
  22. {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/SOURCES.txt +1 -0
  23. {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/requires.txt +2 -0
  24. {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/scm_file_list.json +131 -130
  25. mycli-2.6.1/mycli.egg-info/scm_version.json +8 -0
  26. {mycli-2.5.0 → mycli-2.6.1}/pyproject.toml +2 -0
  27. {mycli-2.5.0 → mycli-2.6.1}/test/myclirc +62 -43
  28. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_app_state.py +72 -0
  29. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_client.py +70 -0
  30. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_completion_engine.py +7 -0
  31. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_hybrid_redirection.py +50 -2
  32. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_main_modes_repl.py +123 -9
  33. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_output.py +90 -1
  34. mycli-2.6.1/test/pytests/test_polars_transform.py +868 -0
  35. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_smart_completion_public_schema_only.py +30 -0
  36. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_sqlcompleter.py +51 -0
  37. mycli-2.5.0/doc/transforms.md +0 -98
  38. mycli-2.5.0/mycli/compat.py +0 -5
  39. mycli-2.5.0/mycli.egg-info/scm_version.json +0 -8
  40. mycli-2.5.0/test/pytests/test_polars_transform.py +0 -405
  41. {mycli-2.5.0 → mycli-2.6.1}/.git-blame-ignore-revs +0 -0
  42. {mycli-2.5.0 → mycli-2.6.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  43. {mycli-2.5.0 → mycli-2.6.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  44. {mycli-2.5.0 → mycli-2.6.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  45. {mycli-2.5.0 → mycli-2.6.1}/.github/dependabot.yml +0 -0
  46. {mycli-2.5.0 → mycli-2.6.1}/.github/workflows/ci.yml +0 -0
  47. {mycli-2.5.0 → mycli-2.6.1}/.github/workflows/lint.yml +0 -0
  48. {mycli-2.5.0 → mycli-2.6.1}/.github/workflows/publish.yml +0 -0
  49. {mycli-2.5.0 → mycli-2.6.1}/.github/workflows/typecheck.yml +0 -0
  50. {mycli-2.5.0 → mycli-2.6.1}/.gitignore +0 -0
  51. {mycli-2.5.0 → mycli-2.6.1}/AGENTS.md +0 -0
  52. {mycli-2.5.0 → mycli-2.6.1}/AUTHORS.rst +0 -0
  53. {mycli-2.5.0 → mycli-2.6.1}/CONTRIBUTING.md +0 -0
  54. {mycli-2.5.0 → mycli-2.6.1}/LICENSE.txt +0 -0
  55. {mycli-2.5.0 → mycli-2.6.1}/MANIFEST.in +0 -0
  56. {mycli-2.5.0 → mycli-2.6.1}/SPONSORS.rst +0 -0
  57. {mycli-2.5.0 → mycli-2.6.1}/doc/key_bindings.rst +0 -0
  58. {mycli-2.5.0 → mycli-2.6.1}/doc/llm.md +0 -0
  59. {mycli-2.5.0 → mycli-2.6.1}/doc/screenshots/main.gif +0 -0
  60. {mycli-2.5.0 → mycli-2.6.1}/doc/screenshots/tables.png +0 -0
  61. {mycli-2.5.0 → mycli-2.6.1}/mycli/AUTHORS +0 -0
  62. {mycli-2.5.0 → mycli-2.6.1}/mycli/SPONSORS +0 -0
  63. {mycli-2.5.0 → mycli-2.6.1}/mycli/__init__.py +0 -0
  64. {mycli-2.5.0 → mycli-2.6.1}/mycli/clibuffer.py +0 -0
  65. {mycli-2.5.0 → mycli-2.6.1}/mycli/client_commands.py +0 -0
  66. {mycli-2.5.0 → mycli-2.6.1}/mycli/client_connection.py +0 -0
  67. {mycli-2.5.0 → mycli-2.6.1}/mycli/client_query.py +0 -0
  68. {mycli-2.5.0 → mycli-2.6.1}/mycli/clistyle.py +0 -0
  69. {mycli-2.5.0 → mycli-2.6.1}/mycli/clitoolbar.py +0 -0
  70. {mycli-2.5.0 → mycli-2.6.1}/mycli/completion_refresher.py +0 -0
  71. {mycli-2.5.0 → mycli-2.6.1}/mycli/config.py +0 -0
  72. {mycli-2.5.0 → mycli-2.6.1}/mycli/constants.py +0 -0
  73. {mycli-2.5.0 → mycli-2.6.1}/mycli/key_bindings.py +0 -0
  74. {mycli-2.5.0 → mycli-2.6.1}/mycli/lexer.py +0 -0
  75. {mycli-2.5.0 → mycli-2.6.1}/mycli/main.py +0 -0
  76. {mycli-2.5.0 → mycli-2.6.1}/mycli/main_modes/batch.py +0 -0
  77. {mycli-2.5.0 → mycli-2.6.1}/mycli/main_modes/checkup.py +0 -0
  78. {mycli-2.5.0 → mycli-2.6.1}/mycli/main_modes/execute.py +0 -0
  79. {mycli-2.5.0 → mycli-2.6.1}/mycli/main_modes/list_dsn.py +0 -0
  80. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/__init__.py +0 -0
  81. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/batch_utils.py +0 -0
  82. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/cli_utils.py +0 -0
  83. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/filepaths.py +0 -0
  84. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/interactive_utils.py +0 -0
  85. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/key_binding_utils.py +0 -0
  86. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/ptoolkit/__init__.py +0 -0
  87. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/ptoolkit/fzf.py +0 -0
  88. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/ptoolkit/history.py +0 -0
  89. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/ptoolkit/utils.py +0 -0
  90. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/__init__.py +0 -0
  91. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/dbcommands.py +0 -0
  92. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/delimitercommand.py +0 -0
  93. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/favoritequeries.py +0 -0
  94. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/iocommands.py +0 -0
  95. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/llm.py +0 -0
  96. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/main.py +0 -0
  97. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/special/utils.py +0 -0
  98. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/sql_utils.py +0 -0
  99. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/string_utils.py +0 -0
  100. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/tabular_output/__init__.py +0 -0
  101. {mycli-2.5.0 → mycli-2.6.1}/mycli/packages/tabular_output/sql_format.py +0 -0
  102. {mycli-2.5.0 → mycli-2.6.1}/mycli/password_sources.py +0 -0
  103. {mycli-2.5.0 → mycli-2.6.1}/mycli/schema_prefetcher.py +0 -0
  104. {mycli-2.5.0 → mycli-2.6.1}/mycli/sqlexecute.py +0 -0
  105. {mycli-2.5.0 → mycli-2.6.1}/mycli/ssh_tunnel.py +0 -0
  106. {mycli-2.5.0 → mycli-2.6.1}/mycli/vault.py +0 -0
  107. {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/dependency_links.txt +0 -0
  108. {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/entry_points.txt +0 -0
  109. {mycli-2.5.0 → mycli-2.6.1}/mycli.egg-info/top_level.txt +0 -0
  110. {mycli-2.5.0 → mycli-2.6.1}/setup.cfg +0 -0
  111. {mycli-2.5.0 → mycli-2.6.1}/test/__init__.py +0 -0
  112. {mycli-2.5.0 → mycli-2.6.1}/test/features/__init__.py +0 -0
  113. {mycli-2.5.0 → mycli-2.6.1}/test/features/auto_vertical.feature +0 -0
  114. {mycli-2.5.0 → mycli-2.6.1}/test/features/basic_commands.feature +0 -0
  115. {mycli-2.5.0 → mycli-2.6.1}/test/features/connection.feature +0 -0
  116. {mycli-2.5.0 → mycli-2.6.1}/test/features/crud_database.feature +0 -0
  117. {mycli-2.5.0 → mycli-2.6.1}/test/features/crud_table.feature +0 -0
  118. {mycli-2.5.0 → mycli-2.6.1}/test/features/db_utils.py +0 -0
  119. {mycli-2.5.0 → mycli-2.6.1}/test/features/environment.py +0 -0
  120. {mycli-2.5.0 → mycli-2.6.1}/test/features/fixture_data/help_commands.txt +0 -0
  121. {mycli-2.5.0 → mycli-2.6.1}/test/features/fixture_utils.py +0 -0
  122. {mycli-2.5.0 → mycli-2.6.1}/test/features/iocommands.feature +0 -0
  123. {mycli-2.5.0 → mycli-2.6.1}/test/features/named_queries.feature +0 -0
  124. {mycli-2.5.0 → mycli-2.6.1}/test/features/specials.feature +0 -0
  125. {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/__init__.py +0 -0
  126. {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/auto_vertical.py +0 -0
  127. {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/basic_commands.py +0 -0
  128. {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/connection.py +0 -0
  129. {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/crud_database.py +0 -0
  130. {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/crud_table.py +0 -0
  131. {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/iocommands.py +0 -0
  132. {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/named_queries.py +0 -0
  133. {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/specials.py +0 -0
  134. {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/utils.py +0 -0
  135. {mycli-2.5.0 → mycli-2.6.1}/test/features/steps/wrappers.py +0 -0
  136. {mycli-2.5.0 → mycli-2.6.1}/test/features/wrappager.py +0 -0
  137. {mycli-2.5.0 → mycli-2.6.1}/test/mylogin.cnf +0 -0
  138. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/conftest.py +0 -0
  139. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_batch_utils.py +0 -0
  140. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_checkup.py +0 -0
  141. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_cli_runner.py +0 -0
  142. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_cli_utils.py +0 -0
  143. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_clibuffer.py +0 -0
  144. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_client_commands.py +0 -0
  145. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_client_connection.py +0 -0
  146. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_client_query.py +0 -0
  147. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_clistyle.py +0 -0
  148. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_clitoolbar.py +0 -0
  149. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_completer_use_switch.py +0 -0
  150. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_completion_refresher.py +0 -0
  151. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_config.py +0 -0
  152. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_delimitercommand.py +0 -0
  153. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_dsn_aliases.py +0 -0
  154. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_favoritequeries.py +0 -0
  155. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_filepaths.py +0 -0
  156. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_interactive_utils.py +0 -0
  157. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_key_binding_utils.py +0 -0
  158. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_key_bindings.py +0 -0
  159. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_lexer.py +0 -0
  160. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_main.py +0 -0
  161. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_main_modes_batch.py +0 -0
  162. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_main_modes_execute.py +0 -0
  163. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_main_modes_list_dsn.py +0 -0
  164. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_naive_completion.py +0 -0
  165. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_password_sources.py +0 -0
  166. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_ptoolkit_fzf.py +0 -0
  167. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_ptoolkit_history.py +0 -0
  168. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_ptoolkit_utils.py +0 -0
  169. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_schema_prefetcher.py +0 -0
  170. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_dbcommands.py +0 -0
  171. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_init.py +0 -0
  172. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_iocommands.py +0 -0
  173. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_llm.py +0 -0
  174. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_main.py +0 -0
  175. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_special_utils.py +0 -0
  176. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_sql_utils.py +0 -0
  177. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_sqlexecute.py +0 -0
  178. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_sqlresult.py +0 -0
  179. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_ssh_tunnel.py +0 -0
  180. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_string_utils.py +0 -0
  181. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_tabular_output.py +0 -0
  182. {mycli-2.5.0 → mycli-2.6.1}/test/pytests/test_vault.py +0 -0
  183. {mycli-2.5.0 → mycli-2.6.1}/test/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.5.0
3
+ Version: 2.6.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
@@ -39,6 +39,7 @@ Requires-Dist: pip==26.*; extra == "llm"
39
39
  Provides-Extra: dataframe
40
40
  Requires-Dist: polars~=1.42.1; extra == "dataframe"
41
41
  Requires-Dist: altair~=6.2.2; extra == "dataframe"
42
+ Requires-Dist: vl-convert-python~=1.9.0; extra == "dataframe"
42
43
  Provides-Extra: all
43
44
  Requires-Dist: mycli[dataframe,llm]; extra == "all"
44
45
  Provides-Extra: dev
@@ -58,6 +59,7 @@ Requires-Dist: pip==26.*; extra == "dev"
58
59
  Requires-Dist: ruff~=0.15.0; extra == "dev"
59
60
  Requires-Dist: polars~=1.42.1; extra == "dev"
60
61
  Requires-Dist: altair~=6.2.2; extra == "dev"
62
+ Requires-Dist: vl-convert-python~=1.9.0; extra == "dev"
61
63
  Dynamic: license-file
62
64
 
63
65
  # mycli
@@ -67,10 +69,11 @@ Dynamic: license-file
67
69
  A command line client for MySQL with auto-completion and syntax highlighting.
68
70
 
69
71
  Homepage: [https://mycli.net](https://mycli.net)
72
+
70
73
  Documentation: [https://mycli.net/docs](https://mycli.net/docs)
71
74
 
72
- ![Completion](doc/screenshots/tables.png)
73
- ![CompletionGif](doc/screenshots/main.gif)
75
+ ![Completion](https://raw.githubusercontent.com/dbcli/mycli/main/doc/screenshots/tables.png)
76
+ ![CompletionGif](https://raw.githubusercontent.com/dbcli/mycli/main/doc/screenshots/main.gif)
74
77
 
75
78
  Mycli is compatible with MySQL, MariaDB, Percona, TiDB, and Apache Doris.
76
79
 
@@ -133,7 +136,7 @@ Features
133
136
  * Pretty print tabular data (with colors!).
134
137
  * Support for SSL connections
135
138
  * Shell-style trailing redirects with `$>`, `$>>` and `$|` operators.
136
- * [Polars](https://pola.rs) dataframe [transforms](doc/transforms.md) with `.|` and Parquet saves with `.>`.
139
+ * [Polars](https://pola.rs) dataframe [transforms and plots](doc/transforms.md) with `.|`, and Parquet saves with `.>`.
137
140
  * Support for querying LLMs with context derived from your schema using `/llm`.
138
141
  * Support for storing passwords in the system keyring.
139
142
 
@@ -5,10 +5,11 @@
5
5
  A command line client for MySQL with auto-completion and syntax highlighting.
6
6
 
7
7
  Homepage: [https://mycli.net](https://mycli.net)
8
+
8
9
  Documentation: [https://mycli.net/docs](https://mycli.net/docs)
9
10
 
10
- ![Completion](doc/screenshots/tables.png)
11
- ![CompletionGif](doc/screenshots/main.gif)
11
+ ![Completion](https://raw.githubusercontent.com/dbcli/mycli/main/doc/screenshots/tables.png)
12
+ ![CompletionGif](https://raw.githubusercontent.com/dbcli/mycli/main/doc/screenshots/main.gif)
12
13
 
13
14
  Mycli is compatible with MySQL, MariaDB, Percona, TiDB, and Apache Doris.
14
15
 
@@ -71,7 +72,7 @@ Features
71
72
  * Pretty print tabular data (with colors!).
72
73
  * Support for SSL connections
73
74
  * Shell-style trailing redirects with `$>`, `$>>` and `$|` operators.
74
- * [Polars](https://pola.rs) dataframe [transforms](doc/transforms.md) with `.|` and Parquet saves with `.>`.
75
+ * [Polars](https://pola.rs) dataframe [transforms and plots](doc/transforms.md) with `.|`, and Parquet saves with `.>`.
75
76
  * Support for querying LLMs with context derived from your schema using `/llm`.
76
77
  * Support for storing passwords in the system keyring.
77
78
 
@@ -1,3 +1,40 @@
1
+ 2.6.1 (2026/07/24)
2
+ ==============
3
+
4
+ Features
5
+ ---------
6
+ * Subcommand completions for the `/dsn` command.
7
+ * Allow file target of `$>` redirection to be quoted.
8
+ * Display of inline plots returned from `.|` operations.
9
+ * Don't attempt inline plots in the Windows console.
10
+ * Save plots, like parquets, with the `.>` operator.
11
+ * Display many native Python types returned from `.|` operations.
12
+
13
+
14
+ Bug Fixes
15
+ ---------
16
+ * Keep Vault username and password fields from being confused.
17
+ * Allow `\g` to terminate `.>` expressions.
18
+
19
+
20
+ Documentation
21
+ ---------
22
+ * `README.md` formatting.
23
+
24
+
25
+ Internal
26
+ ---------
27
+ * Sync test myclirc file closer to package myclirc file.
28
+
29
+
30
+ 2.5.1 (2026/07/22)
31
+ ==============
32
+
33
+ Documentation
34
+ ---------
35
+ * Attempt to make `README.md` images display on PyPi.
36
+
37
+
1
38
  2.5.0 (2026/07/22)
2
39
  ==============
3
40
 
@@ -0,0 +1,128 @@
1
+ # Transforms, Plots, and Parquets with Polars Dataframes
2
+
3
+ ## Installing
4
+
5
+ Install mycli with dataframe support using:
6
+
7
+ ```bash
8
+ pip install --upgrade 'mycli[dataframe]'
9
+ ```
10
+
11
+ or install these libraries separately:
12
+
13
+ * [`polars`](https://pypi.org/project/polars/)
14
+ * [`altair`](https://pypi.org/project/altair/)
15
+ * [`vl-convert-python`](https://pypi.org/project/vl-convert-python/)
16
+
17
+ ## BETA STATUS
18
+
19
+ Dataframe transforms and plots are new and experimental. The interface and
20
+ functionality may still change.
21
+
22
+ Here are some known limitations:
23
+
24
+ * transforms can't be composed with `$|` shell redirection
25
+ * composing multiple transform operations is not supported
26
+ * results from `UNION`s may be unable to be transformed
27
+
28
+ And there are inherent limitations to the post-processing model: the entire
29
+ SQL result must be transferred from the server and loaded into local memory.
30
+
31
+ ## Transforming
32
+
33
+ In the interactive REPL, append the `.|` operator and a Python expression to a
34
+ single SQL statement. The Python expression receives
35
+
36
+ * `df`, a Polars `DataFrame`
37
+ * `pl`, the Polars module
38
+ * `alt`, the Altair module
39
+
40
+ Spaces may be required around the `.|` operator.
41
+
42
+ Transform example:
43
+
44
+ ```sql
45
+ SELECT * FROM orders .| df.group_by('customer_id').len();
46
+ ```
47
+
48
+ which is equivalent to this native SQL:
49
+
50
+ ```sql
51
+ SELECT customer_id, COUNT(1) AS len FROM orders GROUP BY customer_id;
52
+ ```
53
+
54
+ Transform expressions run with normal Python privileges, and expressions
55
+ should not be run from untrusted sources.
56
+
57
+ If the transform operation returns a Polars `DataFrame` or `Series`, or a
58
+ Python type which can be rendered as a table, the result is rendered by mycli
59
+ as tabular output. `None` return values will be silently ignored, and other
60
+ return types will result in a warning message.
61
+
62
+ Transform expressions are useful for operations such as medians which
63
+ cannot be done (or are awkward) in SQL. Example:
64
+
65
+ ```sql
66
+ SELECT * FROM orders .| df.describe();
67
+ ```
68
+
69
+ ## Plotting
70
+
71
+ If the dataframe transform operation returns an Altair plot, the result can
72
+ be rendered as an inline PNG in many terminals.
73
+
74
+ Example:
75
+
76
+ ```sql
77
+ SELECT * FROM orders .| df['total'].plot.hist();
78
+ ```
79
+
80
+ <img src="https://raw.githubusercontent.com/dbcli/mycli/main/doc/screenshots/total_histogram.png" height=400>
81
+
82
+ Image size, display protocol, and other properties can be configured in
83
+ the `[dataframe]` section of `~/.myclirc`.
84
+
85
+ ## Saving
86
+
87
+ A query result, transformed `DataFrame`, or transformed `Series` can be
88
+ written directly to a Parquet file with the `.>` operator. An Altair plot can
89
+ also be written to a file with the same operator.
90
+
91
+ Save example:
92
+
93
+ ```sql
94
+ SELECT * FROM orders .> orders.parquet;
95
+ ```
96
+
97
+ The `.>` operator must be last, requires a `.parquet`, `.png`, `.pdf`, `.svg`,
98
+ or `.html` file extension on the destination, and overwrites any existing file.
99
+
100
+ Spaces may be required around the operator. Destination paths containing
101
+ whitespace must be quoted. A successful write reports its destination and row
102
+ count if appropriate.
103
+
104
+ When saving a plot, the format is deduced from the file extension.
105
+
106
+ When `post_redirect_command` is set in `~/.myclirc`, the given command runs
107
+ after a successful file save.
108
+
109
+ `.>` cannot be combined with the `\x` or `\G` special display terminators.
110
+
111
+ ## Combining
112
+
113
+ Parquet saves may be combined with dataframe transforms. Again, the save
114
+ operator `.>` must be the last operator.
115
+
116
+ Combined transform and save examples:
117
+
118
+ ```sql
119
+ SELECT * FROM orders .| df.group_by('customer_id').len() .> customer_counts.parquet;
120
+ ```
121
+
122
+ ```sql
123
+ SELECT * FROM orders .| df['order_id'] .> order_ids.parquet;
124
+ ```
125
+
126
+ ```sql
127
+ SELECT * FROM orders .| df['total'].plot.hist() .> total_histogram.png;
128
+ ```
@@ -296,8 +296,8 @@ customize password sources/precedence with "password_sources" in ~/.myclirc!
296
296
 
297
297
  redirect query output to a shell command with "$| <command>"!
298
298
 
299
- redirect query output to a file with "$> <filename>"!
299
+ redirect query output to a CSV file with "$> <filename>"!
300
300
 
301
- append query output to a file with "$>> <filename>"!
301
+ append query output to a CSV file with "$>> <filename>"!
302
302
 
303
303
  run a command after shell redirects with "post_redirect_command" in ~/.myclirc!
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
6
6
  from configobj import ConfigObj
7
7
 
8
8
  from mycli.config import strip_matching_quotes
9
+ from mycli.types import ImageProtocol
9
10
 
10
11
  if TYPE_CHECKING:
11
12
  from mycli.client import MyCli
@@ -46,6 +47,16 @@ def normalize_ssl_mode(
46
47
  return ssl_mode, error_notice
47
48
 
48
49
 
50
+ def normalize_image_protocol(image_protocol: str | None) -> tuple[ImageProtocol, str | None]:
51
+ if image_protocol == 'iterm2':
52
+ return 'iterm2', None
53
+ if image_protocol == 'kitty':
54
+ return 'kitty', None
55
+ if image_protocol in ('none', '', None):
56
+ return 'none', None
57
+ return 'none', f'Invalid config option provided for image_protocol ({image_protocol}); disabling.'
58
+
59
+
49
60
  def configure_prompt_state(
50
61
  mycli: MyCli,
51
62
  config: ConfigObj,
@@ -384,13 +384,13 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
384
384
  vault_config = mycli.config.get('vault_beta', {})
385
385
  vault_address = cli_args.vault_address or os.environ.get('VAULT_ADDR') or vault_config.get('address') or None
386
386
  vault_mount = cli_args.vault_mount or vault_config.get('default_mount') or None
387
- vault_field = cli_args.vault_password_field or vault_config.get('default_password_field') or DEFAULT_VAULT_PASSWORD_FIELD
387
+ vault_password_field = cli_args.vault_password_field or vault_config.get('default_password_field') or DEFAULT_VAULT_PASSWORD_FIELD
388
388
  vault_executable = vault_config.get('vault_executable') or DEFAULT_VAULT_EXECUTABLE
389
389
 
390
390
  def load_vault_password() -> str | None:
391
391
  try:
392
392
  return get_field_from_vault(
393
- vault_field,
393
+ vault_password_field,
394
394
  vault_secret,
395
395
  executable=vault_executable,
396
396
  mount=vault_mount,
@@ -406,11 +406,11 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
406
406
  vault_config = mycli.config.get('vault_beta', {})
407
407
  vault_address = cli_args.vault_address or os.environ.get('VAULT_ADDR') or vault_config.get('address') or None
408
408
  vault_mount = cli_args.vault_mount or vault_config.get('default_mount') or None
409
- vault_field = cli_args.vault_username_field or vault_config.get('default_username_field') or DEFAULT_VAULT_USERNAME_FIELD
409
+ vault_username_field = cli_args.vault_username_field or vault_config.get('default_username_field') or DEFAULT_VAULT_USERNAME_FIELD
410
410
  vault_executable = vault_config.get('vault_executable') or DEFAULT_VAULT_EXECUTABLE
411
411
  try:
412
412
  vault_username = get_field_from_vault(
413
- vault_field,
413
+ vault_username_field,
414
414
  cli_args.vault_secret,
415
415
  executable=vault_executable,
416
416
  mount=vault_mount,
@@ -17,6 +17,7 @@ from mycli.app_state import (
17
17
  configure_prompt_state,
18
18
  destructive_keywords_from_config,
19
19
  llm_prompt_truncation,
20
+ normalize_image_protocol,
20
21
  normalize_ssl_mode,
21
22
  )
22
23
  from mycli.client_commands import ClientCommandsMixin
@@ -145,6 +146,12 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
145
146
  self.null_string = c['main'].get('null_string')
146
147
  self.numeric_alignment = c['main'].get('numeric_alignment', 'right') or 'right'
147
148
  self.binary_display = c['main'].get('binary_display')
149
+ self.image_protocol, image_protocol_error = normalize_image_protocol(c['dataframe'].get('image_protocol'))
150
+ if image_protocol_error:
151
+ self.echo(image_protocol_error, err=True, fg='red')
152
+ self.plot_scale_factor = c['dataframe'].as_float('plot_scale_factor')
153
+ self.plot_ppi = c['dataframe'].as_int('plot_ppi')
154
+ self.plot_theme = c['dataframe'].get('plot_theme', 'carbong90') or 'carbong90'
148
155
  self.llm_prompt_field_truncate, self.llm_prompt_section_truncate = llm_prompt_truncation(c)
149
156
 
150
157
  self.ssl_mode, ssl_mode_error = normalize_ssl_mode(c, self.config_without_package_defaults)
@@ -0,0 +1,27 @@
1
+ """Platform and Python version compatibility support."""
2
+
3
+ from importlib import import_module
4
+ import sys
5
+
6
+ WIN: bool = sys.platform in ("win32", "cygwin")
7
+
8
+
9
+ def _is_win32() -> bool:
10
+ return sys.platform == 'win32'
11
+
12
+
13
+ def is_windows_console(output: object | None) -> bool:
14
+ """Return whether output uses a native Windows console backend."""
15
+ if not _is_win32() or output is None:
16
+ return False
17
+
18
+ output_types = tuple(
19
+ getattr(import_module(module_name), class_name)
20
+ for module_name, class_name in (
21
+ ('prompt_toolkit.output.conemu', 'ConEmuOutput'),
22
+ ('prompt_toolkit.output.win32', 'Win32Output'),
23
+ ('prompt_toolkit.output.windows10', 'Windows10_Output'),
24
+ )
25
+ )
26
+
27
+ return isinstance(output, output_types)
@@ -750,20 +750,35 @@ def _one_iteration(
750
750
  mycli.explorer_formatter.query = text
751
751
  if polars_transform is not None:
752
752
  assert polars_pipeline is not None
753
- if polars_pipeline.parquet_path is None:
754
- polars_result = run_polars_transform(polars_transform, results)
753
+ if polars_pipeline.output_path is None:
754
+ polars_result = run_polars_transform(
755
+ polars_transform,
756
+ results,
757
+ image_protocol=mycli.image_protocol,
758
+ plot_scale_factor=mycli.plot_scale_factor,
759
+ plot_ppi=mycli.plot_ppi,
760
+ plot_theme=mycli.plot_theme,
761
+ )
755
762
  else:
756
- polars_result = run_polars_transform(polars_transform, results, polars_pipeline.parquet_path)
757
- if polars_pipeline.parquet_path is None:
763
+ polars_result = run_polars_transform(
764
+ polars_transform,
765
+ results,
766
+ polars_pipeline.output_path,
767
+ image_protocol=mycli.image_protocol,
768
+ plot_scale_factor=mycli.plot_scale_factor,
769
+ plot_ppi=mycli.plot_ppi,
770
+ plot_theme=mycli.plot_theme,
771
+ )
772
+ if polars_pipeline.output_path is None:
758
773
  if polars_pipeline.output_mode == 'explorer':
759
774
  special.set_explorer_output(True)
760
775
  elif polars_pipeline.output_mode == 'expanded':
761
776
  special.set_expanded_output(True)
762
777
  _output_results(mycli, state, iter([polars_result]), start)
763
- if polars_pipeline.parquet_path is not None:
778
+ if polars_pipeline.output_path is not None:
764
779
  special.run_post_redirect_hook(
765
780
  mycli.post_redirect_command,
766
- polars_pipeline.parquet_path,
781
+ polars_pipeline.output_path,
767
782
  )
768
783
  else:
769
784
  _output_results(mycli, state, results, start)
@@ -265,6 +265,27 @@ format = mysql_unicode
265
265
  # Whether to remove the last line from the formatted output.
266
266
  trim_footer = False
267
267
 
268
+ [dataframe]
269
+
270
+ # How to display inline image results in a terminal emulator. Possible values:
271
+ # * kitty
272
+ # * iterm2
273
+ # empty to disable
274
+ image_protocol = kitty
275
+
276
+ # PNG plot resolution in pixels per inch. Must be a positive integer.
277
+ # The apparent size is a combination of plot_ppi and plot_scale_factor.
278
+ plot_ppi = 200
279
+
280
+ # PNG plot resolution multiplier. Must be a positive number.
281
+ # The apparent size is a combination of plot_ppi and plot_scale_factor.
282
+ plot_scale_factor = 1.0
283
+
284
+ # Altair theme for rendering plots. Examples: carbong90, dark, default.
285
+ # Available themes depend on the installed libraries. See
286
+ # https://github.com/vega/vega-themes/#included-themes
287
+ plot_theme = carbong90
288
+
268
289
  [search]
269
290
 
270
291
  # Whether to apply syntax highlighting to the preview window in fuzzy history
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  from datetime import datetime
4
5
  from decimal import Decimal
5
6
  from io import TextIOWrapper
@@ -28,7 +29,7 @@ from prompt_toolkit.styles.style import _MergedStyle
28
29
  from pygments.style import Style as PygmentsStyle
29
30
  from pymysql.cursors import Cursor
30
31
 
31
- from mycli.compat import WIN
32
+ from mycli.compat import WIN, is_windows_console
32
33
  from mycli.constants import DEFAULT_HEIGHT, DEFAULT_WIDTH
33
34
  import mycli.main_modes.repl as repl_mode
34
35
  from mycli.packages import special
@@ -115,6 +116,14 @@ class OutputMixin(MyCliState):
115
116
  is_warnings_style: bool = False,
116
117
  ) -> None:
117
118
  """Output text to stdout or a pager command."""
119
+ prompt_output = self.prompt_session.output if self.prompt_session is not None else None
120
+ if result.image is not None and not is_windows_console(prompt_output):
121
+ if result.image_protocol == 'iterm2':
122
+ click.secho('')
123
+ self.output_iterm2_image(result.image)
124
+ elif result.image_protocol == 'kitty':
125
+ click.secho('')
126
+ self.output_kitty_image(result.image)
118
127
  if output:
119
128
  if self.prompt_session is not None:
120
129
  size = self.prompt_session.output.get_size()
@@ -186,6 +195,22 @@ class OutputMixin(MyCliState):
186
195
  styled_status = to_formatted_text(status, style=add_style)
187
196
  prompt_toolkit.print_formatted_text(styled_status, style=self.ptoolkit_style)
188
197
 
198
+ def output_iterm2_image(self, image: bytes) -> None:
199
+ """Emit a PNG using the iTerm2 inline image protocol."""
200
+ filename = base64.b64encode(b'chart.png').decode('ascii')
201
+ contents = base64.b64encode(image).decode('ascii')
202
+ click.echo(f'\x1b]1337;File=name={filename};size={len(image)};width=auto;height=auto;preserveAspectRatio=1;inline=1:{contents}\x07')
203
+
204
+ def output_kitty_image(self, image: bytes) -> None:
205
+ """Emit a PNG using Kitty's direct graphics protocol."""
206
+ contents = base64.b64encode(image).decode('ascii')
207
+ chunks = [contents[index : index + 4096] for index in range(0, len(contents), 4096)] or ['']
208
+ for index, chunk in enumerate(chunks):
209
+ action = 'a=T,f=100,' if index == 0 else ''
210
+ more = 1 if index < len(chunks) - 1 else 0
211
+ click.echo(f'\x1b_G{action}m={more};{chunk}\x1b\\', nl=False)
212
+ click.echo()
213
+
189
214
  def configure_pager(self) -> None:
190
215
  if not os.environ.get("LESS"):
191
216
  os.environ["LESS"] = "-RXF"
@@ -6,6 +6,7 @@ from typing import Any, Callable, Literal
6
6
  import sqlparse
7
7
  from sqlparse.sql import Comparison, Identifier, Token, Where
8
8
 
9
+ from mycli.packages.special.dsn_aliases import DSN_SUBCOMMANDS
9
10
  from mycli.packages.special.main import COMMANDS as SPECIAL_COMMANDS
10
11
  from mycli.packages.special.main import parse_special_command
11
12
  from mycli.packages.sql_utils import extract_tables, find_prev_keyword, last_word
@@ -816,6 +817,17 @@ def suggest_special(text: str) -> list[dict[str, Any]]:
816
817
  if cmd in ["\\llm", "/llm", "\\ai", "/ai"]:
817
818
  return [{"type": "llm"}]
818
819
 
820
+ if cmd.lower() in (r'\dsn', '/dsn'):
821
+ dsn_arguments = _arg.split(maxsplit=1)
822
+ completing_delete_target = (len(dsn_arguments) == 1 and text[-1].isspace()) or (len(dsn_arguments) == 2 and not text[-1].isspace())
823
+ if dsn_arguments and dsn_arguments[0].lower() == 'delete' and completing_delete_target:
824
+ return [{'type': 'dsn_alias'}]
825
+ if dsn_arguments and dsn_arguments[0].lower() == 'delete' and len(dsn_arguments) == 2:
826
+ return []
827
+ if dsn_arguments and dsn_arguments[0].lower() in DSN_SUBCOMMANDS - {'delete'}:
828
+ return []
829
+ return [{'type': 'special_subcommand', 'subcommands': list(DSN_SUBCOMMANDS)}]
830
+
819
831
  return [{"type": "keyword"}, {"type": "special"}]
820
832
 
821
833
 
@@ -1,5 +1,6 @@
1
1
  import functools
2
2
  import logging
3
+ import shlex
3
4
 
4
5
  import sqlglot
5
6
 
@@ -113,9 +114,6 @@ def invalid_shell_part(
113
114
  file_part: str | None,
114
115
  command_part: str | None,
115
116
  ) -> bool:
116
- if file_part and ' ' in file_part:
117
- return True
118
-
119
117
  if file_part and '>' in file_part:
120
118
  return True
121
119
 
@@ -125,6 +123,19 @@ def invalid_shell_part(
125
123
  return False
126
124
 
127
125
 
126
+ def parse_redirect_filename(file_part: str | None) -> str | None:
127
+ """Return one unquoted redirect filename, or None for an invalid operand."""
128
+ if file_part is None or not file_part:
129
+ return None
130
+ if file_part[0] not in ('\'', '"'):
131
+ return file_part if not any(character.isspace() for character in file_part) else None
132
+ try:
133
+ parts = shlex.split(file_part)
134
+ except ValueError:
135
+ return None
136
+ return parts[0] if len(parts) == 1 else None
137
+
138
+
128
139
  # todo there are still corner cases combining custom delimiters, caching, and redirection
129
140
  @functools.lru_cache(maxsize=1)
130
141
  def get_redirect_components(command: str) -> tuple[str | None, str | None, str | None, str | None]:
@@ -174,7 +185,9 @@ def get_redirect_components(command: str) -> tuple[str | None, str | None, str |
174
185
  )
175
186
 
176
187
  if file_part_tokens:
177
- file_part = assemble_tokens(file_part_tokens)
188
+ file_part = parse_redirect_filename(assemble_tokens(file_part_tokens))
189
+ if file_part is None:
190
+ return None, None, None, None
178
191
  else:
179
192
  file_part = None
180
193