mycli 2.3.0__tar.gz → 2.4.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 (176) hide show
  1. {mycli-2.3.0 → mycli-2.4.0}/AGENTS.md +2 -0
  2. {mycli-2.3.0/mycli.egg-info → mycli-2.4.0}/PKG-INFO +1 -1
  3. {mycli-2.3.0 → mycli-2.4.0}/changelog.md +21 -0
  4. {mycli-2.3.0 → mycli-2.4.0}/mycli/TIPS +2 -0
  5. {mycli-2.3.0 → mycli-2.4.0}/mycli/cli_runner.py +37 -19
  6. {mycli-2.3.0 → mycli-2.4.0}/mycli/client.py +8 -2
  7. {mycli-2.3.0 → mycli-2.4.0}/mycli/client_connection.py +24 -21
  8. {mycli-2.3.0 → mycli-2.4.0}/mycli/clitoolbar.py +13 -13
  9. {mycli-2.3.0 → mycli-2.4.0}/mycli/main.py +0 -9
  10. {mycli-2.3.0 → mycli-2.4.0}/mycli/main_modes/repl.py +9 -1
  11. {mycli-2.3.0 → mycli-2.4.0}/mycli/myclirc +16 -0
  12. mycli-2.4.0/mycli/password_sources.py +77 -0
  13. {mycli-2.3.0 → mycli-2.4.0/mycli.egg-info}/PKG-INFO +1 -1
  14. {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/SOURCES.txt +2 -0
  15. {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/scm_file_list.json +2 -0
  16. mycli-2.4.0/mycli.egg-info/scm_version.json +8 -0
  17. {mycli-2.3.0 → mycli-2.4.0}/test/myclirc +16 -0
  18. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_cli_runner.py +94 -9
  19. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_client.py +32 -0
  20. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_client_connection.py +110 -49
  21. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_clitoolbar.py +3 -3
  22. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_main.py +33 -22
  23. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_main_modes_repl.py +24 -0
  24. mycli-2.4.0/test/pytests/test_password_sources.py +81 -0
  25. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_iocommands.py +4 -2
  26. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_tabular_output.py +4 -1
  27. mycli-2.3.0/mycli.egg-info/scm_version.json +0 -8
  28. {mycli-2.3.0 → mycli-2.4.0}/.git-blame-ignore-revs +0 -0
  29. {mycli-2.3.0 → mycli-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  30. {mycli-2.3.0 → mycli-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  31. {mycli-2.3.0 → mycli-2.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  32. {mycli-2.3.0 → mycli-2.4.0}/.github/dependabot.yml +0 -0
  33. {mycli-2.3.0 → mycli-2.4.0}/.github/workflows/ci.yml +0 -0
  34. {mycli-2.3.0 → mycli-2.4.0}/.github/workflows/lint.yml +0 -0
  35. {mycli-2.3.0 → mycli-2.4.0}/.github/workflows/publish.yml +0 -0
  36. {mycli-2.3.0 → mycli-2.4.0}/.github/workflows/typecheck.yml +0 -0
  37. {mycli-2.3.0 → mycli-2.4.0}/.gitignore +0 -0
  38. {mycli-2.3.0 → mycli-2.4.0}/AUTHORS.rst +0 -0
  39. {mycli-2.3.0 → mycli-2.4.0}/CONTRIBUTING.md +0 -0
  40. {mycli-2.3.0 → mycli-2.4.0}/LICENSE.txt +0 -0
  41. {mycli-2.3.0 → mycli-2.4.0}/MANIFEST.in +0 -0
  42. {mycli-2.3.0 → mycli-2.4.0}/README.md +0 -0
  43. {mycli-2.3.0 → mycli-2.4.0}/SPONSORS.rst +0 -0
  44. {mycli-2.3.0 → mycli-2.4.0}/doc/key_bindings.rst +0 -0
  45. {mycli-2.3.0 → mycli-2.4.0}/doc/llm.md +0 -0
  46. {mycli-2.3.0 → mycli-2.4.0}/doc/screenshots/main.gif +0 -0
  47. {mycli-2.3.0 → mycli-2.4.0}/doc/screenshots/tables.png +0 -0
  48. {mycli-2.3.0 → mycli-2.4.0}/mycli/AUTHORS +0 -0
  49. {mycli-2.3.0 → mycli-2.4.0}/mycli/SPONSORS +0 -0
  50. {mycli-2.3.0 → mycli-2.4.0}/mycli/__init__.py +0 -0
  51. {mycli-2.3.0 → mycli-2.4.0}/mycli/app_state.py +0 -0
  52. {mycli-2.3.0 → mycli-2.4.0}/mycli/clibuffer.py +0 -0
  53. {mycli-2.3.0 → mycli-2.4.0}/mycli/client_commands.py +0 -0
  54. {mycli-2.3.0 → mycli-2.4.0}/mycli/client_query.py +0 -0
  55. {mycli-2.3.0 → mycli-2.4.0}/mycli/clistyle.py +0 -0
  56. {mycli-2.3.0 → mycli-2.4.0}/mycli/compat.py +0 -0
  57. {mycli-2.3.0 → mycli-2.4.0}/mycli/completion_refresher.py +0 -0
  58. {mycli-2.3.0 → mycli-2.4.0}/mycli/config.py +0 -0
  59. {mycli-2.3.0 → mycli-2.4.0}/mycli/constants.py +0 -0
  60. {mycli-2.3.0 → mycli-2.4.0}/mycli/key_bindings.py +0 -0
  61. {mycli-2.3.0 → mycli-2.4.0}/mycli/lexer.py +0 -0
  62. {mycli-2.3.0 → mycli-2.4.0}/mycli/main_modes/batch.py +0 -0
  63. {mycli-2.3.0 → mycli-2.4.0}/mycli/main_modes/checkup.py +0 -0
  64. {mycli-2.3.0 → mycli-2.4.0}/mycli/main_modes/execute.py +0 -0
  65. {mycli-2.3.0 → mycli-2.4.0}/mycli/main_modes/list_dsn.py +0 -0
  66. {mycli-2.3.0 → mycli-2.4.0}/mycli/output.py +0 -0
  67. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/__init__.py +0 -0
  68. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/batch_utils.py +0 -0
  69. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/cli_utils.py +0 -0
  70. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/completion_engine.py +0 -0
  71. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/filepaths.py +0 -0
  72. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/hybrid_redirection.py +0 -0
  73. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/interactive_utils.py +0 -0
  74. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/key_binding_utils.py +0 -0
  75. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/ptoolkit/__init__.py +0 -0
  76. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/ptoolkit/fzf.py +0 -0
  77. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/ptoolkit/history.py +0 -0
  78. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/ptoolkit/utils.py +0 -0
  79. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/__init__.py +0 -0
  80. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/dbcommands.py +0 -0
  81. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/delimitercommand.py +0 -0
  82. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/dsn_aliases.py +0 -0
  83. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/favoritequeries.py +0 -0
  84. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/iocommands.py +0 -0
  85. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/llm.py +0 -0
  86. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/main.py +0 -0
  87. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/utils.py +0 -0
  88. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/sql_utils.py +0 -0
  89. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/sqlresult.py +0 -0
  90. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/string_utils.py +0 -0
  91. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/tabular_output/__init__.py +0 -0
  92. {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/tabular_output/sql_format.py +0 -0
  93. {mycli-2.3.0 → mycli-2.4.0}/mycli/schema_prefetcher.py +0 -0
  94. {mycli-2.3.0 → mycli-2.4.0}/mycli/sqlcompleter.py +0 -0
  95. {mycli-2.3.0 → mycli-2.4.0}/mycli/sqlexecute.py +0 -0
  96. {mycli-2.3.0 → mycli-2.4.0}/mycli/ssh_tunnel.py +0 -0
  97. {mycli-2.3.0 → mycli-2.4.0}/mycli/types.py +0 -0
  98. {mycli-2.3.0 → mycli-2.4.0}/mycli/vault.py +0 -0
  99. {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/dependency_links.txt +0 -0
  100. {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/entry_points.txt +0 -0
  101. {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/requires.txt +0 -0
  102. {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/top_level.txt +0 -0
  103. {mycli-2.3.0 → mycli-2.4.0}/pyproject.toml +0 -0
  104. {mycli-2.3.0 → mycli-2.4.0}/setup.cfg +0 -0
  105. {mycli-2.3.0 → mycli-2.4.0}/test/__init__.py +0 -0
  106. {mycli-2.3.0 → mycli-2.4.0}/test/features/__init__.py +0 -0
  107. {mycli-2.3.0 → mycli-2.4.0}/test/features/auto_vertical.feature +0 -0
  108. {mycli-2.3.0 → mycli-2.4.0}/test/features/basic_commands.feature +0 -0
  109. {mycli-2.3.0 → mycli-2.4.0}/test/features/connection.feature +0 -0
  110. {mycli-2.3.0 → mycli-2.4.0}/test/features/crud_database.feature +0 -0
  111. {mycli-2.3.0 → mycli-2.4.0}/test/features/crud_table.feature +0 -0
  112. {mycli-2.3.0 → mycli-2.4.0}/test/features/db_utils.py +0 -0
  113. {mycli-2.3.0 → mycli-2.4.0}/test/features/environment.py +0 -0
  114. {mycli-2.3.0 → mycli-2.4.0}/test/features/fixture_data/help_commands.txt +0 -0
  115. {mycli-2.3.0 → mycli-2.4.0}/test/features/fixture_utils.py +0 -0
  116. {mycli-2.3.0 → mycli-2.4.0}/test/features/iocommands.feature +0 -0
  117. {mycli-2.3.0 → mycli-2.4.0}/test/features/named_queries.feature +0 -0
  118. {mycli-2.3.0 → mycli-2.4.0}/test/features/specials.feature +0 -0
  119. {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/__init__.py +0 -0
  120. {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/auto_vertical.py +0 -0
  121. {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/basic_commands.py +0 -0
  122. {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/connection.py +0 -0
  123. {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/crud_database.py +0 -0
  124. {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/crud_table.py +0 -0
  125. {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/iocommands.py +0 -0
  126. {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/named_queries.py +0 -0
  127. {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/specials.py +0 -0
  128. {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/utils.py +0 -0
  129. {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/wrappers.py +0 -0
  130. {mycli-2.3.0 → mycli-2.4.0}/test/features/wrappager.py +0 -0
  131. {mycli-2.3.0 → mycli-2.4.0}/test/mylogin.cnf +0 -0
  132. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/conftest.py +0 -0
  133. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_app_state.py +0 -0
  134. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_batch_utils.py +0 -0
  135. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_checkup.py +0 -0
  136. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_cli_utils.py +0 -0
  137. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_clibuffer.py +0 -0
  138. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_client_commands.py +0 -0
  139. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_client_query.py +0 -0
  140. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_clistyle.py +0 -0
  141. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_completer_use_switch.py +0 -0
  142. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_completion_engine.py +0 -0
  143. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_completion_refresher.py +0 -0
  144. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_config.py +0 -0
  145. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_delimitercommand.py +0 -0
  146. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_dsn_aliases.py +0 -0
  147. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_favoritequeries.py +0 -0
  148. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_filepaths.py +0 -0
  149. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_hybrid_redirection.py +0 -0
  150. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_interactive_utils.py +0 -0
  151. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_key_binding_utils.py +0 -0
  152. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_key_bindings.py +0 -0
  153. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_lexer.py +0 -0
  154. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_main_modes_batch.py +0 -0
  155. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_main_modes_execute.py +0 -0
  156. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
  157. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_naive_completion.py +0 -0
  158. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_output.py +0 -0
  159. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
  160. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_ptoolkit_history.py +0 -0
  161. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_ptoolkit_utils.py +0 -0
  162. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_schema_prefetcher.py +0 -0
  163. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
  164. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_dbcommands.py +0 -0
  165. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_init.py +0 -0
  166. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_llm.py +0 -0
  167. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_main.py +0 -0
  168. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_utils.py +0 -0
  169. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_sql_utils.py +0 -0
  170. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_sqlcompleter.py +0 -0
  171. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_sqlexecute.py +0 -0
  172. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_sqlresult.py +0 -0
  173. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_ssh_tunnel.py +0 -0
  174. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_string_utils.py +0 -0
  175. {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_vault.py +0 -0
  176. {mycli-2.3.0 → mycli-2.4.0}/test/utils.py +0 -0
@@ -48,6 +48,8 @@ A command line client for MySQL with auto-completion and syntax highlighting.
48
48
  ├── mycli/packages/sqlresult.py # the `SQLResult` dataclass for holding responses
49
49
  ├── mycli/packages/string_utils.py # generic string utilities
50
50
  ├── mycli/packages/tabular_output/ # extends cli_helper with additional output formats
51
+ ├── mycli/password_sources.py # password sources and precedence
52
+ ├── mycli/ssh_tunnel.py # connecting over a tunnel with `--ssh-jump`
51
53
  ├── mycli/schema_prefetcher.py # background prefetcher for multi-schema auto-completion
52
54
  ├── mycli/sqlcompleter.py # offers SQL completions
53
55
  ├── mycli/sqlexecute.py # runs SQL queries
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: CLI for MySQL Database. With auto-completion and syntax highlighting.
5
5
  Author: Mycli Core Team
6
6
  License-Expression: BSD-3-Clause
@@ -1,3 +1,24 @@
1
+ 2.4.0 (2026/07/18)
2
+ ==============
3
+
4
+ Features
5
+ ---------
6
+ * Make password sources and precedence configurable.
7
+ * Ability to deselect fallback interactive password prompt.
8
+ * Add `\e` prompt/toolbar format string for edit mode.
9
+
10
+
11
+ Bug Fixes
12
+ ---------
13
+ * Don't create `~/.myclirc` if the user adopted the XDG layout for myclirc.
14
+
15
+
16
+ Internal
17
+ ---------
18
+ * Make flaky `/watch` test even more lenient for CI.
19
+ * Add missing file to `AGENTS.md` project layout.
20
+
21
+
1
22
  2.3.0 (2026/07/16)
2
23
  ==============
3
24
 
@@ -288,6 +288,8 @@ set editor_command = "code --wait" in ~/.myclirc to edit queries using VS Code!
288
288
 
289
289
  set up an interactive output explorer using the "[explorer]" section of ~/.myclirc!
290
290
 
291
+ customize password sources/precedence with "password_sources" in ~/.myclirc!
292
+
291
293
  ###
292
294
  ### redirection
293
295
  ###
@@ -15,6 +15,7 @@ from mycli.main_modes.checkup import main_checkup
15
15
  from mycli.main_modes.execute import main_execute_from_cli
16
16
  from mycli.main_modes.list_dsn import main_list_dsn
17
17
  from mycli.packages.cli_utils import is_valid_connection_scheme
18
+ from mycli.password_sources import PasswordCandidates
18
19
  from mycli.vault import (
19
20
  DEFAULT_VAULT_EXECUTABLE,
20
21
  DEFAULT_VAULT_PASSWORD_FIELD,
@@ -165,10 +166,15 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
165
166
  database = cli_args.dbname or cli_args.database
166
167
 
167
168
  dsn_uri = None
169
+ dsn_password: str | None = None
168
170
 
169
171
  # Treat the database argument as a DSN alias only if it matches a configured alias
170
172
  # todo why is port tested but not socket?
171
- truthy_password = cli_args.password not in (None, EMPTY_PASSWORD_FLAG_SENTINEL)
173
+ truthy_password = (
174
+ cli_args.password not in (None, EMPTY_PASSWORD_FLAG_SENTINEL)
175
+ or cli_args.password_file is not None
176
+ or os.environ.get('MYSQL_PWD') is not None
177
+ )
172
178
  if (
173
179
  database
174
180
  and "://" not in database
@@ -235,9 +241,6 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
235
241
  database = dsn_database
236
242
  if not cli_args.user and dsn_user is not None:
237
243
  cli_args.user = dsn_user
238
- # todo: rationalize the behavior of empty-string passwords here
239
- if not cli_args.password and dsn_password is not None:
240
- cli_args.password = dsn_password
241
244
  if not cli_args.host:
242
245
  cli_args.host = dsn_host
243
246
  if not cli_args.port:
@@ -364,25 +367,40 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
364
367
  use_keyring = str_to_bool(cli_args.use_keyring)
365
368
  reset_keyring = False
366
369
 
367
- if cli_args.password is None and cli_args.vault_secret:
370
+ password_candidates = PasswordCandidates()
371
+ if cli_args.password == EMPTY_PASSWORD_FLAG_SENTINEL:
372
+ password_candidates.add_value('prompt', cli_args.password)
373
+ elif cli_args.password is not None:
374
+ password_candidates.add_value('literal', cli_args.password)
375
+ if cli_args.password_file:
376
+ password_candidates.add_loader('file', lambda: main_module.get_password_from_file(cli_args.password_file))
377
+ if os.environ.get('MYSQL_PWD') is not None:
378
+ password_candidates.add_value('environment', os.environ.get('MYSQL_PWD'))
379
+ if dsn_password is not None:
380
+ password_candidates.add_value('dsn', dsn_password)
381
+
382
+ if cli_args.vault_secret:
383
+ vault_secret = cli_args.vault_secret
368
384
  vault_config = mycli.config.get('vault_beta', {})
369
385
  vault_address = cli_args.vault_address or os.environ.get('VAULT_ADDR') or vault_config.get('address') or None
370
386
  vault_mount = cli_args.vault_mount or vault_config.get('default_mount') or None
371
387
  vault_field = cli_args.vault_password_field or vault_config.get('default_password_field') or DEFAULT_VAULT_PASSWORD_FIELD
372
388
  vault_executable = vault_config.get('vault_executable') or DEFAULT_VAULT_EXECUTABLE
373
- try:
374
- vault_password: str | None = get_field_from_vault(
375
- vault_field,
376
- cli_args.vault_secret,
377
- executable=vault_executable,
378
- mount=vault_mount,
379
- address=vault_address,
380
- )
381
- except VaultError as exc:
382
- click.secho(f'Error reading password from Vault: {exc}', err=True, fg='red')
383
- sys.exit(1)
384
- else:
385
- vault_password = None
389
+
390
+ def load_vault_password() -> str | None:
391
+ try:
392
+ return get_field_from_vault(
393
+ vault_field,
394
+ vault_secret,
395
+ executable=vault_executable,
396
+ mount=vault_mount,
397
+ address=vault_address,
398
+ )
399
+ except VaultError as exc:
400
+ click.secho(f'Error reading password from Vault: {exc}', err=True, fg='red')
401
+ sys.exit(1)
402
+
403
+ password_candidates.add_loader('vault', load_vault_password)
386
404
 
387
405
  if cli_args.user is None and cli_args.vault_secret:
388
406
  vault_config = mycli.config.get('vault_beta', {})
@@ -409,7 +427,7 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
409
427
  mycli.connect(
410
428
  database=database,
411
429
  user=vault_username if cli_args.user is None else cli_args.user,
412
- passwd=vault_password if cli_args.password is None else cli_args.password,
430
+ password_candidates=password_candidates,
413
431
  host=cli_args.host,
414
432
  port=cli_args.port,
415
433
  socket=cli_args.socket,
@@ -58,9 +58,9 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
58
58
 
59
59
  # check XDG_CONFIG_HOME exists and not an empty string
60
60
  xdg_config_home = os.environ.get("XDG_CONFIG_HOME", "~/.config")
61
+ xdg_config_file = os.path.join(os.path.expanduser(xdg_config_home), "mycli", "myclirc")
61
62
  system_config_files: list[str | IO[str]] = [
62
63
  "/etc/myclirc",
63
- os.path.join(os.path.expanduser(xdg_config_home), "mycli", "myclirc"),
64
64
  ]
65
65
 
66
66
  def __init__(
@@ -72,7 +72,7 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
72
72
  login_path: str | None = None,
73
73
  auto_vertical_output: bool = False,
74
74
  warn: bool | None = None,
75
- myclirc: str = "~/.myclirc",
75
+ myclirc: str | None = None,
76
76
  show_warnings: bool | None = None,
77
77
  cli_verbosity: int = 0,
78
78
  ) -> None:
@@ -87,6 +87,12 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix
87
87
  self.sandbox_mode: bool = False
88
88
  self.checkpoint: IO | None = None
89
89
 
90
+ if myclirc is None:
91
+ if os.path.exists(self.xdg_config_file):
92
+ myclirc = self.xdg_config_file
93
+ else:
94
+ myclirc = '~/.myclirc'
95
+
90
96
  # Load config.
91
97
  config_files: list[str | IO[str]] = self.system_config_files + [myclirc]
92
98
 
@@ -23,6 +23,7 @@ from mycli.constants import (
23
23
  )
24
24
  from mycli.packages.filepaths import guess_socket_location
25
25
  from mycli.packages.special.utils import format_connection_dsn
26
+ from mycli.password_sources import PasswordCandidates
26
27
  from mycli.sqlexecute import SQLExecute
27
28
  from mycli.ssh_tunnel import SshTunnel, SshTunnelError
28
29
 
@@ -49,7 +50,7 @@ class ClientConnectionMixin:
49
50
  self,
50
51
  database: str | None = "",
51
52
  user: str | None = "",
52
- passwd: str | int | None = None,
53
+ password_candidates: PasswordCandidates | None = None,
53
54
  host: str | None = "",
54
55
  port: str | int | None = "",
55
56
  socket: str | None = "",
@@ -86,6 +87,11 @@ class ClientConnectionMixin:
86
87
  if not host or host == DEFAULT_HOST:
87
88
  socket = socket or user_connection_config.get("default_socket") or mylogin_cnf["socket"] or guess_socket_location()
88
89
 
90
+ if self.config.get('main', {}).get('password_sources'):
91
+ password_source_precedence = self.config.get('main').as_list('password_sources')
92
+ else:
93
+ password_source_precedence = ['prompt']
94
+
89
95
  if ssh_jump:
90
96
  remote_host = host or DEFAULT_HOST
91
97
  remote_port = int_port or DEFAULT_PORT
@@ -114,7 +120,8 @@ class ClientConnectionMixin:
114
120
  pass
115
121
  sys.exit(1)
116
122
 
117
- passwd = passwd if isinstance(passwd, (str, int)) else mylogin_cnf["password"]
123
+ if password_candidates is None:
124
+ password_candidates = PasswordCandidates()
118
125
 
119
126
  if not character_set:
120
127
  if 'main' in self.config_without_package_defaults and 'default_character_set' in self.config_without_package_defaults['main']:
@@ -167,15 +174,6 @@ class ClientConnectionMixin:
167
174
  if not any(v for v in ssl_config.values()):
168
175
  ssl_config = {}
169
176
 
170
- # password hierarchy
171
- # 1. -p / --pass/--password CLI options
172
- # 2. --password-file CLI option
173
- # 3. envvar (MYSQL_PWD)
174
- # 4. DSN (mysql://user:password)
175
- # 5. Vault
176
- # 6. .mylogin.cnf
177
- # 7. keyring
178
-
179
177
  ssh_tunnel_field = urlquote(ssh_jump or '')
180
178
  if ssh_tunnel_field:
181
179
  ssh_tunnel_field = ':' + ssh_tunnel_field
@@ -183,15 +181,17 @@ class ClientConnectionMixin:
183
181
  keyring_domain = 'mycli.net'
184
182
  keyring_retrieved_cleanly = False
185
183
 
186
- if passwd is None and use_keyring and not reset_keyring:
187
- passwd = keyring.get_password(keyring_domain, keyring_identifier)
188
- if passwd is not None:
189
- keyring_retrieved_cleanly = True
184
+ password_candidates.add_value('login_path', mylogin_cnf['password'])
185
+ if use_keyring and not reset_keyring:
186
+ password_candidates.add_loader('keyring', lambda: keyring.get_password(keyring_domain, keyring_identifier))
187
+
188
+ selected_password = password_candidates.resolve(password_source_precedence)
189
+ passwd = selected_password.value if selected_password is not None else None
190
+ keyring_retrieved_cleanly = selected_password is not None and selected_password.source == 'keyring'
190
191
 
191
192
  # prompt for password if requested by user
192
193
  if passwd == EMPTY_PASSWORD_FLAG_SENTINEL:
193
194
  passwd = click.prompt(f"Enter password for {user}", hide_input=True, show_default=False, default='', type=str, err=True)
194
- keyring_retrieved_cleanly = False
195
195
 
196
196
  # should not fail, but will help the typechecker
197
197
  assert not isinstance(passwd, int)
@@ -285,15 +285,18 @@ class ClientConnectionMixin:
285
285
  _connect(
286
286
  retry_ssl=True, keyring_retrieved_cleanly=keyring_retrieved_cleanly, keyring_save_eligible=keyring_save_eligible
287
287
  )
288
- elif e1.args[0] == ACCESS_DENIED_ERROR and connection_info["password"] is None:
288
+ elif e1.args[0] == ACCESS_DENIED_ERROR and connection_info["password"] is None and 'fallback' in password_source_precedence:
289
289
  # if we already tried and failed to connect with a new password, raise the error
290
290
  if retry_password:
291
291
  raise e1
292
- # ask the user for a new password and try to connect again
293
- new_password = click.prompt(
294
- f"Enter password for {user}", hide_input=True, show_default=False, default='', type=str, err=True
292
+ password_candidates.add_loader(
293
+ 'fallback',
294
+ lambda: click.prompt(
295
+ f"Enter password for {user}", hide_input=True, show_default=False, default='', type=str, err=True
296
+ ),
295
297
  )
296
- connection_info["password"] = new_password
298
+ fallback_password = password_candidates.resolve(['fallback'])
299
+ connection_info["password"] = fallback_password.value if fallback_password is not None else None
297
300
  keyring_retrieved_cleanly = False
298
301
  _connect(
299
302
  retry_password=True,
@@ -8,6 +8,17 @@ from prompt_toolkit.key_binding.vi_state import InputMode
8
8
  from mycli.packages import special
9
9
 
10
10
 
11
+ def get_vi_mode() -> str:
12
+ """Get the current vi mode for display."""
13
+ return {
14
+ InputMode.INSERT: "I",
15
+ InputMode.NAVIGATION: "N",
16
+ InputMode.REPLACE: "R",
17
+ InputMode.REPLACE_SINGLE: "R",
18
+ InputMode.INSERT_MULTIPLE: "M",
19
+ }[get_app().vi_state.input_mode]
20
+
21
+
11
22
  def create_toolbar_tokens_func(
12
23
  mycli,
13
24
  show_initial_toolbar_help: Callable[[], bool],
@@ -45,8 +56,8 @@ def create_toolbar_tokens_func(
45
56
 
46
57
  if mycli.prompt_session.editing_mode == EditingMode.VI:
47
58
  result.append(divider)
48
- result.append(("class:bottom-toolbar", "Vi:"))
49
- result.append(("class:bottom-toolbar.on", _get_vi_mode()))
59
+ result.append(("class:bottom-toolbar", "vi:"))
60
+ result.append(("class:bottom-toolbar.on", get_vi_mode()))
50
61
 
51
62
  if mycli.toolbar_error_message:
52
63
  dynamic.append(divider)
@@ -92,14 +103,3 @@ def create_toolbar_tokens_func(
92
103
  return result
93
104
 
94
105
  return get_toolbar_tokens
95
-
96
-
97
- def _get_vi_mode() -> str:
98
- """Get the current vi mode for display."""
99
- return {
100
- InputMode.INSERT: "I",
101
- InputMode.NAVIGATION: "N",
102
- InputMode.REPLACE: "R",
103
- InputMode.REPLACE_SINGLE: "R",
104
- InputMode.INSERT_MULTIPLE: "M",
105
- }[get_app().vi_state.input_mode]
@@ -195,7 +195,6 @@ class CliArgs:
195
195
  )
196
196
  myclirc: str = clickdc.option(
197
197
  type=click.Path(),
198
- default='~/.myclirc',
199
198
  help='Location of myclirc file.',
200
199
  )
201
200
  auto_vertical_output: bool = clickdc.option(
@@ -391,14 +390,6 @@ def preprocess_cli_args(
391
390
  cli_args.database = cli_args.password
392
391
  cli_args.password = EMPTY_PASSWORD_FLAG_SENTINEL
393
392
 
394
- if cli_args.password is None and cli_args.password_file:
395
- password_from_file = get_password_from_file(cli_args.password_file)
396
- if password_from_file is not None:
397
- cli_args.password = password_from_file
398
-
399
- if cli_args.password is None and os.environ.get('MYSQL_PWD') is not None:
400
- cli_args.password = os.environ.get('MYSQL_PWD')
401
-
402
393
  if cli_args.resume and not cli_args.checkpoint:
403
394
  click.secho('Error: --resume requires a --checkpoint file.', err=True, fg='red')
404
395
  sys.exit(1)
@@ -42,7 +42,7 @@ from pymysql.cursors import Cursor
42
42
  import mycli as mycli_package
43
43
  from mycli.clibuffer import cli_is_multiline
44
44
  from mycli.clistyle import style_factory_ptoolkit
45
- from mycli.clitoolbar import create_toolbar_tokens_func
45
+ from mycli.clitoolbar import create_toolbar_tokens_func, get_vi_mode
46
46
  from mycli.compat import WIN
47
47
  from mycli.constants import (
48
48
  DEFAULT_HOST,
@@ -321,6 +321,14 @@ def render_prompt_string(
321
321
  strings = [x.replace('\\_', ' ') for x in strings]
322
322
 
323
323
  checker_string = ' '.join(strings)
324
+ if r'\e' in checker_string:
325
+ if mycli.prompt_session:
326
+ edit_mode = mycli.prompt_session.editing_mode.value.lower()
327
+ if edit_mode == 'vi':
328
+ edit_mode += ':' + get_vi_mode()
329
+ else:
330
+ edit_mode = mycli.key_bindings.lower()
331
+ strings = [x.replace(r'\e', maybe_html_escape(edit_mode, is_html)) for x in strings]
324
332
  if hasattr(sqlexecute, 'conn') and sqlexecute.conn is not None:
325
333
  if '\\y' in checker_string:
326
334
  with sqlexecute.conn.cursor() as cur:
@@ -122,6 +122,7 @@ wider_completion_menu = False
122
122
  # * \s - seconds of the current time
123
123
  # * \P - AM/PM
124
124
  # * \d - selected database/schema
125
+ # * \e - current edit mode (emacs or vi)
125
126
  # * \h - hostname of the server
126
127
  # * \H - shortened hostname of the server
127
128
  # * \p - connection port
@@ -205,6 +206,21 @@ enable_pager = True
205
206
  # Choose a specific pager
206
207
  pager = 'less'
207
208
 
209
+ # Comma-separated sources to respect for a password, in order of precedence.
210
+ # Valid choices:
211
+ # * prompt # plain trailing --password at the CLI
212
+ # * literal # --password=<literal> at the CLI
213
+ # * file # --password-file=<file> at the CLI
214
+ # * environment # $MYSQL_PWD environment variable
215
+ # * dsn # mysql://user:password@ field in a DSN
216
+ # * boundary # value provided by Boundary client (not yet implemented)
217
+ # * vault # value retrieved from "vault kv get"
218
+ # * login_path # --login-path=<path> at the CLI
219
+ # * keyring # value retrieved from system keyring
220
+ # * fallback # interactive prompt after a passwordless connection fails
221
+ # If "fallback" is given, it must come last.
222
+ password_sources = prompt, literal, file, environment, dsn, boundary, vault, login_path, keyring, fallback
223
+
208
224
  # Whether to store and retrieve passwords from the system keyring.
209
225
  # * False - never use keyring
210
226
  # * True - always use keyring
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from typing import Literal
6
+
7
+ import click
8
+
9
+ PasswordSource = Literal[
10
+ 'prompt',
11
+ 'literal',
12
+ 'file',
13
+ 'environment',
14
+ 'dsn',
15
+ 'boundary',
16
+ 'vault',
17
+ 'login_path',
18
+ 'keyring',
19
+ 'fallback',
20
+ ]
21
+ PasswordValue = str | int
22
+ PasswordLoader = Callable[[], PasswordValue | None]
23
+
24
+ KNOWN_PASSWORD_SOURCES: list[PasswordSource] = [
25
+ 'prompt',
26
+ 'literal',
27
+ 'file',
28
+ 'environment',
29
+ 'dsn',
30
+ 'boundary',
31
+ 'vault',
32
+ 'login_path',
33
+ 'keyring',
34
+ 'fallback',
35
+ ]
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class SelectedPassword:
40
+ source: PasswordSource
41
+ value: PasswordValue
42
+
43
+
44
+ @dataclass(frozen=True, slots=True)
45
+ class PasswordCandidate:
46
+ source: PasswordSource
47
+ value: PasswordValue | None = None
48
+ loader: PasswordLoader | None = None
49
+
50
+ def resolve(self) -> SelectedPassword | None:
51
+ value = self.loader() if self.loader is not None else self.value
52
+ if value is None:
53
+ return None
54
+ return SelectedPassword(source=self.source, value=value)
55
+
56
+
57
+ class PasswordCandidates:
58
+ def __init__(self) -> None:
59
+ self._candidates: dict[PasswordSource, PasswordCandidate] = {}
60
+
61
+ def add_value(self, source: PasswordSource, value: PasswordValue | None) -> None:
62
+ self._candidates[source] = PasswordCandidate(source=source, value=value)
63
+
64
+ def add_loader(self, source: PasswordSource, loader: PasswordLoader) -> None:
65
+ self._candidates[source] = PasswordCandidate(source=source, loader=loader)
66
+
67
+ def resolve(self, password_source_precedence) -> SelectedPassword | None:
68
+ for source in password_source_precedence:
69
+ if source not in KNOWN_PASSWORD_SOURCES:
70
+ click.secho(f'Skipping unknown password source: {source}.', err=True, fg='red')
71
+ continue
72
+ candidate = self._candidates.get(source)
73
+ if candidate is None:
74
+ continue
75
+ if selected := candidate.resolve():
76
+ return selected
77
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: CLI for MySQL Database. With auto-completion and syntax highlighting.
5
5
  Author: Mycli Core Team
6
6
  License-Expression: BSD-3-Clause
@@ -43,6 +43,7 @@ mycli/lexer.py
43
43
  mycli/main.py
44
44
  mycli/myclirc
45
45
  mycli/output.py
46
+ mycli/password_sources.py
46
47
  mycli/schema_prefetcher.py
47
48
  mycli/sqlcompleter.py
48
49
  mycli/sqlexecute.py
@@ -150,6 +151,7 @@ test/pytests/test_main_modes_list_dsn.py
150
151
  test/pytests/test_main_modes_repl.py
151
152
  test/pytests/test_naive_completion.py
152
153
  test/pytests/test_output.py
154
+ test/pytests/test_password_sources.py
153
155
  test/pytests/test_ptoolkit_fzf.py
154
156
  test/pytests/test_ptoolkit_history.py
155
157
  test/pytests/test_ptoolkit_utils.py
@@ -60,6 +60,7 @@
60
60
  "test/pytests/test_key_binding_utils.py",
61
61
  "test/pytests/test_clitoolbar.py",
62
62
  "test/pytests/conftest.py",
63
+ "test/pytests/test_password_sources.py",
63
64
  "test/pytests/test_special_main.py",
64
65
  "test/pytests/test_main_modes_repl.py",
65
66
  "test/pytests/test_schema_prefetcher.py",
@@ -114,6 +115,7 @@
114
115
  "mycli/clibuffer.py",
115
116
  "mycli/client_query.py",
116
117
  "mycli/sqlcompleter.py",
118
+ "mycli/password_sources.py",
117
119
  "mycli/types.py",
118
120
  "mycli/constants.py",
119
121
  "mycli/config.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.4.0",
3
+ "distance": 0,
4
+ "node": "gc11245d74ff60a8e61f84b8d2eb6fbf773cb7ee3",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-18"
8
+ }
@@ -122,6 +122,7 @@ wider_completion_menu = False
122
122
  # * \s - seconds of the current time
123
123
  # * \P - AM/PM
124
124
  # * \d - selected database/schema
125
+ # * \e - current edit mode (emacs or vi)
125
126
  # * \h - hostname of the server
126
127
  # * \H - shortened hostname of the server
127
128
  # * \p - connection port
@@ -205,6 +206,21 @@ enable_pager = True
205
206
  # Choose a specific pager
206
207
  pager = python test/features/wrappager.py ---boundary---
207
208
 
209
+ # Comma-separated sources to respect for a password, in order of precedence.
210
+ # Valid choices:
211
+ # * prompt # plain trailing --password at the CLI
212
+ # * literal # --password=<literal> at the CLI
213
+ # * file # --password-file=<file> at the CLI
214
+ # * environment # $MYSQL_PWD environment variable
215
+ # * dsn # mysql://user:password@ field in a DSN
216
+ # * boundary # value provided by Boundary client (not yet implemented)
217
+ # * vault # value retrieved from "vault kv get"
218
+ # * login_path # --login-path=<path> at the CLI
219
+ # * keyring # value retrieved from system keyring
220
+ # * fallback # interactive prompt after a passwordless connection fails
221
+ # If "fallback" is given, it must come last.
222
+ password_sources = prompt, literal, file, environment, dsn, boundary, vault, login_path, keyring, fallback
223
+
208
224
  # Whether to store and retrieve passwords from the system keyring.
209
225
  # * False - never use keyring
210
226
  # * True - always use keyring