Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Upcoming (TBD)
Features
--------
* Populate history file in Docker with examples.
* Faster dynamic completions via faster `--list-dsn`.


Bug Fixes
Expand Down
51 changes: 27 additions & 24 deletions mycli/cli_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
'mycli.main_modes.list_dsn',
'mycli.packages.credentials.password_sources',
'mycli.packages.integrations.vault.vault_credentials',
'mycli.packages.special_commands.dsn_aliases',
'mycli.packages.utils.dsn_utils',
'mycli.packages.utils.cli_utils',
]

Expand All @@ -32,7 +32,7 @@
from mycli.main_modes.checkup import main_checkup
from mycli.main_modes.completions import main_completions
from mycli.main_modes.execute import main_execute_from_cli
from mycli.main_modes.list_dsn import main_list_dsn
from mycli.main_modes.list_dsn import load_list_dsn_config, main_list_dsn
from mycli.packages.credentials.password_sources import PasswordCandidates
from mycli.packages.integrations.vault.vault_credentials import (
DEFAULT_VAULT_EXECUTABLE,
Expand All @@ -41,8 +41,8 @@
VaultError,
get_field_from_vault,
)
from mycli.packages.special_commands.dsn_aliases import INVALID_DSN_ALIAS_ERROR, is_valid_dsn_alias
from mycli.packages.utils.cli_utils import is_valid_connection_scheme
from mycli.packages.utils.dsn_utils import INVALID_DSN_ALIAS_ERROR, is_valid_dsn_alias

if TYPE_CHECKING:
from mycli.main import CliArgs
Expand Down Expand Up @@ -122,7 +122,7 @@ def expand_dsn_alias_env_vars(
)


def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> None:
def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory | None = None) -> None:
from mycli import main as main_module

if cli_args.completions:
Expand All @@ -131,6 +131,29 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non

cli_verbosity = main_module.preprocess_cli_args(cli_args, is_valid_connection_scheme)

if not cli_args.checkup:
if cli_args.csv and cli_args.format not in [None, 'csv']:
click.secho("Conflicting --csv and --format arguments.", err=True, fg="red")
sys.exit(1)

if cli_args.table and cli_args.format not in [None, 'table']:
click.secho("Conflicting --table and --format arguments.", err=True, fg="red")
sys.exit(1)

if not cli_args.format:
cli_args.format = 'default'
if cli_args.csv:
cli_args.format = 'csv'
if cli_args.table:
cli_args.format = 'table'

if cli_args.list_dsn:
config = load_list_dsn_config(cli_args.myclirc)
verbosity = cli_verbosity or (-1 if config['main'].as_bool('less_chatty') else 0)
sys.exit(main_list_dsn(config, verbosity))

