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.
- {mycli-2.3.0 → mycli-2.4.0}/AGENTS.md +2 -0
- {mycli-2.3.0/mycli.egg-info → mycli-2.4.0}/PKG-INFO +1 -1
- {mycli-2.3.0 → mycli-2.4.0}/changelog.md +21 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/TIPS +2 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/cli_runner.py +37 -19
- {mycli-2.3.0 → mycli-2.4.0}/mycli/client.py +8 -2
- {mycli-2.3.0 → mycli-2.4.0}/mycli/client_connection.py +24 -21
- {mycli-2.3.0 → mycli-2.4.0}/mycli/clitoolbar.py +13 -13
- {mycli-2.3.0 → mycli-2.4.0}/mycli/main.py +0 -9
- {mycli-2.3.0 → mycli-2.4.0}/mycli/main_modes/repl.py +9 -1
- {mycli-2.3.0 → mycli-2.4.0}/mycli/myclirc +16 -0
- mycli-2.4.0/mycli/password_sources.py +77 -0
- {mycli-2.3.0 → mycli-2.4.0/mycli.egg-info}/PKG-INFO +1 -1
- {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/SOURCES.txt +2 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/scm_file_list.json +2 -0
- mycli-2.4.0/mycli.egg-info/scm_version.json +8 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/myclirc +16 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_cli_runner.py +94 -9
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_client.py +32 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_client_connection.py +110 -49
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_clitoolbar.py +3 -3
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_main.py +33 -22
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_main_modes_repl.py +24 -0
- mycli-2.4.0/test/pytests/test_password_sources.py +81 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_iocommands.py +4 -2
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_tabular_output.py +4 -1
- mycli-2.3.0/mycli.egg-info/scm_version.json +0 -8
- {mycli-2.3.0 → mycli-2.4.0}/.git-blame-ignore-revs +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/.github/dependabot.yml +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/.github/workflows/ci.yml +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/.github/workflows/lint.yml +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/.github/workflows/publish.yml +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/.github/workflows/typecheck.yml +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/.gitignore +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/AUTHORS.rst +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/CONTRIBUTING.md +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/LICENSE.txt +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/MANIFEST.in +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/README.md +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/SPONSORS.rst +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/doc/key_bindings.rst +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/doc/llm.md +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/doc/screenshots/main.gif +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/doc/screenshots/tables.png +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/AUTHORS +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/SPONSORS +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/__init__.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/app_state.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/clibuffer.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/client_commands.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/client_query.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/clistyle.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/compat.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/completion_refresher.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/config.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/constants.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/key_bindings.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/lexer.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/main_modes/batch.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/main_modes/checkup.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/main_modes/execute.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/main_modes/list_dsn.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/output.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/__init__.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/batch_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/cli_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/completion_engine.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/filepaths.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/hybrid_redirection.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/interactive_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/key_binding_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/ptoolkit/__init__.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/ptoolkit/fzf.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/ptoolkit/history.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/ptoolkit/utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/__init__.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/dbcommands.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/delimitercommand.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/dsn_aliases.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/favoritequeries.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/iocommands.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/llm.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/main.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/special/utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/sql_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/sqlresult.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/string_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/tabular_output/__init__.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/packages/tabular_output/sql_format.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/schema_prefetcher.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/sqlcompleter.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/sqlexecute.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/ssh_tunnel.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/types.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli/vault.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/dependency_links.txt +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/entry_points.txt +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/requires.txt +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/mycli.egg-info/top_level.txt +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/pyproject.toml +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/setup.cfg +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/__init__.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/__init__.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/auto_vertical.feature +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/basic_commands.feature +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/connection.feature +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/crud_database.feature +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/crud_table.feature +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/db_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/environment.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/fixture_data/help_commands.txt +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/fixture_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/iocommands.feature +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/named_queries.feature +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/specials.feature +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/__init__.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/auto_vertical.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/basic_commands.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/connection.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/crud_database.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/crud_table.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/iocommands.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/named_queries.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/specials.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/steps/wrappers.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/features/wrappager.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/mylogin.cnf +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/conftest.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_app_state.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_batch_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_checkup.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_cli_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_clibuffer.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_client_commands.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_client_query.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_clistyle.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_completer_use_switch.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_completion_engine.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_completion_refresher.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_config.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_delimitercommand.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_dsn_aliases.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_favoritequeries.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_filepaths.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_hybrid_redirection.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_interactive_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_key_binding_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_key_bindings.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_lexer.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_main_modes_batch.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_main_modes_execute.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_main_modes_list_dsn.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_naive_completion.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_output.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_ptoolkit_fzf.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_ptoolkit_history.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_ptoolkit_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_schema_prefetcher.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_smart_completion_public_schema_only.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_dbcommands.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_init.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_llm.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_main.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_special_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_sql_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_sqlcompleter.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_sqlexecute.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_sqlresult.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_ssh_tunnel.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_string_utils.py +0 -0
- {mycli-2.3.0 → mycli-2.4.0}/test/pytests/test_vault.py +0 -0
- {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,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 =
|
|
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
|
-
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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", "
|
|
49
|
-
result.append(("class:bottom-toolbar.on",
|
|
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
|
|
@@ -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",
|
|
@@ -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
|