baucli 1.0.1__tar.gz → 1.0.3__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 (35) hide show
  1. {baucli-1.0.1 → baucli-1.0.3}/PKG-INFO +1 -1
  2. {baucli-1.0.1 → baucli-1.0.3}/baucli/__init__.py +1 -1
  3. {baucli-1.0.1 → baucli-1.0.3}/baucli/api.py +5 -0
  4. {baucli-1.0.1 → baucli-1.0.3}/baucli/cli.py +203 -4
  5. {baucli-1.0.1 → baucli-1.0.3}/baucli/config.py +160 -10
  6. {baucli-1.0.1 → baucli-1.0.3}/baucli/project.py +8 -0
  7. {baucli-1.0.1 → baucli-1.0.3}/baucli.egg-info/PKG-INFO +1 -1
  8. baucli-1.0.3/baucli.egg-info/entry_points.txt +2 -0
  9. {baucli-1.0.1 → baucli-1.0.3}/setup.py +2 -2
  10. baucli-1.0.1/baucli.egg-info/entry_points.txt +0 -2
  11. {baucli-1.0.1 → baucli-1.0.3}/README.md +0 -0
  12. {baucli-1.0.1 → baucli-1.0.3}/baucli/apex_rebuild.py +0 -0
  13. {baucli-1.0.1 → baucli-1.0.3}/baucli/apexlang.py +0 -0
  14. {baucli-1.0.1 → baucli-1.0.3}/baucli/db/__init__.py +0 -0
  15. {baucli-1.0.1 → baucli-1.0.3}/baucli/db/mssql.py +0 -0
  16. {baucli-1.0.1 → baucli-1.0.3}/baucli/db/mysql.py +0 -0
  17. {baucli-1.0.1 → baucli-1.0.3}/baucli/db/oracle.py +0 -0
  18. {baucli-1.0.1 → baucli-1.0.3}/baucli/db/postgresql.py +0 -0
  19. {baucli-1.0.1 → baucli-1.0.3}/baucli/docmig.py +0 -0
  20. {baucli-1.0.1 → baucli-1.0.3}/baucli/git.py +0 -0
  21. {baucli-1.0.1 → baucli-1.0.3}/baucli/legacy/__init__.py +0 -0
  22. {baucli-1.0.1 → baucli-1.0.3}/baucli/legacy/delphi.py +0 -0
  23. {baucli-1.0.1 → baucli-1.0.3}/baucli/mcp_server.py +0 -0
  24. {baucli-1.0.1 → baucli-1.0.3}/baucli/rag_embed.py +0 -0
  25. {baucli-1.0.1 → baucli-1.0.3}/baucli/sample_scan.py +0 -0
  26. {baucli-1.0.1 → baucli-1.0.3}/baucli/scope_scan.py +0 -0
  27. {baucli-1.0.1 → baucli-1.0.3}/baucli/screenshot.py +0 -0
  28. {baucli-1.0.1 → baucli-1.0.3}/baucli/seed_check.py +0 -0
  29. {baucli-1.0.1 → baucli-1.0.3}/baucli/stress.py +0 -0
  30. {baucli-1.0.1 → baucli-1.0.3}/baucli/sync.py +0 -0
  31. {baucli-1.0.1 → baucli-1.0.3}/baucli.egg-info/SOURCES.txt +0 -0
  32. {baucli-1.0.1 → baucli-1.0.3}/baucli.egg-info/dependency_links.txt +0 -0
  33. {baucli-1.0.1 → baucli-1.0.3}/baucli.egg-info/requires.txt +0 -0
  34. {baucli-1.0.1 → baucli-1.0.3}/baucli.egg-info/top_level.txt +0 -0
  35. {baucli-1.0.1 → baucli-1.0.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: baucli
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: BAUCLI — AI-powered database documentation CLI for BAU
5
5
  Home-page: https://www.begcloud.com
6
6
  Author: BEG Support
@@ -3,6 +3,6 @@
3
3
  import warnings
4
4
  warnings.filterwarnings("ignore", message="urllib3 v2 only supports OpenSSL")
5
5
 
6
- __version__ = "1.0.1"
6
+ __version__ = "1.0.3"
7
7
  __product__ = "BAUCLI"
8
8
  __author__ = "BEG Support"
@@ -822,6 +822,11 @@ class BauApi:
822
822
  """GET /quota — Full quota status."""
823
823
  return self._request("GET", "quota")
824
824
 
825
+ def whoami(self) -> dict:
826
+ """GET /whoami — dono (BAU_USERS) da API key atual: user_email/name/role,
827
+ tenant_code, key_prefix. Usado por `status` e pelo cabeçalho."""
828
+ return self._request("GET", "whoami")
829
+
825
830
  # ── DOCAPEX endpoints ──
826
831
 
827
832
  def apex_sync(self, payload: dict) -> dict:
@@ -36,6 +36,7 @@ from baucli.config import (
36
36
  load_config, save_config, is_configured, get_database,
37
37
  add_database, remove_database, list_databases, set_db_password,
38
38
  get_db_password, delete_db_password, DEFAULT_CONFIG,
39
+ BauConfigError,
39
40
  )
40
41
  from baucli.api import BauApi, BauApiError, BauSessionError
41
42
  from baucli.sync import sync_database, sync_all, get_extractor, get_sync_recommendation, PARALLEL_THRESHOLD
@@ -54,6 +55,13 @@ def show_db_panel(title: str, db_name: str = None, db_config: dict = None,
54
55
  lines = [f"[bold red]{__product__} v{__version__}[/bold red] — {title}"]
55
56
  lines.append("")
56
57
 
58
+ # Dono da API key (cacheado por `status`/`init` via GET /whoami).
59
+ if cfg.get("key_user_email"):
60
+ _who = cfg["key_user_email"]
61
+ if cfg.get("key_user_name"):
62
+ _who += f" ({cfg['key_user_name']})"
63
+ lines.append(f" Usuário: [cyan]{_who}[/cyan]")
64
+
57
65
  if db_name and db_config:
58
66
  engine = db_config.get("engine", "?").upper()
59
67
  host = db_config.get("host", "?")
@@ -79,7 +87,8 @@ def show_db_panel(title: str, db_name: str = None, db_config: dict = None,
79
87
  if uuid != "—":
80
88
  lines.append(f" BAU UUID: {uuid}")
81
89
  else:
82
- lines.append(f" License: [cyan]{cfg.get('license_key', '—')}[/cyan]")
90
+ if cfg.get("license_key"):
91
+ lines.append(f" License: [cyan]{cfg['license_key']}[/cyan]")
83
92
  lines.append(f" Server: {cfg.get('api_url', '—')}")
84
93
  lines.append(f" Tenant: {cfg.get('tenant_code', '—')}")
85
94
  lines.append(f" Language: [cyan]{cfg.get('language') or 'en_US (default)'}[/cyan]")
@@ -134,7 +143,9 @@ def _do_login(cfg: dict, email: str, password: str) -> bool:
134
143
  # ═══════════════════════════════════════════════════════════
135
144
  @click.group()
136
145
  @click.version_option(__version__, prog_name=__product__)
137
- def main():
146
+ @click.option("--profile", "profile", default=None,
147
+ help="Perfil de credencial a usar nesta invocação (sobrepõe bau.toml/default).")
148
+ def main(profile):
138
149
  """BAUCLI — AI-powered database documentation.
139
150
 
140
151
  \b
@@ -144,7 +155,8 @@ def main():
144
155
  baucli sync
145
156
  baucli context
146
157
  """
147
- pass
158
+ if profile:
159
+ os.environ["BAUCLI_PROFILE"] = profile
148
160
 
149
161
 
150
162
  # ── Aviso global: "golden desatualizado" ────────────────────────────
@@ -390,6 +402,18 @@ def _run_init(license_key: str, api_key_manual: str, api_url: str,
390
402
  except BauApiError:
391
403
  pass
392
404
 
405
+ # Dono da API key (cacheia p/ o cabeçalho + confirma no init)
406
+ try:
407
+ me = api.whoami()
408
+ if isinstance(me, dict) and me.get("status") == "ok":
409
+ cfg["key_user_email"] = me.get("user_email")
410
+ cfg["key_user_name"] = me.get("user_name")
411
+ save_config(cfg)
412
+ _uname = f" ({me.get('user_name')})" if me.get("user_name") else ""
413
+ console.print(f" Usuário: [green]{me.get('user_email')}[/green]{_uname}")
414
+ except BauApiError:
415
+ pass
416
+
393
417
  from .config import CONFIG_FILE
394
418
  console.print()
395
419
  console.print(f"[green]Config saved to {CONFIG_FILE}[/green]")
@@ -1846,6 +1870,113 @@ def _write_root_pointer_md(current_schema: str, primary_locale: str,
1846
1870
  pointer.write_text(new_content, encoding="utf-8")
1847
1871
 
1848
1872
 
1873
+ # ═══════════════════════════════════════════════════════════
1874
+ # baucli profile (Fase 1 — perfis de credencial por tenant)
1875
+ # ═══════════════════════════════════════════════════════════
1876
+ @main.group()
1877
+ def profiles():
1878
+ """Perfis de credencial (1 BAUCLI_HOME, N tenants).
1879
+
1880
+ \b
1881
+ profiles add NOME --tenant T --api-key K [--base-url U] [--db DB] [--default]
1882
+ profiles ls
1883
+ profiles use NOME
1884
+ profiles rm NOME
1885
+ O bau.toml do projeto escolhe o perfil por [bau].profile (sem segredo no repo).
1886
+ """
1887
+ pass
1888
+
1889
+
1890
+ @profiles.command("add")
1891
+ @click.argument("name")
1892
+ @click.option("--tenant", "tenant_code", default=None, help="Código do tenant (ex.: GRUPO_DASS).")
1893
+ @click.option("--api-key", "api_key", required=True, help="API key do dev nesse tenant (bau_...).")
1894
+ @click.option("--base-url", "base_url", default=None, help="URL da API (default: a do config).")
1895
+ @click.option("--db", "default_db", default=None, help="Alias de database default do perfil.")
1896
+ @click.option("--default", "make_default", is_flag=True, help="Torna este o perfil default do BAUCLI_HOME.")
1897
+ def profile_add(name, tenant_code, api_key, base_url, default_db, make_default):
1898
+ """Cria/atualiza um perfil e grava sua api_key no keyring."""
1899
+ from .config import load_config as _lc, save_config, set_profile_api_key
1900
+ cfg = _lc(apply_profile=False)
1901
+ profiles = cfg.setdefault("profiles", {})
1902
+ entry = profiles.get(name, {})
1903
+ if tenant_code:
1904
+ entry["tenant_code"] = tenant_code
1905
+ if base_url:
1906
+ entry["base_url"] = base_url
1907
+ if default_db:
1908
+ entry["default_db"] = default_db
1909
+ profiles[name] = entry
1910
+ if make_default or not cfg.get("default_profile"):
1911
+ cfg["default_profile"] = name
1912
+ save_config(cfg)
1913
+ in_kr = set_profile_api_key(name, api_key)
1914
+ tail = " (default do BAUCLI_HOME)" if cfg.get("default_profile") == name else ""
1915
+ console.print(f"[green]Perfil '{name}' salvo{tail}[/green] — chave no "
1916
+ f"{'keyring' if in_kr else '.credentials'}.")
1917
+
1918
+
1919
+ @profiles.command("ls")
1920
+ def profile_ls():
1921
+ """Lista os perfis do BAUCLI_HOME e qual está ativo."""
1922
+ from .config import load_config as _lc, resolve_profile_name, get_profile_api_key
1923
+ cfg = _lc(apply_profile=False)
1924
+ profiles = cfg.get("profiles", {})
1925
+ if not profiles:
1926
+ console.print(" [yellow]Nenhum perfil.[/yellow] Crie: "
1927
+ "[cyan]baucli profile add NOME --tenant T --api-key K[/cyan]")
1928
+ return
1929
+ try:
1930
+ active = resolve_profile_name(cfg)
1931
+ except Exception:
1932
+ active = None
1933
+ default = cfg.get("default_profile")
1934
+ table = RichTable(title=f"Perfis ({len(profiles)})", border_style="dim")
1935
+ table.add_column("Nome", style="cyan")
1936
+ table.add_column("Tenant")
1937
+ table.add_column("DB default")
1938
+ table.add_column("Chave")
1939
+ table.add_column("Default", style="yellow")
1940
+ table.add_column("Ativo", style="green")
1941
+ for n, p in profiles.items():
1942
+ table.add_row(n, p.get("tenant_code", "—"), p.get("default_db", "—"),
1943
+ "sim" if get_profile_api_key(n) else "—",
1944
+ "★" if n == default else "", "●" if n == active else "")
1945
+ console.print(table)
1946
+ console.print(" [dim]Ativo = env BAUCLI_PROFILE > bau.toml [bau].profile > default.[/dim]")
1947
+
1948
+
1949
+ @profiles.command("use")
1950
+ @click.argument("name")
1951
+ def profile_use(name):
1952
+ """Define o perfil default do BAUCLI_HOME."""
1953
+ from .config import load_config as _lc, save_config
1954
+ cfg = _lc(apply_profile=False)
1955
+ if name not in cfg.get("profiles", {}):
1956
+ console.print(f"[red]Perfil '{name}' não existe.[/red] Veja: baucli profile ls")
1957
+ raise SystemExit(1)
1958
+ cfg["default_profile"] = name
1959
+ save_config(cfg)
1960
+ console.print(f"[green]Perfil default = '{name}'.[/green]")
1961
+
1962
+
1963
+ @profiles.command("rm")
1964
+ @click.argument("name")
1965
+ def profile_rm(name):
1966
+ """Remove um perfil e sua api_key."""
1967
+ from .config import load_config as _lc, save_config, delete_profile_api_key
1968
+ cfg = _lc(apply_profile=False)
1969
+ if name not in cfg.get("profiles", {}):
1970
+ console.print(f"[yellow]Perfil '{name}' já não existe.[/yellow]")
1971
+ return
1972
+ cfg["profiles"].pop(name, None)
1973
+ if cfg.get("default_profile") == name:
1974
+ cfg["default_profile"] = next(iter(cfg["profiles"]), None)
1975
+ save_config(cfg)
1976
+ delete_profile_api_key(name)
1977
+ console.print(f"[green]Perfil '{name}' removido.[/green]")
1978
+
1979
+
1849
1980
  # ═══════════════════════════════════════════════════════════
1850
1981
  # baucli status
1851
1982
  # ═══════════════════════════════════════════════════════════
@@ -1854,6 +1985,21 @@ def status():
1854
1985
  """Show agent config, quota, and pending deploys."""
1855
1986
  cfg = load_config()
1856
1987
 
1988
+ if cfg.get("_active_profile"):
1989
+ console.print(f"[dim]Perfil ativo:[/dim] [cyan]{cfg['_active_profile']}[/cyan] "
1990
+ f"· tenant {cfg.get('tenant_code') or '—'}")
1991
+
1992
+ # Resolve o dono da API key (ao vivo) e cacheia p/ o cabeçalho.
1993
+ if cfg.get("api_key"):
1994
+ try:
1995
+ me = get_api(cfg).whoami()
1996
+ if isinstance(me, dict) and me.get("status") == "ok":
1997
+ cfg["key_user_email"] = me.get("user_email")
1998
+ cfg["key_user_name"] = me.get("user_name")
1999
+ save_config(cfg)
2000
+ except Exception:
2001
+ pass
2002
+
1857
2003
  show_db_panel("Status")
1858
2004
 
1859
2005
  # Databases table
@@ -7750,5 +7896,58 @@ def rag_search_cmd(pergunta, schema, top):
7750
7896
  f"[bold]{it['key']}[/bold]: {it['text'][:140]}")
7751
7897
 
7752
7898
 
7899
+ def _report_crash(exc: Exception):
7900
+ """Salva o traceback completo num log local e mostra mensagem de Suporte."""
7901
+ import traceback
7902
+ import time
7903
+ from baucli.config import BAUCLI_DIR
7904
+ log_path = None
7905
+ try:
7906
+ BAUCLI_DIR.mkdir(parents=True, exist_ok=True)
7907
+ log_path = BAUCLI_DIR / "last_error.log"
7908
+ with open(log_path, "w", encoding="utf-8") as f:
7909
+ f.write(f"{__product__} v{__version__}\n")
7910
+ f.write(f"cmd: {' '.join(sys.argv)}\n")
7911
+ f.write(f"time: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
7912
+ f.write("".join(traceback.format_exception(type(exc), exc, exc.__traceback__)))
7913
+ except Exception:
7914
+ log_path = None
7915
+ console.print()
7916
+ console.print("[red]Ops — o BAUCLI encontrou um erro inesperado e não conseguiu concluir a operação.[/red]")
7917
+ console.print(f" [dim]{type(exc).__name__}: {exc}[/dim]")
7918
+ console.print(" Contate o [bold]Suporte BEG[/bold]: [cyan]www.begcloud.com[/cyan]")
7919
+ if log_path:
7920
+ console.print(f" Anexe este arquivo ao chamado: [cyan]{log_path}[/cyan]")
7921
+ console.print(" [dim]Detalhe técnico (devs): rode de novo com BAUCLI_DEBUG=1[/dim]")
7922
+
7923
+
7924
+ def cli():
7925
+ """Entry point do console com captura GLOBAL de erros (UX amigável).
7926
+
7927
+ - Erros de configuração (JSON inválido) → mensagem clara e auto-corrigível.
7928
+ - Ctrl-C → 'Cancelado.'.
7929
+ - Qualquer erro não tratado → mensagem de Suporte + traceback salvo em
7930
+ BAUCLI_HOME/last_error.log. BAUCLI_DEBUG=1 mostra o traceback cru.
7931
+ Erros de uso do Click (opção inválida etc.) seguem o comportamento padrão.
7932
+ """
7933
+ try:
7934
+ main(standalone_mode=True)
7935
+ except SystemExit:
7936
+ raise
7937
+ except KeyboardInterrupt:
7938
+ console.print("\n[dim]Cancelado.[/dim]")
7939
+ sys.exit(130)
7940
+ except BauConfigError as e:
7941
+ console.print(f"[red]Configuração inválida.[/red]\n{e}")
7942
+ sys.exit(1)
7943
+ except click.ClickException:
7944
+ raise
7945
+ except Exception as e: # noqa: BLE001 — captura geral proposital
7946
+ if os.environ.get("BAUCLI_DEBUG"):
7947
+ raise
7948
+ _report_crash(e)
7949
+ sys.exit(1)
7950
+
7951
+
7753
7952
  if __name__ == "__main__":
7754
- main()
7953
+ cli()
@@ -38,6 +38,12 @@ try:
38
38
  except Exception:
39
39
  _keyring = None
40
40
 
41
+ class BauConfigError(Exception):
42
+ """Problema no arquivo de configuração que o próprio usuário pode corrigir
43
+ (ex.: JSON malformado). Tratado pela CLI com mensagem clara — sem traceback,
44
+ sem 'contate o Suporte' (é auto-corrigível)."""
45
+
46
+
41
47
  DEFAULT_CONFIG = {
42
48
  "license_key": None,
43
49
  "api_url": "https://begbrokerdev.begcloud.com/ords/bauctx/api/v1",
@@ -52,6 +58,8 @@ DEFAULT_CONFIG = {
52
58
  "session_expires_at": None,
53
59
  "user_email": None,
54
60
  "language": "en_US",
61
+ "profiles": {},
62
+ "default_profile": None,
55
63
  }
56
64
 
57
65
 
@@ -63,23 +71,73 @@ def ensure_dir():
63
71
  os.chmod(BAU_DIR, 0o700)
64
72
 
65
73
 
66
- def load_config() -> dict:
67
- """Load config from ~/.baucli/config.json."""
74
+ _in_profile_apply = False
75
+
76
+
77
+ def load_config(apply_profile: bool = True) -> dict:
78
+ """Load config from ~/.baucli/config.json.
79
+
80
+ When profiles are defined AND ``apply_profile`` is True, overlays the active
81
+ profile's credential (api_url / tenant_code / default_db / api_key) onto the
82
+ flat config so every consumer (CLI + MCP) transparently uses the right
83
+ (tenant, key). Precedence of the active profile: env BAUCLI_PROFILE >
84
+ bau.toml [bau].profile > config.json default_profile. No profiles / no match
85
+ → identical to the legacy flat behavior. Pass ``apply_profile=False`` to get
86
+ the raw config (used by the profile-management commands themselves).
87
+ """
68
88
  if not CONFIG_FILE.exists():
69
- return DEFAULT_CONFIG.copy()
70
- with open(CONFIG_FILE) as f:
71
- cfg = json.load(f)
72
- # Merge with defaults for missing keys
73
- merged = DEFAULT_CONFIG.copy()
74
- merged.update(cfg)
89
+ merged = DEFAULT_CONFIG.copy()
90
+ else:
91
+ try:
92
+ with open(CONFIG_FILE) as f:
93
+ cfg = json.load(f)
94
+ except json.JSONDecodeError as e:
95
+ raise BauConfigError(
96
+ f"O arquivo de configuração está com JSON inválido:\n"
97
+ f" {CONFIG_FILE}\n"
98
+ f" → linha {e.lineno}, coluna {e.colno} ({e.msg}).\n"
99
+ f"Dica: todo valor de texto precisa de aspas (ex.: "
100
+ f"\"tenant_code\": \"GRUPO_DASS\"); só null/true/false/números ficam sem.\n"
101
+ f"Valide com: python3 -m json.tool {CONFIG_FILE}"
102
+ ) from e
103
+ # Merge with defaults for missing keys
104
+ merged = DEFAULT_CONFIG.copy()
105
+ merged.update(cfg)
106
+
107
+ global _in_profile_apply
108
+ if apply_profile and merged.get("profiles") and not _in_profile_apply:
109
+ try:
110
+ _in_profile_apply = True
111
+ name = resolve_profile_name(merged)
112
+ if name:
113
+ _apply_profile(merged, name)
114
+ except Exception:
115
+ pass
116
+ finally:
117
+ _in_profile_apply = False
75
118
  return merged
76
119
 
77
120
 
78
121
  def save_config(cfg: dict):
79
- """Save config to ~/.baucli/config.json."""
122
+ """Save config to ~/.baucli/config.json.
123
+
124
+ Never persists a profile overlay into the flat config: strips the transient
125
+ `_active_profile`/`_pre_profile` markers and, for each key the active profile
126
+ overlaid, writes the PRE-profile (raw) value back — unless the caller changed
127
+ it on purpose (then the caller's value wins). Keeps profile secrets out of
128
+ config.json and prevents a load→mutate→save from leaking one tenant's key.
129
+ """
80
130
  ensure_dir()
131
+ to_write = dict(cfg)
132
+ pre = to_write.pop("_pre_profile", None)
133
+ to_write.pop("_active_profile", None)
134
+ if pre:
135
+ for k, ov in pre.items():
136
+ raw, overlaid = ov[0], ov[1]
137
+ if to_write.get(k) == overlaid: # caller didn't change it → restore raw
138
+ to_write[k] = raw
81
139
  with open(CONFIG_FILE, "w") as f:
82
- json.dump(cfg, f, indent=2, ensure_ascii=False)
140
+ json.dump(to_write, f, indent=2, ensure_ascii=False)
83
141
  if sys.platform != "win32":
84
142
  os.chmod(CONFIG_FILE, 0o600)
85
143
 
@@ -148,6 +206,98 @@ def delete_db_password(db_name: str):
148
206
  save_credentials(creds)
149
207
 
150
208
 
209
+ # ── Profiles (Fase 1: 1 BAUCLI_HOME, N perfis por tenant) ──────────────────
210
+ # Um "profile" é uma credencial NOMEADA: {tenant_code, base_url, default_db} no
211
+ # config.json + a api_key no keyring (namespaced). O bau.toml do projeto escolhe
212
+ # o perfil por nome lógico ([bau].profile) — sem segredo no repo. Precedência do
213
+ # perfil ativo: env BAUCLI_PROFILE > [bau].profile > config.json default_profile.
214
+
215
+ def _profile_keyring_user(name: str) -> str:
216
+ return f"__profile__:{name}"
217
+
218
+
219
+ def get_profile_api_key(name: str) -> Optional[str]:
220
+ """API key do perfil: keyring primeiro, senão o arquivo .credentials."""
221
+ if _keyring:
222
+ try:
223
+ k = _keyring.get_password(KEYRING_SERVICE, _profile_keyring_user(name))
224
+ if k:
225
+ return k
226
+ except Exception:
227
+ pass
228
+ return load_credentials().get(_profile_keyring_user(name), {}).get("api_key")
229
+
230
+
231
+ def set_profile_api_key(name: str, api_key: str) -> bool:
232
+ """Grava a api_key do perfil (keyring se houver, senão .credentials)."""
233
+ if _keyring:
234
+ try:
235
+ _keyring.set_password(KEYRING_SERVICE, _profile_keyring_user(name), api_key)
236
+ return True
237
+ except Exception:
238
+ pass
239
+ creds = load_credentials()
240
+ creds.setdefault(_profile_keyring_user(name), {})["api_key"] = api_key
241
+ save_credentials(creds)
242
+ return False
243
+
244
+
245
+ def delete_profile_api_key(name: str):
246
+ """Remove a api_key do perfil (keyring + .credentials). Idempotente."""
247
+ if _keyring:
248
+ try:
249
+ _keyring.delete_password(KEYRING_SERVICE, _profile_keyring_user(name))
250
+ except Exception:
251
+ pass
252
+ creds = load_credentials()
253
+ if _profile_keyring_user(name) in creds:
254
+ creds.pop(_profile_keyring_user(name), None)
255
+ save_credentials(creds)
256
+
257
+
258
+ def _project_profile() -> Optional[str]:
259
+ """[bau].profile do bau.toml mais próximo (walk-up). None em qualquer falha."""
260
+ try:
261
+ from .project import find_project
262
+ proj = find_project()
263
+ return proj.profile if proj else None
264
+ except Exception:
265
+ return None
266
+
267
+
268
+ def resolve_profile_name(cfg: dict) -> Optional[str]:
269
+ """Nome do perfil ativo: env BAUCLI_PROFILE > [bau].profile > default_profile."""
270
+ return (os.environ.get("BAUCLI_PROFILE")
271
+ or _project_profile()
272
+ or cfg.get("default_profile"))
273
+
274
+
275
+ def _apply_profile(cfg: dict, name: str) -> dict:
276
+ """Overlay dos campos do perfil sobre o cfg flat (in-place).
277
+
278
+ Registra em `_pre_profile` {campo: [raw, overlaid]} para o save_config poder
279
+ reverter (nunca persistir a chave do perfil nos campos flat)."""
280
+ prof = (cfg.get("profiles") or {}).get(name)
281
+ if not prof:
282
+ return cfg
283
+ pre = {}
284
+
285
+ def _ov(key, val):
286
+ if val is None:
287
+ return
288
+ pre[key] = [cfg.get(key), val]
289
+ cfg[key] = val
290
+
291
+ _ov("api_url", prof.get("base_url"))
292
+ _ov("tenant_code", prof.get("tenant_code"))
293
+ _ov("default_db", prof.get("default_db"))
294
+ _ov("api_key", get_profile_api_key(name))
295
+ cfg["_active_profile"] = name
296
+ if pre:
297
+ cfg["_pre_profile"] = pre
298
+ return cfg
299
+
300
+
151
301
  def get_database(cfg: dict, db_name: Optional[str] = None) -> tuple:
152
302
  """Get database config by name or default.
153
303
 
@@ -88,6 +88,14 @@ class Project:
88
88
  v = self._get("project", "scope")
89
89
  return v.upper() if v else None
90
90
 
91
+ # -- [bau] -------------------------------------------------------------
92
+ @property
93
+ def profile(self) -> str | None:
94
+ """[bau].profile — nome lógico do perfil de credencial (BAUCLI_HOME).
95
+ Seleciona qual (tenant, chave) o baucli/MCP usam neste projeto, sem
96
+ segredo no repo. Precedência: env BAUCLI_PROFILE > este > default_profile."""
97
+ return self._get("bau", "profile")
98
+
91
99
  # -- [connection] ------------------------------------------------------
92
100
  @property
93
101
  def connection_name(self) -> str | None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: baucli
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: BAUCLI — AI-powered database documentation CLI for BAU
5
5
  Home-page: https://www.begcloud.com
6
6
  Author: BEG Support
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ baucli = baucli.cli:cli
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="baucli",
5
- version="1.0.1",
5
+ version="1.0.3",
6
6
  description="BAUCLI — AI-powered database documentation CLI for BAU",
7
7
  long_description=open("README.md").read(),
8
8
  long_description_content_type="text/markdown",
@@ -28,7 +28,7 @@ setup(
28
28
  },
29
29
  entry_points={
30
30
  "console_scripts": [
31
- "baucli=baucli.cli:main",
31
+ "baucli=baucli.cli:cli",
32
32
  ],
33
33
  },
34
34
  classifiers=[
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- baucli = baucli.cli:main
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes