clickhouse-users-cli 0.1.1__tar.gz → 0.1.2__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 (19) hide show
  1. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/PKG-INFO +9 -1
  2. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/README.md +7 -0
  3. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/__init__.py +1 -1
  4. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/app.py +111 -15
  5. clickhouse_users_cli-0.1.2/clickhouse_users_cli/session.py +80 -0
  6. clickhouse_users_cli-0.1.2/clickhouse_users_cli/style.py +114 -0
  7. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/PKG-INFO +9 -1
  8. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/SOURCES.txt +1 -0
  9. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/requires.txt +1 -0
  10. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/pyproject.toml +1 -0
  11. clickhouse_users_cli-0.1.1/clickhouse_users_cli/style.py +0 -55
  12. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/LICENSE +0 -0
  13. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/db.py +0 -0
  14. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/sql_builder.py +0 -0
  15. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/validators.py +0 -0
  16. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/dependency_links.txt +0 -0
  17. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/entry_points.txt +0 -0
  18. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/top_level.txt +0 -0
  19. {clickhouse_users_cli-0.1.1 → clickhouse_users_cli-0.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clickhouse-users-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: CLI interativa para criar usuarios ClickHouse com privilegio minimo (least privilege)
5
5
  License-Expression: MIT
6
6
  Keywords: clickhouse,cli,users,access-control,least-privilege
@@ -19,6 +19,7 @@ License-File: LICENSE
19
19
  Requires-Dist: questionary>=2.0.0
20
20
  Requires-Dist: clickhouse-connect>=0.8.0
21
21
  Requires-Dist: rich>=13.0.0
22
+ Requires-Dist: pyyaml>=6.0
22
23
  Dynamic: license-file
23
24
 
24
25
  # ClickHouse Users CLI
@@ -47,6 +48,13 @@ Para listar/gerenciar também precisa de `SHOW USERS` + `ALTER USER` / `DROP USE
47
48
  Após conectar, escolha: **Criar usuário** · **Listar usuários** · **Gerenciar usuário** · **Sair**.
48
49
  O menu fica em loop — dá para criar, auditar e desativar vários sem reconectar.
49
50
 
51
+ ## Sessão salva (manter conectado)
52
+
53
+ - Na primeira conexão o CLI pergunta se quer **manter conectado**: salva `~/.ch-users/connection.yaml` com host, porta, protocolo, usuário e senha.
54
+ - Na próxima execução, oferece **usar a salva / nova conexão / apagar a salva**.
55
+ - No menu há **Esquecer sessão** (apaga o YAML, com confirmação).
56
+ - Segurança: a senha fica em **texto puro** no arquivo (permissão `0600` no Linux/mac; no Windows vale o aviso em tela). Só use em máquina confiável — nunca compartilhe o arquivo.
57
+
50
58
  ## Fluxo — criar (6 etapas)
51
59
 
52
60
  1. **Conexão** — host, protocolo, porta, usuário/senha admin. Testa com `SELECT 1` e permite tentar de novo / editar / sair.
@@ -24,6 +24,13 @@ Para listar/gerenciar também precisa de `SHOW USERS` + `ALTER USER` / `DROP USE
24
24
  Após conectar, escolha: **Criar usuário** · **Listar usuários** · **Gerenciar usuário** · **Sair**.
25
25
  O menu fica em loop — dá para criar, auditar e desativar vários sem reconectar.
26
26
 
27
+ ## Sessão salva (manter conectado)
28
+
29
+ - Na primeira conexão o CLI pergunta se quer **manter conectado**: salva `~/.ch-users/connection.yaml` com host, porta, protocolo, usuário e senha.
30
+ - Na próxima execução, oferece **usar a salva / nova conexão / apagar a salva**.
31
+ - No menu há **Esquecer sessão** (apaga o YAML, com confirmação).
32
+ - Segurança: a senha fica em **texto puro** no arquivo (permissão `0600` no Linux/mac; no Windows vale o aviso em tela). Só use em máquina confiável — nunca compartilhe o arquivo.
33
+
27
34
  ## Fluxo — criar (6 etapas)
28
35
 
29
36
  1. **Conexão** — host, protocolo, porta, usuário/senha admin. Testa com `SELECT 1` e permite tentar de novo / editar / sair.
@@ -1,5 +1,5 @@
1
1
  """ClickHouse Users CLI — crie usuários com escopo mínimo (least privilege)."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.1.2"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -6,7 +6,6 @@ import sys
6
6
 
7
7
  import questionary
8
8
  from rich.syntax import Syntax
9
- from rich.table import Table
10
9
 
11
10
  from clickhouse_users_cli.db import SYSTEM_DATABASES, ClickHouseAdmin, ConnectionInfo
12
11
  from clickhouse_users_cli.sql_builder import (
@@ -19,7 +18,8 @@ from clickhouse_users_cli.sql_builder import (
19
18
  build_deactivate_user_sql,
20
19
  build_drop_user_sql,
21
20
  )
22
- from clickhouse_users_cli.style import APP_STYLE, banner, console, error, info, step, success
21
+ from clickhouse_users_cli.style import APP_STYLE, banner, console, error, info, make_table, step, success, warn
22
+ from clickhouse_users_cli.session import SESSION_FILE
23
23
  from clickhouse_users_cli.validators import (
24
24
  validate_at_least_one,
25
25
  validate_host,
@@ -288,7 +288,7 @@ def ask_create_options() -> bool:
288
288
 
289
289
  def show_review(spec: UserSpec) -> None:
290
290
  step("6/6 · Revisão", "Confira o resumo e o SQL exato que será executado.")
291
- table = Table(title="Resumo do novo usuário", show_header=True, header_style="bold cyan")
291
+ table = make_table("Resumo do novo usuário")
292
292
  table.add_column("Campo", style="dim")
293
293
  table.add_column("Valor", style="bold")
294
294
  table.add_row("Usuário", spec.username)
@@ -326,18 +326,107 @@ def confirm_and_execute(admin: ClickHouseAdmin, spec: UserSpec) -> None:
326
326
 
327
327
 
328
328
 
329
+ # ── Sessão salva (YAML local, opt-in) ────────────────────────────────────────────
330
+
331
+ def maybe_load_saved_session() -> ConnectionInfo | None:
332
+ """Se há YAML salvo, oferece usar / nova conexão / apagar. None = digitar."""
333
+ from clickhouse_users_cli.session import delete_session, has_saved_session, load_session
334
+
335
+ if not has_saved_session():
336
+ return None
337
+ step("Sessão salva", f"Arquivo: {SESSION_FILE} (senha em texto puro — nunca compartilhe)")
338
+ try:
339
+ saved = load_session()
340
+ except RuntimeError as e:
341
+ error(str(e))
342
+ fix = _ask_or_abort(
343
+ Q.select(
344
+ "A sessão salva está inválida. O que fazer?",
345
+ choices=[
346
+ Q.Choice("Apagar e digitar nova conexão", value="delete"),
347
+ Q.Choice("Digitar nova conexão (manter arquivo)", value="new"),
348
+ ],
349
+ style=APP_STYLE, qmark="›",
350
+ )
351
+ )
352
+ if fix == "delete":
353
+ delete_session()
354
+ success("Sessão apagada.")
355
+ return None
356
+
357
+ info(f"Conexão guardada: {saved.protocol}://{saved.host}:{saved.port} · usuário '{saved.username}'.")
358
+ choice = _ask_or_abort(
359
+ Q.select(
360
+ "Usar a conexão salva?",
361
+ choices=[
362
+ Q.Choice("Usar conexão salva", value="use"),
363
+ Q.Choice("Nova conexão (digitar de novo)", value="new"),
364
+ Q.Choice("Apagar conexão salva", value="delete"),
365
+ ],
366
+ default="use",
367
+ style=APP_STYLE, qmark="›",
368
+ instruction="(↑↓ navega · enter confirma)",
369
+ )
370
+ )
371
+ if choice == "delete":
372
+ delete_session()
373
+ success("Sessão apagada.")
374
+ return None
375
+ if choice == "new":
376
+ return None
377
+ return saved
378
+
379
+
380
+ def ask_remember(conn: ConnectionInfo) -> None:
381
+ """Opt-in: salva a conexão em YAML local (senha em texto puro)."""
382
+ from clickhouse_users_cli.session import save_session
383
+
384
+ warn("A senha será gravada em texto puro no arquivo — só use em máquina confiável.")
385
+ keep = _ask_or_abort(
386
+ Q.confirm("Manter conectado neste computador (salvar sessão)?", default=False, style=APP_STYLE, qmark="›")
387
+ )
388
+ if not keep:
389
+ return
390
+ try:
391
+ path = save_session(conn)
392
+ except RuntimeError as e:
393
+ error(str(e))
394
+ return
395
+ success(f"Sessão salva em {path}")
396
+
397
+
398
+ def flow_forget_session() -> None:
399
+ from clickhouse_users_cli.session import delete_session, has_saved_session
400
+
401
+ if not has_saved_session():
402
+ info("Nenhuma sessão salva.")
403
+ return
404
+ step("Esquecer sessão", f"Arquivo: {SESSION_FILE}")
405
+ go = _ask_or_abort(
406
+ Q.confirm("Apagar a sessão salva (vai pedir credenciais na próxima vez)?", default=False, style=APP_STYLE, qmark="›")
407
+ )
408
+ if not go:
409
+ console.print("[dim]Nada foi apagado.[/]")
410
+ return
411
+ delete_session()
412
+ success("Sessão apagada.")
413
+
414
+
329
415
  # ── Menu principal + listar/gerenciar ────────────────────────────────────────────
330
416
 
331
- def ask_main_action() -> str:
417
+ def ask_main_action(show_forget: bool = False) -> str:
418
+ choices = [
419
+ Q.Choice("Criar usuário — com privilégio mínimo", value="create"),
420
+ Q.Choice("Listar usuários — SHOW USERS + grants", value="list"),
421
+ Q.Choice("Gerenciar usuário — desativar / reativar / excluir", value="manage"),
422
+ ]
423
+ if show_forget:
424
+ choices.append(Q.Choice("Esquecer sessão — apagar YAML salvo", value="forget"))
425
+ choices.append(Q.Choice("Sair", value="exit"))
332
426
  return _ask_or_abort(
333
427
  Q.select(
334
428
  "O que deseja fazer?",
335
- choices=[
336
- Q.Choice("Criar usuário — com privilégio mínimo", value="create"),
337
- Q.Choice("Listar usuários — SHOW USERS + grants", value="list"),
338
- Q.Choice("Gerenciar usuário — desativar / reativar / excluir", value="manage"),
339
- Q.Choice("Sair", value="exit"),
340
- ],
429
+ choices=choices,
341
430
  default="create",
342
431
  style=APP_STYLE, qmark="›",
343
432
  instruction="(↑↓ navega · enter confirma)",
@@ -362,7 +451,7 @@ def show_user_details(admin: ClickHouseAdmin, username: str) -> tuple[str, list[
362
451
  error(f"Falha ao ver grants de '{username}': {e}")
363
452
  grants = []
364
453
 
365
- table = Table(title=f"Usuário {username}", show_header=True, header_style="bold cyan")
454
+ table = make_table(f"Usuário {username}")
366
455
  table.add_column("Campo", style="dim")
367
456
  table.add_column("Valor", style="bold")
368
457
  status = "inativo (HOST NONE)" if _is_inactive(create_sql) else "ativo"
@@ -392,7 +481,7 @@ def flow_list_users(admin: ClickHouseAdmin) -> None:
392
481
  info("Nenhum usuário encontrado.")
393
482
  return
394
483
 
395
- table = Table(title=f"{len(users)} usuário(s)", show_header=True, header_style="bold cyan")
484
+ table = make_table(f"{len(users)} usuário(s)")
396
485
  table.add_column("#", style="dim")
397
486
  table.add_column("Usuário", style="bold")
398
487
  for i, u in enumerate(users, 1):
@@ -440,7 +529,7 @@ def flow_manage_users(admin: ClickHouseAdmin) -> None:
440
529
  inactive = _is_inactive(create_sql)
441
530
 
442
531
  if target == admin.conn.username:
443
- error(f" Atenção: '{target}' é o usuário conectado agora. Desativar/excluir pode te derrubar.")
532
+ warn(f"'{target}' é o usuário conectado agora. Desativar/excluir pode te derrubar.")
444
533
 
445
534
  options = []
446
535
  if inactive:
@@ -518,17 +607,24 @@ def flow_create_user(admin: ClickHouseAdmin) -> None:
518
607
  def main() -> None:
519
608
  try:
520
609
  banner()
521
- conn = ask_connection()
610
+ from clickhouse_users_cli.session import has_saved_session
611
+
612
+ saved_conn = maybe_load_saved_session()
613
+ conn = saved_conn or ask_connection()
522
614
  admin = connect_with_retry(conn)
615
+ if saved_conn is None:
616
+ ask_remember(conn)
523
617
 
524
618
  while True:
525
- action = ask_main_action()
619
+ action = ask_main_action(show_forget=has_saved_session())
526
620
  if action == "create":
527
621
  flow_create_user(admin)
528
622
  elif action == "list":
529
623
  flow_list_users(admin)
530
624
  elif action == "manage":
531
625
  flow_manage_users(admin)
626
+ elif action == "forget":
627
+ flow_forget_session()
532
628
  else:
533
629
  console.print("[dim]Até logo![/]")
534
630
  return
@@ -0,0 +1,80 @@
1
+ """Sessão local opcional — salva a conexão admin em YAML para reutilizar.
2
+
3
+ Segurança: a senha fica em texto puro no arquivo. O arquivo é criado com
4
+ permissão restrita (0600 no POSIX) e o salvamento é sempre opt-in explícito.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+
12
+ from clickhouse_users_cli.db import ConnectionInfo
13
+
14
+ SESSION_DIR = Path.home() / ".ch-users"
15
+ SESSION_FILE = SESSION_DIR / "connection.yaml"
16
+
17
+
18
+ def has_saved_session() -> bool:
19
+ return SESSION_FILE.is_file()
20
+
21
+
22
+ def describe_session(data: dict) -> str:
23
+ proto = "https" if data.get("secure") else "http"
24
+ return f"{proto}://{data.get('host')}:{data.get('port')} · usuário '{data.get('username')}'"
25
+
26
+
27
+ def save_session(conn: ConnectionInfo) -> Path:
28
+ """Grava a conexão em YAML com permissão restrita. Retorna o caminho."""
29
+ try:
30
+ import yaml
31
+ except ImportError as e: # pragma: no cover
32
+ raise RuntimeError("Dependência 'pyyaml' não instalada. Rode: pip install pyyaml") from e
33
+
34
+ SESSION_DIR.mkdir(parents=True, exist_ok=True)
35
+ payload = {
36
+ "host": conn.host,
37
+ "port": conn.port,
38
+ "secure": conn.secure,
39
+ "username": conn.username,
40
+ "password": conn.password,
41
+ }
42
+ with open(SESSION_FILE, "w", encoding="utf-8") as f:
43
+ yaml.safe_dump(payload, f, allow_unicode=True, sort_keys=False)
44
+ try:
45
+ os.chmod(SESSION_FILE, 0o600)
46
+ except OSError:
47
+ pass # Windows: melhor esforço; o aviso em tela cobre o risco
48
+ return SESSION_FILE
49
+
50
+
51
+ def load_session() -> ConnectionInfo:
52
+ """Lê o YAML e devolve ConnectionInfo. Erro amigável se ausente/corrompido."""
53
+ try:
54
+ import yaml
55
+ except ImportError as e: # pragma: no cover
56
+ raise RuntimeError("Dependência 'pyyaml' não instalada. Rode: pip install pyyaml") from e
57
+
58
+ try:
59
+ with open(SESSION_FILE, encoding="utf-8") as f:
60
+ data = yaml.safe_load(f) or {}
61
+ return ConnectionInfo(
62
+ host=str(data["host"]),
63
+ port=int(data["port"]),
64
+ secure=bool(data["secure"]),
65
+ username=str(data["username"]),
66
+ password=str(data.get("password") or ""),
67
+ )
68
+ except KeyError as e:
69
+ raise RuntimeError(f"Arquivo de sessão incompleto (falta {e}). Apague e salve de novo.") from e
70
+ except (OSError, ValueError, TypeError) as e:
71
+ raise RuntimeError(f"Não foi possível ler a sessão salva: {e}") from e
72
+
73
+
74
+ def delete_session() -> bool:
75
+ """Apaga o YAML. Retorna True se algo foi removido."""
76
+ try:
77
+ SESSION_FILE.unlink()
78
+ return True
79
+ except FileNotFoundError:
80
+ return False
@@ -0,0 +1,114 @@
1
+ """Estilo centralizado (UI) + helpers de apresentação com Rich."""
2
+
3
+ from questionary import Style
4
+ from rich.align import Align
5
+ from rich.box import DOUBLE, ROUNDED
6
+ from rich.console import Console, Group
7
+ from rich.panel import Panel
8
+ from rich.rule import Rule
9
+ from rich.table import Table
10
+ from rich.text import Text
11
+
12
+ from clickhouse_users_cli import __version__
13
+
14
+ console = Console()
15
+
16
+ CYAN = "#00d7ff"
17
+ MAGENTA = "#c792ea"
18
+ GREEN = "#00ff87"
19
+ RED = "#ff5555"
20
+ AMBER = "#ffcb6b"
21
+ MUTED = "#6c7086"
22
+ BORDER = "#3a3f4b"
23
+
24
+ # Estilo questionary: alto contraste, foco visível, perigo em vermelho.
25
+ # Mantém a navegação por teclado óbvia (setas + espaço + enter).
26
+ APP_STYLE = Style(
27
+ [
28
+ ("qmark", "fg:#00d7ff bold"), # "?" das perguntas
29
+ ("question", "fg:#ffffff bold"), # texto da pergunta
30
+ ("answer", "fg:#00ff87 bold"), # resposta escolhida
31
+ ("pointer", "fg:#00d7ff bold"), # "»" do item focado
32
+ ("highlighted", "fg:#00d7ff bold"), # item focado
33
+ ("selected", "fg:#00ff87"), # checkbox marcado "●"
34
+ ("separator", "fg:#6c7086"),
35
+ ("instruction", "fg:#a6adc8 italic"),
36
+ ("text", "fg:#ffffff"),
37
+ ("disabled", "fg:#6c7086 italic"),
38
+ ]
39
+ )
40
+
41
+
42
+ def _logo_title() -> Text:
43
+ """Título em degradê ciano → magenta."""
44
+ t = Text(no_wrap=True)
45
+ t.append("⬢ ", style=f"bold {CYAN}")
46
+ t.append("ClickHouse", style=f"bold {CYAN}")
47
+ t.append(" ", style="bold")
48
+ t.append("Users CLI", style=f"bold {MAGENTA}")
49
+ return t
50
+
51
+
52
+ def banner() -> None:
53
+ mark = Text("╭──────────╮\n│ ▓▓▓▓▓▓▓▓ │\n│ │\n╰──────────╯", style=f"bold {CYAN}", justify="center")
54
+ body = Group(
55
+ Align.center(mark),
56
+ Align.center(_logo_title()),
57
+ Align.center(Text("Crie usuários com escopo mínimo necessário", style="dim")),
58
+ Align.center(Text("criar ◆ listar ◆ desativar ◆ auditar", style=f"bold {CYAN}")),
59
+ )
60
+ console.print(
61
+ Panel(
62
+ body,
63
+ box=DOUBLE,
64
+ border_style=CYAN,
65
+ title=f"[black on {CYAN}] v{__version__} ",
66
+ subtitle="[dim]least privilege por padrão[/]",
67
+ padding=(1, 4),
68
+ )
69
+ )
70
+
71
+
72
+ def step(title: str, subtitle: str = "") -> None:
73
+ """Cabeçalho de etapa — orienta o usuário sobre 'onde estou' no fluxo."""
74
+ console.print()
75
+ console.print(Rule(f"[black on {CYAN}] {title} [/]", style=CYAN))
76
+ if subtitle:
77
+ console.print(f" [dim]{subtitle}[/]")
78
+
79
+
80
+ def make_table(title: str) -> Table:
81
+ """Tabela padrão: bordas arredondadas, cabeçalho ciano."""
82
+ return Table(
83
+ title=f"[bold white]{title}[/]",
84
+ show_header=True,
85
+ header_style=f"bold {CYAN}",
86
+ border_style=BORDER,
87
+ box=ROUNDED,
88
+ show_lines=False,
89
+ padding=(0, 1),
90
+ )
91
+
92
+
93
+ def success(msg: str) -> None:
94
+ console.print(f"[black on {GREEN}] ✔ [/] [bold {GREEN}]{msg}[/]")
95
+
96
+
97
+ def error(msg: str) -> None:
98
+ console.print(
99
+ Panel(
100
+ f"[bold {RED}]{msg}[/]",
101
+ title=f"[bold {RED}]✘ Erro[/]",
102
+ border_style=RED,
103
+ box=ROUNDED,
104
+ padding=(0, 1),
105
+ )
106
+ )
107
+
108
+
109
+ def warn(msg: str) -> None:
110
+ console.print(f"[black on {AMBER}] ! [/] [{AMBER}]{msg}[/]")
111
+
112
+
113
+ def info(msg: str) -> None:
114
+ console.print(f"[dim]▸ {msg}[/]")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clickhouse-users-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: CLI interativa para criar usuarios ClickHouse com privilegio minimo (least privilege)
5
5
  License-Expression: MIT
6
6
  Keywords: clickhouse,cli,users,access-control,least-privilege
@@ -19,6 +19,7 @@ License-File: LICENSE
19
19
  Requires-Dist: questionary>=2.0.0
20
20
  Requires-Dist: clickhouse-connect>=0.8.0
21
21
  Requires-Dist: rich>=13.0.0
22
+ Requires-Dist: pyyaml>=6.0
22
23
  Dynamic: license-file
23
24
 
24
25
  # ClickHouse Users CLI
@@ -47,6 +48,13 @@ Para listar/gerenciar também precisa de `SHOW USERS` + `ALTER USER` / `DROP USE
47
48
  Após conectar, escolha: **Criar usuário** · **Listar usuários** · **Gerenciar usuário** · **Sair**.
48
49
  O menu fica em loop — dá para criar, auditar e desativar vários sem reconectar.
49
50
 
51
+ ## Sessão salva (manter conectado)
52
+
53
+ - Na primeira conexão o CLI pergunta se quer **manter conectado**: salva `~/.ch-users/connection.yaml` com host, porta, protocolo, usuário e senha.
54
+ - Na próxima execução, oferece **usar a salva / nova conexão / apagar a salva**.
55
+ - No menu há **Esquecer sessão** (apaga o YAML, com confirmação).
56
+ - Segurança: a senha fica em **texto puro** no arquivo (permissão `0600` no Linux/mac; no Windows vale o aviso em tela). Só use em máquina confiável — nunca compartilhe o arquivo.
57
+
50
58
  ## Fluxo — criar (6 etapas)
51
59
 
52
60
  1. **Conexão** — host, protocolo, porta, usuário/senha admin. Testa com `SELECT 1` e permite tentar de novo / editar / sair.
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  clickhouse_users_cli/__init__.py
5
5
  clickhouse_users_cli/app.py
6
6
  clickhouse_users_cli/db.py
7
+ clickhouse_users_cli/session.py
7
8
  clickhouse_users_cli/sql_builder.py
8
9
  clickhouse_users_cli/style.py
9
10
  clickhouse_users_cli/validators.py
@@ -1,3 +1,4 @@
1
1
  questionary>=2.0.0
2
2
  clickhouse-connect>=0.8.0
3
3
  rich>=13.0.0
4
+ pyyaml>=6.0
@@ -25,6 +25,7 @@ dependencies = [
25
25
  "questionary>=2.0.0",
26
26
  "clickhouse-connect>=0.8.0",
27
27
  "rich>=13.0.0",
28
+ "pyyaml>=6.0",
28
29
  ]
29
30
 
30
31
  [project.scripts]
@@ -1,55 +0,0 @@
1
- """Estilo centralizado (UI) + helpers de apresentação com Rich."""
2
-
3
- from questionary import Style
4
- from rich.console import Console
5
- from rich.panel import Panel
6
- from rich.rule import Rule
7
-
8
- console = Console()
9
-
10
- # Estilo questionary: alto contraste, foco visível, perigo em vermelho.
11
- # Mantém a navegação por teclado óbvia (setas + espaço + enter).
12
- APP_STYLE = Style(
13
- [
14
- ("qmark", "fg:#00d7ff bold"), # "?" das perguntas
15
- ("question", "fg:#ffffff bold"), # texto da pergunta
16
- ("answer", "fg:#00ff87 bold"), # resposta escolhida
17
- ("pointer", "fg:#00d7ff bold"), # "»" do item focado
18
- ("highlighted", "fg:#00d7ff bold"), # item focado
19
- ("selected", "fg:#00ff87"), # checkbox marcado "●"
20
- ("separator", "fg:#6c7086"),
21
- ("instruction", "fg:#a6adc8 italic"),
22
- ("text", "fg:#ffffff"),
23
- ("disabled", "fg:#6c7086 italic"),
24
- ]
25
- )
26
-
27
-
28
- def banner() -> None:
29
- console.print(
30
- Panel.fit(
31
- "[bold cyan]ClickHouse Users CLI[/]\n"
32
- "[dim]Crie usuários com escopo mínimo necessário (least privilege)[/dim]",
33
- border_style="cyan",
34
- )
35
- )
36
-
37
-
38
- def step(title: str, subtitle: str = "") -> None:
39
- """Cabeçalho de etapa — orienta o usuário sobre 'onde estou' no fluxo."""
40
- console.print()
41
- console.print(Rule(f"[bold cyan]{title}[/]", style="cyan"))
42
- if subtitle:
43
- console.print(f"[dim]{subtitle}[/]")
44
-
45
-
46
- def success(msg: str) -> None:
47
- console.print(f"[bold green]✔ {msg}[/]")
48
-
49
-
50
- def error(msg: str) -> None:
51
- console.print(f"[bold red]✘ {msg}[/]")
52
-
53
-
54
- def info(msg: str) -> None:
55
- console.print(f"[dim]{msg}[/]")