clickhouse-users-cli 0.1.0__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.0 → clickhouse_users_cli-0.1.2}/PKG-INFO +10 -2
  2. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/README.md +8 -1
  3. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/__init__.py +1 -1
  4. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/app.py +116 -18
  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.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/PKG-INFO +10 -2
  8. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/SOURCES.txt +1 -0
  9. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/requires.txt +1 -0
  10. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/pyproject.toml +1 -0
  11. clickhouse_users_cli-0.1.0/clickhouse_users_cli/style.py +0 -55
  12. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/LICENSE +0 -0
  13. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/db.py +0 -0
  14. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/sql_builder.py +0 -0
  15. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli/validators.py +0 -0
  16. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/dependency_links.txt +0 -0
  17. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/entry_points.txt +0 -0
  18. {clickhouse_users_cli-0.1.0 → clickhouse_users_cli-0.1.2}/clickhouse_users_cli.egg-info/top_level.txt +0 -0
  19. {clickhouse_users_cli-0.1.0 → 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.0
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.
@@ -88,7 +96,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
88
96
 
89
97
  Decisões de UX:
90
98
 
91
- - **Sistemas por último e desmarcados** (`system`, `information_schema`) para evitar `GRANT` acidental.
99
+ - **Nada pré-marcado + opção `✓ Todos os bancos`** (least privilege: acesso só via opt-in explícito; sistemas vão por último com tag `(sistema)`).
92
100
  - **Ctrl+C = saída limpa**, nada executa sem confirmação final.
93
101
  - **SQL separado da UI** (`clickhouse_users_cli/sql_builder.py`) — dá para testar sem banco.
94
102
  - **Escape correto** de identificadores (backticks) e strings (aspas simples).
@@ -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.
@@ -65,7 +72,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
65
72
 
66
73
  Decisões de UX:
67
74
 
68
- - **Sistemas por último e desmarcados** (`system`, `information_schema`) para evitar `GRANT` acidental.
75
+ - **Nada pré-marcado + opção `✓ Todos os bancos`** (least privilege: acesso só via opt-in explícito; sistemas vão por último com tag `(sistema)`).
69
76
  - **Ctrl+C = saída limpa**, nada executa sem confirmação final.
70
77
  - **SQL separado da UI** (`clickhouse_users_cli/sql_builder.py`) — dá para testar sem banco.
71
78
  - **Escape correto** de identificadores (backticks) e strings (aspas simples).
@@ -1,5 +1,5 @@
1
1
  """ClickHouse Users CLI — crie usuários com escopo mínimo (least privilege)."""
2
2
 
3
- __version__ = "0.1.0"
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,
@@ -152,12 +152,12 @@ def ask_databases(admin: ClickHouseAdmin) -> list[str]:
152
152
  error("Nenhum banco encontrado.")
153
153
  raise SystemExit(1)
154
154
 
155
- choices = [
155
+ choices = [Q.Choice(title="✓ Todos os bancos", value="__ALL__")] + [
156
156
  Q.Choice(
157
157
  title=f"{db} (sistema)" if db in SYSTEM_DATABASES else db,
158
158
  value=db,
159
- # UX: sistema desmarcado por padrão para evitar GRANT acidental.
160
- checked=db not in SYSTEM_DATABASES,
159
+ # UX: nada pré-marcado (least privilege) — o usuário opta explicitamente.
160
+ checked=False,
161
161
  )
162
162
  for db in databases
163
163
  ]
@@ -170,6 +170,8 @@ def ask_databases(admin: ClickHouseAdmin) -> list[str]:
170
170
  instruction="(espaço marca · enter confirma)",
171
171
  )
172
172
  )
173
+ if "__ALL__" in selected:
174
+ return list(databases)
173
175
  return selected
174
176
 
175
177
 
@@ -286,7 +288,7 @@ def ask_create_options() -> bool:
286
288
 
287
289
  def show_review(spec: UserSpec) -> None:
288
290
  step("6/6 · Revisão", "Confira o resumo e o SQL exato que será executado.")