if client_factory is None:
client_factory = main_module.MyCli
mycli = client_factory(
prompt=cli_args.prompt,
toolbar_format=cli_args.toolbar,
Expand All @@ -147,26 +170,6 @@ def run_from_cli_args(cli_args: 'CliArgs', client_factory: ClientFactory) -> Non
main_checkup(mycli)
sys.exit(0)

if cli_args.csv and cli_args.format not in [None, 'csv']:
click.secho("Conflicting --csv and --format arguments.", err=True, fg="red")
sys.exit(1)

if cli_args.table and cli_args.format not in [None, 'table']:
click.secho("Conflicting --table and --format arguments.", err=True, fg="red")
sys.exit(1)

if not cli_args.format:
cli_args.format = 'default'

if cli_args.csv:
cli_args.format = 'csv'

if cli_args.table:
cli_args.format = 'table'

if cli_args.list_dsn:
sys.exit(main_list_dsn(mycli))

dsn_uri = None
dsn_password: str | None = None
database: str | None = ''
Expand Down
15 changes: 6 additions & 9 deletions mycli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@
from mycli.client_connection import ClientConnectionMixin
from mycli.client_query import ClientQueryMixin
from mycli.config import (
SYSTEM_CONFIG_FILES,
get_mylogin_cnf_path,
get_xdg_config_file,
open_mylogin_cnf,
read_config_file,
read_config_files,
resolve_user_config_path,
write_default_config,
)
from mycli.constants import DEFAULT_PROMPT
Expand Down Expand Up @@ -61,10 +64,8 @@ class MyCli(AppStateMixin, OutputMixin, ClientCommandsMixin, ClientConnectionMix

# check XDG_CONFIG_HOME exists and not an empty string
xdg_config_home = os.environ.get("XDG_CONFIG_HOME", "~/.config")
xdg_config_file = os.path.join(os.path.expanduser(xdg_config_home), "mycli", "myclirc")
system_config_files: list[str | IO[str]] = [
"/etc/myclirc",
]
xdg_config_file = get_xdg_config_file()
system_config_files: list[str | IO[str]] = SYSTEM_CONFIG_FILES

def __init__(
self,
Expand Down Expand Up @@ -92,11 +93,7 @@ def __init__(
self.sandbox_mode: bool = False
self.checkpoint: IO | None = None

if myclirc is None:
if os.path.exists(self.xdg_config_file):
myclirc = self.xdg_config_file
else:
myclirc = '~/.myclirc'
myclirc = resolve_user_config_path(myclirc, xdg_config_file=self.xdg_config_file)
self.myclirc_path = os.path.abspath(os.path.expanduser(myclirc))

# Load config.
Expand Down
18 changes: 18 additions & 0 deletions mycli/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
__lazy_modules__ = ['Cryptodome.Cipher']

from copy import copy
from importlib import resources
from io import BytesIO, TextIOWrapper
Expand All @@ -12,6 +14,22 @@
from Cryptodome.Cipher import AES

logger = logging.getLogger(__name__)
SYSTEM_CONFIG_FILES: list[str | IO[str]] = ['/etc/myclirc']


def get_xdg_config_file() -> str:
return os.path.join(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')), 'mycli', 'myclirc')


def resolve_user_config_path(myclirc: str | None, *, xdg_config_file: str | None = None) -> str:
if myclirc is not None:
return myclirc
xdg_config_file = get_xdg_config_file() if xdg_config_file is None else xdg_config_file
return xdg_config_file if os.path.exists(xdg_config_file) else '~/.myclirc'


def read_user_config(myclirc: str | None) -> ConfigObj:
return read_config_files(SYSTEM_CONFIG_FILES + [resolve_user_config_path(myclirc)])


class SQLValuePreservingConfigObj(ConfigObj):
Expand Down
4 changes: 2 additions & 2 deletions mycli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

import mycli as mycli_package
from mycli.cli_runner import run_from_cli_args
from mycli.client import MyCli
from mycli.client import MyCli as MyCli
from mycli.constants import (
DEFAULT_PROMPT,
EMPTY_PASSWORD_FLAG_SENTINEL,
Expand Down Expand Up @@ -482,7 +482,7 @@ def click_entrypoint(

"""

run_from_cli_args(cli_args, client_factory=MyCli)
run_from_cli_args(cli_args, client_factory=None)


def main() -> int | None:
Expand Down
19 changes: 11 additions & 8 deletions mycli/main_modes/list_dsn.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import click
from configobj import ConfigObj

from mycli.config import read_user_config
from mycli.packages.utils.dsn_utils import is_valid_dsn_alias, merge_shared_dsns

from mycli.packages.special_commands.dsn_aliases import is_valid_dsn_alias

if TYPE_CHECKING:
from mycli.client import MyCli
def load_list_dsn_config(myclirc: str | None) -> ConfigObj:
config = read_user_config(myclirc)
merge_shared_dsns(config, config['main'].get('shared_dsns_file'))
return config


def main_list_dsn(mycli: 'MyCli') -> int:
def main_list_dsn(config: ConfigObj, verbosity: int) -> int:
try:
alias_dsn = mycli.config['alias_dsn']
alias_dsn = config['alias_dsn']
except KeyError:
click.secho('Invalid DSNs found in the config file. Please check the "[alias_dsn]" section in myclirc.', err=True, fg='red')
return 1
Expand All @@ -22,7 +25,7 @@ def main_list_dsn(mycli: 'MyCli') -> int:
for alias, value in alias_dsn.items():
if not is_valid_dsn_alias(alias):
continue
if mycli.verbosity >= 1:
if verbosity >= 1:
click.secho(f'{alias} : {value}')
else:
click.secho(alias)
Expand Down
41 changes: 3 additions & 38 deletions mycli/packages/special_commands/dsn_aliases.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
from __future__ import annotations

import logging
import os
from typing import TYPE_CHECKING, Any
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit

from mycli.config import log, read_config_file, str_to_bool
from mycli.config import read_config_file, str_to_bool
from mycli.constants import DEFAULT_CHARSET, DEFAULT_PROMPT, KNOWN_DSN_QUERY_PARAMS

logger = logging.getLogger(__name__)
from mycli.packages.utils.dsn_utils import INVALID_DSN_ALIAS_ERROR, is_valid_dsn_alias, merge_shared_dsns

if TYPE_CHECKING:
from mycli.client import MyCli

DSN_SUBCOMMANDS = {'help', 'list', 'show', 'save', 'edit', 'delete'}
INVALID_DSN_ALIAS_ERROR = 'Error: DSN aliases cannot start with a dash.'
MISSING = object()

SSL_QUERY_PARAMS = {
Expand All @@ -29,10 +26,6 @@
}


def is_valid_dsn_alias(alias: str) -> bool:
return not alias.startswith('-')


def _config_bool(value: Any) -> bool:
try:
return str_to_bool(value)
Expand Down Expand Up @@ -108,35 +101,7 @@ def from_config(
shared_dsns_file: str | None = None,
) -> DsnAliases:
aliases = cls(config, mycli, config_file)
if not shared_dsns_file:
return aliases

shared_dsns_file = os.path.expanduser(shared_dsns_file)
if not os.path.isabs(shared_dsns_file):
log(
logger,
logging.WARNING,
f"Shared DSNs file path must be absolute: '{shared_dsns_file}'.",
)
return aliases

if not os.path.isfile(shared_dsns_file):
log(
logger,
logging.WARNING,
f"Unable to read shared DSNs file '{shared_dsns_file}'.",
)
return aliases

shared_config = read_config_file(shared_dsns_file)
if shared_config is None:
return aliases

configured_aliases = config.get(cls.section_name, {})
shared_aliases = shared_config.get(cls.section_name, {})
config[cls.section_name] = {}
config[cls.section_name].update(shared_aliases)
config[cls.section_name].update(configured_aliases)
merge_shared_dsns(config, shared_dsns_file)
return aliases

def _config_for_write(self) -> Any:
Expand Down
37 changes: 37 additions & 0 deletions mycli/packages/utils/dsn_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from __future__ import annotations

import logging
import os
from typing import Any

from mycli.config import log, read_config_file

logger = logging.getLogger(__name__)
INVALID_DSN_ALIAS_ERROR = 'Error: DSN aliases cannot start with a dash.'


def is_valid_dsn_alias(alias: str) -> bool:
return not alias.startswith('-')


def merge_shared_dsns(config: Any, shared_dsns_file: str | None) -> None:
if not shared_dsns_file:
return

shared_dsns_file = os.path.expanduser(shared_dsns_file)
if not os.path.isabs(shared_dsns_file):
log(logger, logging.WARNING, f"Shared DSNs file path must be absolute: '{shared_dsns_file}'.")
return
if not os.path.isfile(shared_dsns_file):
log(logger, logging.WARNING, f"Unable to read shared DSNs file '{shared_dsns_file}'.")
return

shared_config = read_config_file(shared_dsns_file)
if shared_config is None:
return

configured_aliases = config.get('alias_dsn', {})
shared_aliases = shared_config.get('alias_dsn', {})
config['alias_dsn'] = {}
config['alias_dsn'].update(shared_aliases)
config['alias_dsn'].update(configured_aliases)
14 changes: 8 additions & 6 deletions mycli_test/pytests/test_cli_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from types import SimpleNamespace
from typing import Any

from configobj import ConfigObj
import pytest

from mycli import cli_runner, main
Expand Down Expand Up @@ -1418,21 +1419,22 @@ def test_run_from_cli_args_sets_legacy_format_flags(
def test_run_from_cli_args_list_dsn_exits_with_mode_result(monkeypatch: pytest.MonkeyPatch) -> None:
cli_args = make_cli_args()
cli_args.list_dsn = True
client = DummyMyCli()
list_dsn_calls: list[DummyMyCli] = []
config = ConfigObj({'main': {'less_chatty': False}, 'alias_dsn': {}})
list_dsn_calls: list[tuple[ConfigObj, int]] = []
monkeypatch.setattr(main, 'preprocess_cli_args', lambda args, scheme_validator: 0)
monkeypatch.setattr(cli_runner, 'load_list_dsn_config', lambda path: config)

def fake_main_list_dsn(value: DummyMyCli) -> int:
list_dsn_calls.append(value)
def fake_main_list_dsn(value: ConfigObj, verbosity: int) -> int:
list_dsn_calls.append((value, verbosity))
return 7

monkeypatch.setattr(cli_runner, 'main_list_dsn', fake_main_list_dsn)

with pytest.raises(SystemExit) as excinfo:
cli_runner.run_from_cli_args(cli_args, lambda **_kwargs: client)
cli_runner.run_from_cli_args(cli_args, lambda **_kwargs: pytest.fail('client factory called'))

assert excinfo.value.code == 7
assert list_dsn_calls == [client]
assert list_dsn_calls == [(config, 0)]


def test_run_from_cli_args_maps_dsn_socket_parameter(monkeypatch: pytest.MonkeyPatch) -> None:
Expand Down
Loading
Loading