mycli 2.24.4__tar.gz → 2.25.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 (200) hide show
  1. {mycli-2.24.4 → mycli-2.25.0}/.github/dependabot.yml +2 -0
  2. {mycli-2.24.4 → mycli-2.25.0}/.github/workflows/ci.yml +24 -5
  3. {mycli-2.24.4 → mycli-2.25.0}/.github/workflows/publish.yml +15 -8
  4. {mycli-2.24.4 → mycli-2.25.0}/.github/workflows/typecheck.yml +1 -1
  5. {mycli-2.24.4 → mycli-2.25.0}/AGENTS.md +2 -0
  6. {mycli-2.24.4/mycli.egg-info → mycli-2.25.0}/PKG-INFO +18 -12
  7. {mycli-2.24.4 → mycli-2.25.0}/README.md +17 -11
  8. {mycli-2.24.4 → mycli-2.25.0}/changelog.md +28 -0
  9. {mycli-2.24.4 → mycli-2.25.0}/docker/resources/test_db-1.0.7.tar.gz +1 -1
  10. {mycli-2.24.4 → mycli-2.25.0}/mycli/client_connection.py +2 -1
  11. mycli-2.25.0/mycli/keyring_utils.py +34 -0
  12. mycli-2.25.0/mycli/macos_keychain.py +80 -0
  13. {mycli-2.24.4 → mycli-2.25.0/mycli.egg-info}/PKG-INFO +18 -12
  14. {mycli-2.24.4 → mycli-2.25.0}/mycli.egg-info/SOURCES.txt +4 -0
  15. {mycli-2.24.4 → mycli-2.25.0}/mycli.egg-info/scm_file_list.json +4 -0
  16. mycli-2.25.0/mycli.egg-info/scm_version.json +8 -0
  17. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_client_connection.py +50 -7
  18. mycli-2.25.0/test/pytests/test_keyring_utils.py +105 -0
  19. mycli-2.25.0/test/pytests/test_macos_keychain.py +160 -0
  20. mycli-2.24.4/mycli.egg-info/scm_version.json +0 -8
  21. {mycli-2.24.4 → mycli-2.25.0}/.git-blame-ignore-revs +0 -0
  22. {mycli-2.24.4 → mycli-2.25.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  23. {mycli-2.24.4 → mycli-2.25.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  24. {mycli-2.24.4 → mycli-2.25.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  25. {mycli-2.24.4 → mycli-2.25.0}/.github/workflows/lint.yml +0 -0
  26. {mycli-2.24.4 → mycli-2.25.0}/.gitignore +0 -0
  27. {mycli-2.24.4 → mycli-2.25.0}/AUTHORS.rst +0 -0
  28. {mycli-2.24.4 → mycli-2.25.0}/CONTRIBUTING.md +0 -0
  29. {mycli-2.24.4 → mycli-2.25.0}/LICENSE.txt +0 -0
  30. {mycli-2.24.4 → mycli-2.25.0}/MANIFEST.in +0 -0
  31. {mycli-2.24.4 → mycli-2.25.0}/SPONSORS.rst +0 -0
  32. {mycli-2.24.4 → mycli-2.25.0}/doc/key_bindings.rst +0 -0
  33. {mycli-2.24.4 → mycli-2.25.0}/doc/llm.md +0 -0
  34. {mycli-2.24.4 → mycli-2.25.0}/doc/screenshots/main.gif +0 -0
  35. {mycli-2.24.4 → mycli-2.25.0}/doc/screenshots/tables.png +0 -0
  36. {mycli-2.24.4 → mycli-2.25.0}/doc/screenshots/total_histogram.png +0 -0
  37. {mycli-2.24.4 → mycli-2.25.0}/doc/transforms.md +0 -0
  38. {mycli-2.24.4 → mycli-2.25.0}/docker/Dockerfile +0 -0
  39. {mycli-2.24.4 → mycli-2.25.0}/mycli/AUTHORS +0 -0
  40. {mycli-2.24.4 → mycli-2.25.0}/mycli/SPONSORS +0 -0
  41. {mycli-2.24.4 → mycli-2.25.0}/mycli/TIPS +0 -0
  42. {mycli-2.24.4 → mycli-2.25.0}/mycli/__init__.py +0 -0
  43. {mycli-2.24.4 → mycli-2.25.0}/mycli/app_state.py +0 -0
  44. {mycli-2.24.4 → mycli-2.25.0}/mycli/boundary_tunnel.py +0 -0
  45. {mycli-2.24.4 → mycli-2.25.0}/mycli/cli_runner.py +0 -0
  46. {mycli-2.24.4 → mycli-2.25.0}/mycli/clibuffer.py +0 -0
  47. {mycli-2.24.4 → mycli-2.25.0}/mycli/client.py +0 -0
  48. {mycli-2.24.4 → mycli-2.25.0}/mycli/client_commands.py +0 -0
  49. {mycli-2.24.4 → mycli-2.25.0}/mycli/client_query.py +0 -0
  50. {mycli-2.24.4 → mycli-2.25.0}/mycli/clistyle.py +0 -0
  51. {mycli-2.24.4 → mycli-2.25.0}/mycli/clitoolbar.py +0 -0
  52. {mycli-2.24.4 → mycli-2.25.0}/mycli/compat.py +0 -0
  53. {mycli-2.24.4 → mycli-2.25.0}/mycli/completion_refresher.py +0 -0
  54. {mycli-2.24.4 → mycli-2.25.0}/mycli/config.py +0 -0
  55. {mycli-2.24.4 → mycli-2.25.0}/mycli/constants.py +0 -0
  56. {mycli-2.24.4 → mycli-2.25.0}/mycli/key_bindings.py +0 -0
  57. {mycli-2.24.4 → mycli-2.25.0}/mycli/kubectl_tunnel.py +0 -0
  58. {mycli-2.24.4 → mycli-2.25.0}/mycli/lexer.py +0 -0
  59. {mycli-2.24.4 → mycli-2.25.0}/mycli/main.py +0 -0
  60. {mycli-2.24.4 → mycli-2.25.0}/mycli/main_modes/batch.py +0 -0
  61. {mycli-2.24.4 → mycli-2.25.0}/mycli/main_modes/checkup.py +0 -0
  62. {mycli-2.24.4 → mycli-2.25.0}/mycli/main_modes/completions.py +0 -0
  63. {mycli-2.24.4 → mycli-2.25.0}/mycli/main_modes/execute.py +0 -0
  64. {mycli-2.24.4 → mycli-2.25.0}/mycli/main_modes/list_dsn.py +0 -0
  65. {mycli-2.24.4 → mycli-2.25.0}/mycli/main_modes/repl.py +0 -0
  66. {mycli-2.24.4 → mycli-2.25.0}/mycli/myclirc +0 -0
  67. {mycli-2.24.4 → mycli-2.25.0}/mycli/output.py +0 -0
  68. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/__init__.py +0 -0
  69. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/batch_utils.py +0 -0
  70. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/cli_utils.py +0 -0
  71. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/completion_engine.py +0 -0
  72. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/filepaths.py +0 -0
  73. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/hybrid_redirection.py +0 -0
  74. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/interactive_utils.py +0 -0
  75. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/key_binding_utils.py +0 -0
  76. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/polars_completion.py +0 -0
  77. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/polars_transform.py +0 -0
  78. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/ptoolkit/__init__.py +0 -0
  79. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/ptoolkit/fzf.py +0 -0
  80. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/ptoolkit/history.py +0 -0
  81. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/ptoolkit/utils.py +0 -0
  82. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/special/__init__.py +0 -0
  83. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/special/dbcommands.py +0 -0
  84. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/special/delimitercommand.py +0 -0
  85. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/special/dsn_aliases.py +0 -0
  86. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/special/favoritequeries.py +0 -0
  87. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/special/iocommands.py +0 -0
  88. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/special/llm.py +0 -0
  89. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/special/main.py +0 -0
  90. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/special/source.py +0 -0
  91. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/special/utils.py +0 -0
  92. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/sql_utils.py +0 -0
  93. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/sqlresult.py +0 -0
  94. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/string_utils.py +0 -0
  95. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/tabular_output/__init__.py +0 -0
  96. {mycli-2.24.4 → mycli-2.25.0}/mycli/packages/tabular_output/sql_format.py +0 -0
  97. {mycli-2.24.4 → mycli-2.25.0}/mycli/password_sources.py +0 -0
  98. {mycli-2.24.4 → mycli-2.25.0}/mycli/resources/completions/bash/mycli +0 -0
  99. {mycli-2.24.4 → mycli-2.25.0}/mycli/resources/completions/fish/mycli.fish +0 -0
  100. {mycli-2.24.4 → mycli-2.25.0}/mycli/resources/completions/zsh/_mycli +0 -0
  101. {mycli-2.24.4 → mycli-2.25.0}/mycli/schema_prefetcher.py +0 -0
  102. {mycli-2.24.4 → mycli-2.25.0}/mycli/sqlcompleter.py +0 -0
  103. {mycli-2.24.4 → mycli-2.25.0}/mycli/sqlexecute.py +0 -0
  104. {mycli-2.24.4 → mycli-2.25.0}/mycli/ssh_tunnel.py +0 -0
  105. {mycli-2.24.4 → mycli-2.25.0}/mycli/types.py +0 -0
  106. {mycli-2.24.4 → mycli-2.25.0}/mycli/vault.py +0 -0
  107. {mycli-2.24.4 → mycli-2.25.0}/mycli.egg-info/dependency_links.txt +0 -0
  108. {mycli-2.24.4 → mycli-2.25.0}/mycli.egg-info/entry_points.txt +0 -0
  109. {mycli-2.24.4 → mycli-2.25.0}/mycli.egg-info/requires.txt +0 -0
  110. {mycli-2.24.4 → mycli-2.25.0}/mycli.egg-info/top_level.txt +0 -0
  111. {mycli-2.24.4 → mycli-2.25.0}/pyproject.toml +0 -0
  112. {mycli-2.24.4 → mycli-2.25.0}/setup.cfg +0 -0
  113. {mycli-2.24.4 → mycli-2.25.0}/test/__init__.py +0 -0
  114. {mycli-2.24.4 → mycli-2.25.0}/test/features/__init__.py +0 -0
  115. {mycli-2.24.4 → mycli-2.25.0}/test/features/auto_vertical.feature +0 -0
  116. {mycli-2.24.4 → mycli-2.25.0}/test/features/basic_commands.feature +0 -0
  117. {mycli-2.24.4 → mycli-2.25.0}/test/features/connection.feature +0 -0
  118. {mycli-2.24.4 → mycli-2.25.0}/test/features/crud_database.feature +0 -0
  119. {mycli-2.24.4 → mycli-2.25.0}/test/features/crud_table.feature +0 -0
  120. {mycli-2.24.4 → mycli-2.25.0}/test/features/db_utils.py +0 -0
  121. {mycli-2.24.4 → mycli-2.25.0}/test/features/environment.py +0 -0
  122. {mycli-2.24.4 → mycli-2.25.0}/test/features/fixture_data/help_commands.txt +0 -0
  123. {mycli-2.24.4 → mycli-2.25.0}/test/features/fixture_utils.py +0 -0
  124. {mycli-2.24.4 → mycli-2.25.0}/test/features/iocommands.feature +0 -0
  125. {mycli-2.24.4 → mycli-2.25.0}/test/features/named_queries.feature +0 -0
  126. {mycli-2.24.4 → mycli-2.25.0}/test/features/specials.feature +0 -0
  127. {mycli-2.24.4 → mycli-2.25.0}/test/features/steps/__init__.py +0 -0
  128. {mycli-2.24.4 → mycli-2.25.0}/test/features/steps/auto_vertical.py +0 -0
  129. {mycli-2.24.4 → mycli-2.25.0}/test/features/steps/basic_commands.py +0 -0
  130. {mycli-2.24.4 → mycli-2.25.0}/test/features/steps/connection.py +0 -0
  131. {mycli-2.24.4 → mycli-2.25.0}/test/features/steps/crud_database.py +0 -0
  132. {mycli-2.24.4 → mycli-2.25.0}/test/features/steps/crud_table.py +0 -0
  133. {mycli-2.24.4 → mycli-2.25.0}/test/features/steps/iocommands.py +0 -0
  134. {mycli-2.24.4 → mycli-2.25.0}/test/features/steps/named_queries.py +0 -0
  135. {mycli-2.24.4 → mycli-2.25.0}/test/features/steps/specials.py +0 -0
  136. {mycli-2.24.4 → mycli-2.25.0}/test/features/steps/utils.py +0 -0
  137. {mycli-2.24.4 → mycli-2.25.0}/test/features/steps/wrappers.py +0 -0
  138. {mycli-2.24.4 → mycli-2.25.0}/test/features/wrappager.py +0 -0
  139. {mycli-2.24.4 → mycli-2.25.0}/test/myclirc +0 -0
  140. {mycli-2.24.4 → mycli-2.25.0}/test/mylogin.cnf +0 -0
  141. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/conftest.py +0 -0
  142. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_app_state.py +0 -0
  143. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_bash_completion.py +0 -0
  144. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_batch_utils.py +0 -0
  145. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_boundary_tunnel.py +0 -0
  146. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_checkup.py +0 -0
  147. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_cli_runner.py +0 -0
  148. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_cli_utils.py +0 -0
  149. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_clibuffer.py +0 -0
  150. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_client.py +0 -0
  151. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_client_commands.py +0 -0
  152. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_client_query.py +0 -0
  153. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_clistyle.py +0 -0
  154. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_clitoolbar.py +0 -0
  155. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_completer_use_switch.py +0 -0
  156. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_completion_engine.py +0 -0
  157. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_completion_refresher.py +0 -0
  158. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_config.py +0 -0
  159. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_delimitercommand.py +0 -0
  160. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_dsn_aliases.py +0 -0
  161. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_favoritequeries.py +0 -0
  162. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_filepaths.py +0 -0
  163. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_fish_completion.py +0 -0
  164. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_hybrid_redirection.py +0 -0
  165. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_interactive_utils.py +0 -0
  166. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_key_binding_utils.py +0 -0
  167. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_key_bindings.py +0 -0
  168. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_kubectl_tunnel.py +0 -0
  169. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_main.py +0 -0
  170. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_main_modes_batch.py +0 -0
  171. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_main_modes_execute.py +0 -0
  172. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
  173. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_main_modes_repl.py +0 -0
  174. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_naive_completion.py +0 -0
  175. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_output.py +0 -0
  176. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_password_sources.py +0 -0
  177. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_polars_completion.py +0 -0
  178. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_polars_transform.py +0 -0
  179. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
  180. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_ptoolkit_history.py +0 -0
  181. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_ptoolkit_utils.py +0 -0
  182. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_schema_prefetcher.py +0 -0
  183. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
  184. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_special_dbcommands.py +0 -0
  185. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_special_init.py +0 -0
  186. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_special_iocommands.py +0 -0
  187. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_special_llm.py +0 -0
  188. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_special_main.py +0 -0
  189. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_special_source.py +0 -0
  190. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_special_utils.py +0 -0
  191. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_sql_utils.py +0 -0
  192. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_sqlcompleter.py +0 -0
  193. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_sqlexecute.py +0 -0
  194. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_sqlresult.py +0 -0
  195. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_ssh_tunnel.py +0 -0
  196. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_string_utils.py +0 -0
  197. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_tabular_output.py +0 -0
  198. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_vault.py +0 -0
  199. {mycli-2.24.4 → mycli-2.25.0}/test/pytests/test_zsh_completion.py +0 -0
  200. {mycli-2.24.4 → mycli-2.25.0}/test/utils.py +0 -0
@@ -1,6 +1,8 @@
1
1
  version: 2
2
2
  updates:
3
3
  - package-ecosystem: "github-actions"
4
+ cooldown:
5
+ default-days: 7
4
6
  directory: "/"
5
7
  schedule:
6
8
  interval: "daily"
@@ -24,7 +24,7 @@ jobs:
24
24
  steps:
25
25
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
26
26
 
27
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
27
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
28
28
  with:
29
29
  version: "latest"
30
30
 
@@ -66,7 +66,7 @@ jobs:
66
66
  steps:
67
67
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
68
68
 
69
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
69
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
70
70
  with:
71
71
  version: "latest"
72
72
 
@@ -116,7 +116,7 @@ jobs:
116
116
  steps:
117
117
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
118
118
 
119
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
119
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
120
120
  with:
121
121
  version: 'latest'
122
122
 
@@ -130,7 +130,7 @@ jobs:
130
130
  run: uv sync --all-extras -p ${{ matrix.python-version }}
131
131
 
132
132
  - name: Set up MySQL
133
- uses: shogo82148/actions-setup-mysql@62da9377d83991fce27b6ed2a397d3306121e41d # v1.53.1
133
+ uses: shogo82148/actions-setup-mysql@083ec148d28e62f2b24b8f4541693dfe1d50488d # v1.54.0
134
134
  with:
135
135
  mysql-version: '9.7'
136
136
  root-password: root
@@ -220,7 +220,7 @@ jobs:
220
220
  steps:
221
221
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
222
222
 
223
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
223
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
224
224
  with:
225
225
  version: "latest"
226
226
 
@@ -249,3 +249,22 @@ jobs:
249
249
  TERM: xterm
250
250
  run: |
251
251
  uv run tox -e py3.14
252
+
253
+ docker-build:
254
+ name: Build Docker Image
255
+ runs-on: ubuntu-latest
256
+
257
+ permissions:
258
+ contents: read
259
+
260
+ steps:
261
+ - name: Checkout repository
262
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
263
+
264
+ - name: Build Docker image
265
+ id: build
266
+ uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
267
+ with:
268
+ context: .
269
+ file: docker/Dockerfile
270
+ push: false
@@ -1,4 +1,4 @@
1
- name: Publish Python Package
1
+ name: Publish Packages
2
2
 
3
3
  on:
4
4
  release:
@@ -32,7 +32,7 @@ jobs:
32
32
 
33
33
  steps:
34
34
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
35
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
35
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
36
36
  with:
37
37
  version: "latest"
38
38
 
@@ -77,7 +77,7 @@ jobs:
77
77
  steps:
78
78
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
79
79
 
80
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
80
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
81
81
  with:
82
82
  version: "latest"
83
83
 
@@ -127,7 +127,7 @@ jobs:
127
127
  steps:
128
128
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
129
129
 
130
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
130
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
131
131
  with:
132
132
  version: 'latest'
133
133
 
@@ -141,7 +141,7 @@ jobs:
141
141
  run: uv sync --all-extras -p ${{ matrix.python-version }}
142
142
 
143
143
  - name: Set up MySQL
144
- uses: shogo82148/actions-setup-mysql@62da9377d83991fce27b6ed2a397d3306121e41d # v1.53.1
144
+ uses: shogo82148/actions-setup-mysql@083ec148d28e62f2b24b8f4541693dfe1d50488d # v1.54.0
145
145
  with:
146
146
  mysql-version: '9.7'
147
147
  root-password: root
@@ -230,7 +230,7 @@ jobs:
230
230
 
231
231
  steps:
232
232
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
233
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
233
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
234
234
  with:
235
235
  version: "latest"
236
236
 
@@ -252,7 +252,7 @@ jobs:
252
252
  path: dist/
253
253
 
254
254
  publish-pypi:
255
- name: Publish to PyPI
255
+ name: Publish Wheel to PyPI
256
256
  runs-on: ubuntu-latest
257
257
  if: startsWith(github.ref, 'refs/tags/')
258
258
  needs: [build]
@@ -269,9 +269,11 @@ jobs:
269
269
  uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
270
270
 
271
271
  publish-image:
272
- name: Publish to ghcr.io
272
+ name: Publish Image to ghcr.io
273
273
  runs-on: ubuntu-latest
274
274
  if: startsWith(github.ref, 'refs/tags/')
275
+ # doesn't actually need the results of build, but it makes sense to run this
276
+ # concurrently with publish-pypi
275
277
  needs: [build]
276
278
  environment: release
277
279
 
@@ -293,6 +295,10 @@ jobs:
293
295
  username: ${{ github.actor }}
294
296
  password: ${{ secrets.GITHUB_TOKEN }}
295
297
 
298
+ - name: Set up Docker Buildx
299
+ id: buildx
300
+ uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
301
+
296
302
  - name: Extract metadata (tags, labels) for Docker
297
303
  id: meta
298
304
  uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
@@ -306,5 +312,6 @@ jobs:
306
312
  context: .
307
313
  file: docker/Dockerfile
308
314
  push: true
315
+ platforms: linux/amd64,linux/arm64
309
316
  tags: ${{ steps.meta.outputs.tags }}
310
317
  labels: ${{ steps.meta.outputs.labels }}
@@ -25,7 +25,7 @@ jobs:
25
25
  with:
26
26
  python-version: '3.14'
27
27
 
28
- - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
28
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
29
29
  with:
30
30
  version: 'latest'
31
31
 
@@ -25,8 +25,10 @@ A command line client for MySQL with auto-completion and syntax highlighting.
25
25
  ├── mycli/config.py # configuration file readers and utilities
26
26
  ├── mycli/constants.py # shared constants
27
27
  ├── mycli/key_bindings.py # prompt_toolkit key binding utilities
28
+ ├── mycli/keyring_utils.py # saved credential utilities
28
29
  ├── mycli/kubectl_tunnel.py # connection over kubectl tunnel
29
30
  ├── mycli/lexer.py # extends `MySqlLexer` from Pygments
31
+ ├── mycli/macos_keychain.py # macOS Keychain credentials preserving access controls
30
32
  ├── mycli/main.py # processes CLI arguments
31
33
  ├── mycli/main_modes/ # main execution paths
32
34
  ├── mycli/main_modes/batch.py # `--batch` mode
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.24.4
3
+ Version: 2.25.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
@@ -143,20 +143,26 @@ mycli --help
143
143
  * Fuzzy history search using [fzf](https://github.com/junegunn/fzf).
144
144
  * Output explorer using [fzf](https://github.com/junegunn/fzf) or other tools.
145
145
  * Syntax highlighting using [Pygments](https://pygments.org/).
146
- * Smart-completion (enabled by default) will suggest context-sensitive completion.
147
- - `SELECT * FROM <tab>` will only show table names.
148
- - `SELECT * FROM users WHERE <tab>` will only show column names.
149
- * Support for multiline queries.
146
+ * Smart-completion suggests context-sensitive completion:
147
+ - `SELECT * FROM `<kbd>tab</kbd> will only show table names.
148
+ - `SELECT * FROM users WHERE `<kbd>tab</kbd> will only show column names.
149
+ * Multiline queries.
150
150
  * Favorite queries with positional or named parameters using [Jinja](https://jinja.palletsprojects.com/en/stable/). Save a query using
151
- `/fs <alias> <query>` and execute it with `/f <alias>` or `/f <alias> --key=value`.
152
- * Timing of sql statements and table rendering.
153
- * Log every query and its results to a file (disabled by default).
154
- * Pretty print tabular data with colors.
155
- * Support for SSL connections.
151
+ ```
152
+ /fs alias <query>
153
+ ```
154
+ and execute it with
155
+ ```
156
+ /f alias --key=value
157
+ ```
158
+ * Timing of SQL statements and table rendering.
159
+ * Logging of every query and its results to a file (off by default).
160
+ * Pretty printing of tabular data.
156
161
  * Shell-style trailing redirects with `$>`, `$>>` and `$|` operators.
157
162
  * [Polars](https://pola.rs) dataframe [transforms and plots](https://github.com/dbcli/mycli/blob/main/doc/transforms.md) with `.|`, and Parquet saves with `.>`.
158
- * Support for [querying LLMs](https://www.mycli.net/llm) with context derived from your schema using `/llm`.
159
- * Support for storing passwords in the system keyring.
163
+ * [Querying LLMs](https://www.mycli.net/llm) with context derived from your schema using `/llm`.
164
+ * Storing passwords in the system keyring.
165
+ * Modern defaults such as UTF-8 connection type and SSL.
160
166
  * Integrations with: [SSH](https://www.openssh.org/), [Kubernetes](https://kubernetes.io/), [Vault](https://github.com/hashicorp/vault), and [Boundary](https://github.com/hashicorp/boundary).
161
167
 
162
168
  Mycli creates a config file `~/.myclirc` on the first run; you can use the
@@ -71,20 +71,26 @@ mycli --help
71
71
  * Fuzzy history search using [fzf](https://github.com/junegunn/fzf).
72
72
  * Output explorer using [fzf](https://github.com/junegunn/fzf) or other tools.
73
73
  * Syntax highlighting using [Pygments](https://pygments.org/).
74
- * Smart-completion (enabled by default) will suggest context-sensitive completion.
75
- - `SELECT * FROM <tab>` will only show table names.
76
- - `SELECT * FROM users WHERE <tab>` will only show column names.
77
- * Support for multiline queries.
74
+ * Smart-completion suggests context-sensitive completion:
75
+ - `SELECT * FROM `<kbd>tab</kbd> will only show table names.
76
+ - `SELECT * FROM users WHERE `<kbd>tab</kbd> will only show column names.
77
+ * Multiline queries.
78
78
  * Favorite queries with positional or named parameters using [Jinja](https://jinja.palletsprojects.com/en/stable/). Save a query using
79
- `/fs <alias> <query>` and execute it with `/f <alias>` or `/f <alias> --key=value`.
80
- * Timing of sql statements and table rendering.
81
- * Log every query and its results to a file (disabled by default).
82
- * Pretty print tabular data with colors.
83
- * Support for SSL connections.
79
+ ```
80
+ /fs alias <query>
81
+ ```
82
+ and execute it with
83
+ ```
84
+ /f alias --key=value
85
+ ```
86
+ * Timing of SQL statements and table rendering.
87
+ * Logging of every query and its results to a file (off by default).
88
+ * Pretty printing of tabular data.
84
89
  * Shell-style trailing redirects with `$>`, `$>>` and `$|` operators.
85
90
  * [Polars](https://pola.rs) dataframe [transforms and plots](https://github.com/dbcli/mycli/blob/main/doc/transforms.md) with `.|`, and Parquet saves with `.>`.
86
- * Support for [querying LLMs](https://www.mycli.net/llm) with context derived from your schema using `/llm`.
87
- * Support for storing passwords in the system keyring.
91
+ * [Querying LLMs](https://www.mycli.net/llm) with context derived from your schema using `/llm`.
92
+ * Storing passwords in the system keyring.
93
+ * Modern defaults such as UTF-8 connection type and SSL.
88
94
  * Integrations with: [SSH](https://www.openssh.org/), [Kubernetes](https://kubernetes.io/), [Vault](https://github.com/hashicorp/vault), and [Boundary](https://github.com/hashicorp/boundary).
89
95
 
90
96
  Mycli creates a config file `~/.myclirc` on the first run; you can use the
@@ -1,3 +1,31 @@
1
+ 2.25.0 (2026/09/16)
2
+ ==============
3
+
4
+ Features
5
+ --------
6
+ * Publish multi-architecture Docker images.
7
+
8
+
9
+ 2.24.5 (2026/09/16)
10
+ ==============
11
+
12
+ Bug Fixes
13
+ --------
14
+ * New macOS keyring entries no longer automatically trust Python.
15
+
16
+
17
+ Documentation
18
+ --------
19
+ * Update features bullets in `README.md`.
20
+
21
+
22
+ Internal
23
+ --------
24
+ * Naming and commentary in `publish.yml` workflow.
25
+ * Build Docker image as part of regular CI tests.
26
+ * One week cooldown for dependabot.
27
+
28
+
1
29
  2.24.4 (2026/09/12)
2
30
  ==============
3
31
 
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
- name: mycli-2.24.4/docker/resources/test_db-1.0.7.tar.gz
2
+ name: mycli-2.25.0/docker/resources/test_db-1.0.7.tar.gz
3
3
  size: 35607473 bytes
4
4
  sha256: c44c140f352f35d47fdb65df60f52b779ef552822fad6c4efcfa7b134c3faf84
@@ -22,6 +22,7 @@ from mycli.constants import (
22
22
  EMPTY_PASSWORD_FLAG_SENTINEL,
23
23
  ER_MUST_CHANGE_PASSWORD_LOGIN,
24
24
  )
25
+ from mycli.keyring_utils import set_keyring_password
25
26
  from mycli.kubectl_tunnel import KubectlTunnel, KubectlTunnelError
26
27
  from mycli.packages.filepaths import guess_socket_location
27
28
  from mycli.packages.special.utils import format_connection_dsn
@@ -381,7 +382,7 @@ class ClientConnectionMixin:
381
382
  try:
382
383
  saved_pw = keyring.get_password(keyring_domain, keyring_identifier)
383
384
  if password != saved_pw or reset_keyring:
384
- keyring.set_password(keyring_domain, keyring_identifier, password)
385
+ set_keyring_password(keyring_domain, keyring_identifier, password)
385
386
  click.secho(
386
387
  f'Password from source: "{password_source}" '
387
388
  f'saved to the system keyring at {keyring_domain}/{keyring_identifier}',
@@ -0,0 +1,34 @@
1
+ import sys
2
+
3
+ import keyring
4
+ from keyring.backend import KeyringBackend
5
+
6
+
7
+ def _set_password_with_backend(backend: KeyringBackend, service: str, account: str, password: str) -> None:
8
+ from keyring.backends.macOS import Keyring
9
+
10
+ if type(backend) is Keyring:
11
+ from mycli import macos_keychain
12
+
13
+ macos_keychain.set_password(service, account, password)
14
+ else:
15
+ backend.set_password(service, account, password)
16
+
17
+
18
+ def set_keyring_password(service: str, account: str, password: str) -> None:
19
+ if sys.platform != 'darwin':
20
+ keyring.set_password(service, account, password)
21
+ return
22
+
23
+ from keyring.backends.chainer import ChainerBackend # type: ignore[unreachable]
24
+
25
+ backend = keyring.get_keyring()
26
+ if type(backend) is ChainerBackend:
27
+ for candidate in backend.backends:
28
+ try:
29
+ _set_password_with_backend(candidate, service, account, password)
30
+ return
31
+ except NotImplementedError:
32
+ continue
33
+ else:
34
+ _set_password_with_backend(backend, service, account, password)
@@ -0,0 +1,80 @@
1
+ import ctypes
2
+ from typing import Any
3
+
4
+ from keyring.errors import KeyringLocked, PasswordSetError
5
+
6
+
7
+ def _get_api() -> Any:
8
+ from keyring.backends.macOS import api
9
+
10
+ return api
11
+
12
+
13
+ def set_password(service: str, account: str, password: str) -> None:
14
+ """Preserve existing ACLs and create credentials without trusting Python."""
15
+ api = _get_api()
16
+ cf_array_create = api._found.CFArrayCreate
17
+ cf_array_create.restype = ctypes.c_void_p
18
+ cf_array_create.argtypes = (ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long, ctypes.c_void_p)
19
+ cf_data_create = api._found.CFDataCreate
20
+ cf_data_create.restype = ctypes.c_void_p
21
+ cf_data_create.argtypes = (ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long)
22
+ cf_release = api._found.CFRelease
23
+ cf_release.restype = None
24
+ cf_release.argtypes = (ctypes.c_void_p,)
25
+ sec_access_create = api._sec.SecAccessCreate
26
+ sec_access_create.restype = api.OS_status
27
+ sec_access_create.argtypes = (ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p))
28
+ sec_item_update = api._sec.SecItemUpdate
29
+ sec_item_update.restype = api.OS_status
30
+ sec_item_update.argtypes = (ctypes.c_void_p, ctypes.c_void_p)
31
+
32
+ retained: list[ctypes.c_void_p] = []
33
+
34
+ def retain(value: int | ctypes.c_void_p | None, name: str) -> ctypes.c_void_p:
35
+ if not value:
36
+ raise RuntimeError(f'Unable to allocate Keychain {name}')
37
+ pointer = value if isinstance(value, ctypes.c_void_p) else ctypes.c_void_p(value)
38
+ retained.append(pointer)
39
+ return pointer
40
+
41
+ try:
42
+ service_value = retain(api.create_cf(service), 'service')
43
+ account_value = retain(api.create_cf(account), 'account')
44
+ encoded = password.encode('utf-8')
45
+ password_value = retain(cf_data_create(None, ctypes.create_string_buffer(encoded), len(encoded)), 'password')
46
+ search = retain(
47
+ api.create_query(
48
+ kSecClass=api.k_('kSecClassGenericPassword'),
49
+ kSecAttrService=service_value,
50
+ kSecAttrAccount=account_value,
51
+ ),
52
+ 'search',
53
+ )
54
+ attributes = retain(api.create_query(kSecValueData=password_value), 'attributes')
55
+ status = sec_item_update(search, attributes)
56
+ if status == api.error.item_not_found:
57
+ # An empty array trusts nobody; NULL would trust the calling executable.
58
+ trusted_apps = retain(cf_array_create(None, None, 0, None), 'access controls')
59
+ access = ctypes.c_void_p()
60
+ api.Error.raise_for_status(sec_access_create(service_value, trusted_apps, ctypes.byref(access)))
61
+ retain(access, 'access')
62
+ item = retain(
63
+ api.create_query(
64
+ kSecClass=api.k_('kSecClassGenericPassword'),
65
+ kSecAttrService=service_value,
66
+ kSecAttrAccount=account_value,
67
+ kSecValueData=password_value,
68
+ kSecAttrAccess=access,
69
+ ),
70
+ 'item',
71
+ )
72
+ status = api.SecItemAdd(item, None)
73
+ api.Error.raise_for_status(status)
74
+ except api.KeychainDenied as e:
75
+ raise KeyringLocked(f"Can't store password on keychain: {e}") from e
76
+ except api.Error as e:
77
+ raise PasswordSetError(f"Can't store password on keychain: {e}") from e
78
+ finally:
79
+ for value in reversed(retained):
80
+ cf_release(value)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mycli
3
- Version: 2.24.4
3
+ Version: 2.25.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
@@ -143,20 +143,26 @@ mycli --help
143
143
  * Fuzzy history search using [fzf](https://github.com/junegunn/fzf).
144
144
  * Output explorer using [fzf](https://github.com/junegunn/fzf) or other tools.
145
145
  * Syntax highlighting using [Pygments](https://pygments.org/).
146
- * Smart-completion (enabled by default) will suggest context-sensitive completion.
147
- - `SELECT * FROM <tab>` will only show table names.
148
- - `SELECT * FROM users WHERE <tab>` will only show column names.
149
- * Support for multiline queries.
146
+ * Smart-completion suggests context-sensitive completion:
147
+ - `SELECT * FROM `<kbd>tab</kbd> will only show table names.
148
+ - `SELECT * FROM users WHERE `<kbd>tab</kbd> will only show column names.
149
+ * Multiline queries.
150
150
  * Favorite queries with positional or named parameters using [Jinja](https://jinja.palletsprojects.com/en/stable/). Save a query using
151
- `/fs <alias> <query>` and execute it with `/f <alias>` or `/f <alias> --key=value`.
152
- * Timing of sql statements and table rendering.
153
- * Log every query and its results to a file (disabled by default).
154
- * Pretty print tabular data with colors.
155
- * Support for SSL connections.
151
+ ```
152
+ /fs alias <query>
153
+ ```
154
+ and execute it with
155
+ ```
156
+ /f alias --key=value
157
+ ```
158
+ * Timing of SQL statements and table rendering.
159
+ * Logging of every query and its results to a file (off by default).
160
+ * Pretty printing of tabular data.
156
161
  * Shell-style trailing redirects with `$>`, `$>>` and `$|` operators.
157
162
  * [Polars](https://pola.rs) dataframe [transforms and plots](https://github.com/dbcli/mycli/blob/main/doc/transforms.md) with `.|`, and Parquet saves with `.>`.
158
- * Support for [querying LLMs](https://www.mycli.net/llm) with context derived from your schema using `/llm`.
159
- * Support for storing passwords in the system keyring.
163
+ * [Querying LLMs](https://www.mycli.net/llm) with context derived from your schema using `/llm`.
164
+ * Storing passwords in the system keyring.
165
+ * Modern defaults such as UTF-8 connection type and SSL.
160
166
  * Integrations with: [SSH](https://www.openssh.org/), [Kubernetes](https://kubernetes.io/), [Vault](https://github.com/hashicorp/vault), and [Boundary](https://github.com/hashicorp/boundary).
161
167
 
162
168
  Mycli creates a config file `~/.myclirc` on the first run; you can use the
@@ -44,8 +44,10 @@ mycli/completion_refresher.py
44
44
  mycli/config.py
45
45
  mycli/constants.py
46
46
  mycli/key_bindings.py
47
+ mycli/keyring_utils.py
47
48
  mycli/kubectl_tunnel.py
48
49
  mycli/lexer.py
50
+ mycli/macos_keychain.py
49
51
  mycli/main.py
50
52
  mycli/myclirc
51
53
  mycli/output.py
@@ -159,7 +161,9 @@ test/pytests/test_hybrid_redirection.py
159
161
  test/pytests/test_interactive_utils.py
160
162
  test/pytests/test_key_binding_utils.py
161
163
  test/pytests/test_key_bindings.py
164
+ test/pytests/test_keyring_utils.py
162
165
  test/pytests/test_kubectl_tunnel.py
166
+ test/pytests/test_macos_keychain.py
163
167
  test/pytests/test_main.py
164
168
  test/pytests/test_main_modes_batch.py
165
169
  test/pytests/test_main_modes_execute.py
@@ -45,8 +45,10 @@
45
45
  "mycli/config.py",
46
46
  "mycli/constants.py",
47
47
  "mycli/key_bindings.py",
48
+ "mycli/keyring_utils.py",
48
49
  "mycli/kubectl_tunnel.py",
49
50
  "mycli/lexer.py",
51
+ "mycli/macos_keychain.py",
50
52
  "mycli/main.py",
51
53
  "mycli/main_modes/batch.py",
52
54
  "mycli/main_modes/checkup.py",
@@ -152,7 +154,9 @@
152
154
  "test/pytests/test_interactive_utils.py",
153
155
  "test/pytests/test_key_binding_utils.py",
154
156
  "test/pytests/test_key_bindings.py",
157
+ "test/pytests/test_keyring_utils.py",
155
158
  "test/pytests/test_kubectl_tunnel.py",
159
+ "test/pytests/test_macos_keychain.py",
156
160
  "test/pytests/test_main.py",
157
161
  "test/pytests/test_main_modes_batch.py",
158
162
  "test/pytests/test_main_modes_execute.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.25.0",
3
+ "distance": 0,
4
+ "node": "g225bf21e310f12ec2e22cbb178e1ab93822bb385",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-16"
8
+ }
@@ -260,11 +260,14 @@ def test_connect_retrieves_password_from_keyring(monkeypatch: pytest.MonkeyPatch
260
260
  return 'from-keyring'
261
261
 
262
262
  monkeypatch.setattr(client_connection.keyring, 'get_password', fake_get_password)
263
+ writes: list[tuple[str, str, str]] = []
264
+ monkeypatch.setattr(client_connection, 'set_keyring_password', lambda *args: writes.append(args))
263
265
 
264
266
  client.connect(user='alice', host='db', port=3307, use_keyring=True)
265
267
 
266
268
  assert FakeSQLExecute.calls[-1]['password'] == 'from-keyring'
267
269
  assert get_password_calls == [('mycli.net', 'alice@db:3307:')]
270
+ assert writes == []
268
271
 
269
272
 
270
273
  def test_connect_uses_mylogin_password_before_keyring(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -275,7 +278,7 @@ def test_connect_uses_mylogin_password_before_keyring(monkeypatch: pytest.Monkey
275
278
  'get_password',
276
279
  lambda domain, identifier: get_password_calls.append((domain, identifier)) or 'from-mylogin', # type: ignore[func-returns-value]
277
280
  )
278
- monkeypatch.setattr(client_connection.keyring, 'set_password', lambda *_args: None)
281
+ monkeypatch.setattr(client_connection, 'set_keyring_password', lambda *_args: None)
279
282
 
280
283
  client.connect(user='alice', host='db', port=3307, use_keyring=True)
281
284
 
@@ -293,7 +296,7 @@ def test_connect_resolves_supplied_candidates_after_connection_details(monkeypat
293
296
  'get_password',
294
297
  lambda domain, identifier: keyring_calls.append((domain, identifier)) or 'from-dsn', # type: ignore[func-returns-value]
295
298
  )
296
- monkeypatch.setattr(client_connection.keyring, 'set_password', lambda *_args: None)
299
+ monkeypatch.setattr(client_connection, 'set_keyring_password', lambda *_args: None)
297
300
 
298
301
  client.connect(user='alice', host='db', port=3307, password_candidates=candidates, use_keyring=True)
299
302
 
@@ -326,8 +329,8 @@ def test_connect_saves_selected_password_to_keyring(monkeypatch: pytest.MonkeyPa
326
329
  secho_calls: list[tuple[str, dict[str, Any]]] = []
327
330
  monkeypatch.setattr(client_connection.keyring, 'get_password', lambda *_args: 'old-secret')
328
331
  monkeypatch.setattr(
329
- client_connection.keyring,
330
- 'set_password',
332
+ client_connection,
333
+ 'set_keyring_password',
331
334
  lambda domain, identifier, password: set_password_calls.append((domain, identifier, password)),
332
335
  )
333
336
  monkeypatch.setattr(client_connection.click, 'secho', lambda message, **kwargs: secho_calls.append((message, kwargs)))
@@ -348,7 +351,7 @@ def test_connect_reports_keyring_save_error(monkeypatch: pytest.MonkeyPatch) ->
348
351
  def fail_set_password(*_args: Any) -> None:
349
352
  raise RuntimeError('locked')
350
353
 
351
- monkeypatch.setattr(client_connection.keyring, 'set_password', fail_set_password)
354
+ monkeypatch.setattr(client_connection, 'set_keyring_password', fail_set_password)
352
355
  monkeypatch.setattr(client_connection.click, 'secho', lambda message, **kwargs: secho_calls.append((message, kwargs)))
353
356
 
354
357
  client.connect(user='alice', host='db', port=3307, password_candidates=candidates, use_keyring=True)
@@ -356,6 +359,46 @@ def test_connect_reports_keyring_save_error(monkeypatch: pytest.MonkeyPatch) ->
356
359
  assert secho_calls == [('Password not saved to the system keyring: locked', {'err': True, 'fg': 'red'})]
357
360
 
358
361
 
362
+ def test_connect_saves_fallback_password_when_keyring_is_empty(monkeypatch: pytest.MonkeyPatch) -> None:
363
+ client = DummyClient()
364
+ FakeSQLExecute.effects = [op_error(1045), None]
365
+ writes: list[tuple[str, str, str]] = []
366
+ monkeypatch.setattr(client_connection.keyring, 'get_password', lambda *_args: None)
367
+ monkeypatch.setattr(client_connection.click, 'prompt', lambda *_args, **_kwargs: 'new-secret')
368
+ monkeypatch.setattr(client_connection, 'set_keyring_password', lambda *args: writes.append(args))
369
+
370
+ client.connect(user='alice', host='db', port=3307, use_keyring=True)
371
+
372
+ assert writes == [('mycli.net', 'alice@db:3307:', 'new-secret')]
373
+
374
+
375
+ def test_connect_does_not_rewrite_rejected_keyring_password(monkeypatch: pytest.MonkeyPatch) -> None:
376
+ client = DummyClient()
377
+ FakeSQLExecute.effects = [op_error(1045)]
378
+ writes: list[tuple[str, str, str]] = []
379
+ monkeypatch.setattr(client_connection.keyring, 'get_password', lambda *_args: 'old-secret')
380
+ monkeypatch.setattr(client_connection, 'set_keyring_password', lambda *args: writes.append(args))
381
+
382
+ with pytest.raises(SystemExit):
383
+ client.connect(user='alice', host='db', port=3307, use_keyring=True)
384
+
385
+ assert writes == []
386
+
387
+
388
+ @pytest.mark.parametrize('reset', [False, True])
389
+ def test_connect_only_rewrites_unchanged_password_for_reset(monkeypatch: pytest.MonkeyPatch, reset: bool) -> None:
390
+ client = DummyClient()
391
+ candidates = PasswordCandidates()
392
+ candidates.add_value('literal', 'secret')
393
+ writes: list[tuple[str, str, str]] = []
394
+ monkeypatch.setattr(client_connection.keyring, 'get_password', lambda *_args: 'secret')
395
+ monkeypatch.setattr(client_connection, 'set_keyring_password', lambda *args: writes.append(args))
396
+
397
+ client.connect(user='alice', host='db', port=3307, password_candidates=candidates, use_keyring=True, reset_keyring=reset)
398
+
399
+ assert writes == ([('mycli.net', 'alice@db:3307:', 'secret')] if reset else [])
400
+
401
+
359
402
  def test_connect_uses_ssh_jump_with_remote_socket(monkeypatch: pytest.MonkeyPatch) -> None:
360
403
  tunnel_calls: list[dict[str, Any]] = []
361
404
 
@@ -485,8 +528,8 @@ def test_connect_uses_kubectl_tunnel_with_resolved_database_port(monkeypatch: py
485
528
  lambda domain, identifier: keyring_calls.append(('get', domain, identifier)),
486
529
  )
487
530
  monkeypatch.setattr(
488
- client_connection.keyring,
489
- 'set_password',
531
+ client_connection,
532
+ 'set_keyring_password',
490
533
  lambda domain, identifier, password: keyring_calls.append(('set', domain, identifier, password)),
491
534
  )
492
535
  password_candidates = PasswordCandidates()