289
- table = Table(title="Resumo do novo usuário", show_header=True, header_style="bold cyan")
291
+ table = make_table("Resumo do novo usuário")
290
292
  table.add_column("Campo", style="dim")
291
293
  table.add_column("Valor", style="bold")
292
294
  table.add_row("Usuário", spec.username)
@@ -324,18 +326,107 @@ def confirm_and_execute(admin: ClickHouseAdmin, spec: UserSpec) -> None:
324
326
 
325
327
 
326
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
+
327
415
  # ── Menu principal + listar/gerenciar ────────────────────────────────────────────
328
416
 
329
- 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"))
330
426
  return _ask_or_abort(
331
427
  Q.select(
332
428
  "O que deseja fazer?",
333
- choices=[
334
- Q.Choice("Criar usuário — com privilégio mínimo", value="create"),
335
- Q.Choice("Listar usuários — SHOW USERS + grants", value="list"),
336
- Q.Choice("Gerenciar usuário — desativar / reativar / excluir", value="manage"),
337
- Q.Choice("Sair", value="exit"),
338
- ],
429
+ choices=choices,
339
430
  default="create",
340
431
  style=APP_STYLE, qmark="›",
341
432
  instruction="(↑↓ navega · enter confirma)",
@@ -360,7 +451,7 @@ def show_user_details(admin: ClickHouseAdmin, username: str) -> tuple[str, list[
360
451
  error(f"Falha ao ver grants de '{username}': {e}")
361
452
  grants = []
362
453
 
363
- table = Table(title=f"Usuário {username}", show_header=True, header_style="bold cyan")
454
+ table = make_table(f"Usuário {username}")
364
455
  table.add_column("Campo", style="dim")
365
456
  table.add_column("Valor", style="bold")
366
457
  status = "inativo (HOST NONE)" if _is_inactive(create_sql) else "ativo"
@@ -390,7 +481,7 @@ def flow_list_users(admin: ClickHouseAdmin) -> None:
390
481
  info("Nenhum usuário encontrado.")
391
482
  return
392
483
 
393
- 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)")
394
485
  table.add_column("#", style="dim")
395
486
  table.add_column("Usuário", style="bold")
396
487
  for i, u in enumerate(users, 1):
@@ -438,7 +529,7 @@ def flow_manage_users(admin: ClickHouseAdmin) -> None:
438
529
  inactive = _is_inactive(create_sql)
439
530
 
440
531
  if target == admin.conn.username:
441
- 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.")
442
533
 
443
534
  options = []
444
535
  if inactive:
@@ -516,17 +607,24 @@ def flow_create_user(admin: ClickHouseAdmin) -> None:
516
607
  def main() -> None:
517
608
  try:
518
609
  banner()
519
- 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()
520
614
  admin = connect_with_retry(conn)
615
+ if saved_conn is None:
616
+ ask_remember(conn)
521
617
 
522
618
  while True:
523
- action = ask_main_action()
619
+ action = ask_main_action(show_forget=has_saved_session())
524
620
  if action == "create":
525
621
  flow_create_user(admin)
526
622
  elif action == "list":
527
623
  flow_list_users(admin)
528
624
  elif action == "manage":
529
625
  flow_manage_users(admin)
626
+ elif action == "forget":
627
+ flow_forget_session()
530
628
  else:
531
629
  console.print("[dim]Até logo![/]")
532
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.0
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.
@@ -88,7 +96,7 @@ GRANT SHOW, SELECT ON `vendas`.`clientes` TO `analyst_julho`;
88
96
 
89
97
  Decisões de UX:
90
98
 
91
- - **Sistemas por último e desmarcados** (`system`, `information_schema`) para evitar `GRANT` acidental.
99
+ - **Nada pré-marcado + opção `✓ Todos os bancos`** (least privilege: acesso só via opt-in explícito; sistemas vão por último com tag `(sistema)`).
92
100
  - **Ctrl+C = saída limpa**, nada executa sem confirmação final.
93
101
  - **SQL separado da UI** (`clickhouse_users_cli/sql_builder.py`) — dá para testar sem banco.
94
102
  - **Escape correto** de identificadores (backticks) e strings (aspas simples).
@@ -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}[/]")