fix(core): preserve adapter command contracts
This commit is contained in:
+16
-48
@@ -1,43 +1,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from sqlalchemy.exc import OperationalError
|
||||
|
||||
from tht.adapters.dwh import ThothRestDwhAdapter
|
||||
from tht.adapters.factory import build_dwh
|
||||
from tht.cli.config_cmd import CONFIG_OPT
|
||||
from tht.config import ConfigError, load_config
|
||||
from tht.db.connection import can_create_in_schema, make_engine, ping, writable_tables
|
||||
from tht.db.fetch_ca import CaFetchError, describe_pem, fetch_chain_pem, parse_host_port
|
||||
|
||||
db_app = typer.Typer(help="Operazioni sul database target")
|
||||
|
||||
|
||||
def _ping_rest(adapter: ThothRestDwhAdapter, cfg, schema: str) -> None:
|
||||
"""Health check via REST. Il read-only è garantito strutturalmente dall'API
|
||||
(ammette solo SELECT/WITH): non serve il controllo dei privilegi di scrittura."""
|
||||
from tht.rest.client import RestError
|
||||
|
||||
try:
|
||||
info = adapter._client.ping()
|
||||
except RestError as e:
|
||||
typer.secho(f"ERRORE di connessione: {e}", fg=typer.colors.RED, err=True)
|
||||
raise typer.Exit(code=1)
|
||||
if not info.get("db_connected") or not info.get("schema_accessible"):
|
||||
typer.secho(
|
||||
f"ERRORE: DWH non accessibile via REST (risposta: {info}).",
|
||||
fg=typer.colors.RED,
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(code=1)
|
||||
typer.secho(
|
||||
f"OK: connesso via REST a {cfg.rest.base_url} (schema {schema})", fg=typer.colors.GREEN
|
||||
)
|
||||
typer.secho(
|
||||
"OK: accesso read-only garantito dall'API (solo SELECT/WITH).", fg=typer.colors.GREEN
|
||||
)
|
||||
|
||||
|
||||
@db_app.command("ping")
|
||||
def ping_cmd(config: Path = CONFIG_OPT) -> None:
|
||||
"""Testa la connessione e verifica che l'utente sia effettivamente read-only."""
|
||||
@@ -46,29 +17,26 @@ def ping_cmd(config: Path = CONFIG_OPT) -> None:
|
||||
except ConfigError as e:
|
||||
typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True)
|
||||
raise typer.Exit(code=1)
|
||||
schema = cfg.database.db_schema
|
||||
adapter = build_dwh(cfg)
|
||||
if isinstance(adapter, ThothRestDwhAdapter):
|
||||
_ping_rest(adapter, cfg, schema)
|
||||
return
|
||||
engine = make_engine(cfg.database)
|
||||
try:
|
||||
ping(engine)
|
||||
except OperationalError as e:
|
||||
typer.secho(f"ERRORE di connessione: {e.orig}", fg=typer.colors.RED, err=True)
|
||||
health = build_dwh(cfg).health()
|
||||
if not health.ok:
|
||||
typer.secho(f"ERRORE di connessione: {health.detail}", fg=typer.colors.RED, err=True)
|
||||
raise typer.Exit(code=1)
|
||||
typer.secho(f"OK: connesso a {cfg.database.database} (schema {schema})", fg=typer.colors.GREEN)
|
||||
|
||||
writable = writable_tables(engine, schema)
|
||||
can_create = can_create_in_schema(engine, schema)
|
||||
if writable or can_create:
|
||||
if health.endpoint:
|
||||
typer.secho(f"OK: connesso via REST a {health.endpoint} (schema {health.schema})",
|
||||
fg=typer.colors.GREEN)
|
||||
typer.secho("OK: accesso read-only garantito dall'API (solo SELECT/WITH).",
|
||||
fg=typer.colors.GREEN)
|
||||
return
|
||||
typer.secho(f"OK: connesso a {health.database} (schema {health.schema})",
|
||||
fg=typer.colors.GREEN)
|
||||
if not health.read_only:
|
||||
typer.secho(
|
||||
f"ERRORE: l'utente '{cfg.database.user}' NON e' read-only.", fg=typer.colors.RED, err=True
|
||||
)
|
||||
if writable:
|
||||
typer.echo(f" Tabelle scrivibili: {', '.join(writable[:10])}", err=True)
|
||||
if can_create:
|
||||
typer.echo(f" L'utente puo' creare oggetti nello schema {schema}.", err=True)
|
||||
if health.writable_tables:
|
||||
typer.echo(f" Tabelle scrivibili: {', '.join(health.writable_tables[:10])}", err=True)
|
||||
if health.can_create:
|
||||
typer.echo(f" L'utente puo' creare oggetti nello schema {health.schema}.", err=True)
|
||||
typer.echo(" Crea un ruolo read-only con scripts/create_readonly_role.sql.", err=True)
|
||||
raise typer.Exit(code=2)
|
||||
typer.secho("OK: l'utente e' read-only sullo schema target.", fg=typer.colors.GREEN)
|
||||
|
||||
@@ -47,7 +47,9 @@ def build_cmd(config: Path = CONFIG_OPT) -> None:
|
||||
if not is_text_type(column.type) or not effective_eligibility(column, ann_col)[0]:
|
||||
continue
|
||||
try:
|
||||
distinct = dwh.distinct_values(table_name, column_name)
|
||||
distinct = dwh.distinct_values(
|
||||
table_name, column_name, limit=cfg.lsh.max_values_per_column
|
||||
)
|
||||
except Exception as e:
|
||||
skipped.append(SkippedColumn(table_name, column_name, f"errore: {e}"))
|
||||
continue
|
||||
|
||||
@@ -156,7 +156,6 @@ def save_one_cmd(
|
||||
from tht.adapters.factory import build_vector_store
|
||||
from tht.cli.vector_cmd import make_embedder
|
||||
from tht.memory import load_registry, promote, save_one_memory
|
||||
from tht.vectorstore.rest_client import VectorRestClient
|
||||
|
||||
cfg = _load_config_or_exit(config)
|
||||
manifest = load_session_or_exit(cfg, session)
|
||||
@@ -168,7 +167,7 @@ def save_one_cmd(
|
||||
fg=typer.colors.RED, err=True,
|
||||
)
|
||||
raise typer.Exit(code=4)
|
||||
build_vector_store(cfg, require_write=True)
|
||||
store = build_vector_store(cfg, require_write=True)
|
||||
|
||||
sdir = session_dir(cfg, session)
|
||||
# Promuove la decisione scelta nel registro locale (idempotente: salta se gia' presente
|
||||
@@ -176,9 +175,8 @@ def save_one_cmd(
|
||||
promote(sdir, manifest, seqs=[decision], registry_path=registry_path(cfg))
|
||||
records = [r for r in load_registry(registry_path(cfg)) if r.session_id == manifest.id]
|
||||
|
||||
writer = VectorRestClient(cfg.vector_write_rest)
|
||||
embedder = make_embedder(cfg.embeddings)
|
||||
count = save_one_memory(records, decision, writer=writer, embedder=embedder)
|
||||
count = save_one_memory(records, decision, store=store, embedder=embedder)
|
||||
|
||||
msg = (
|
||||
f"{count} memoria salvata su pgvector (decision_seq {decision})."
|
||||
@@ -455,21 +453,20 @@ def index_solved_session(cfg, session_id: str) -> int:
|
||||
from tht.cli.sql_cmd import promoted_tables_for
|
||||
from tht.cli.vector_cmd import make_embedder
|
||||
from tht.solved import build_solved_record, save_solved_question
|
||||
from tht.vectorstore.rest_client import VectorRestClient
|
||||
|
||||
if not has_vector_write_rest(cfg):
|
||||
raise RuntimeError(
|
||||
"vector_write_rest assente: la coppia domanda->SQL si indicizza solo con la "
|
||||
"writer key configurata nel workspace yaml"
|
||||
)
|
||||
build_vector_store(cfg, require_write=True)
|
||||
store = build_vector_store(cfg, require_write=True)
|
||||
manifest = load_session_or_exit(cfg, session_id)
|
||||
record = build_solved_record(
|
||||
session_dir(cfg, session_id), manifest, promoted_tables_for(cfg, session_id)
|
||||
)
|
||||
return save_solved_question(
|
||||
record,
|
||||
writer=VectorRestClient(cfg.vector_write_rest),
|
||||
store=store,
|
||||
embedder=make_embedder(cfg.embeddings),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
import typer
|
||||
from tht.adapters.factory import build_dwh
|
||||
@@ -8,6 +9,23 @@ from tht.db.sampling import is_text_type
|
||||
from tht.mschema.eligibility import classify_all
|
||||
|
||||
schema_app = typer.Typer(help="Gestione mschema (rappresentazione canonica dello schema)")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _add_examples(dwh, phys, examples) -> None:
|
||||
for table_name, table in phys.tables.items():
|
||||
for column_name, column in table.columns.items():
|
||||
if not is_text_type(column.type):
|
||||
continue
|
||||
try:
|
||||
sampled = dwh.sample_column(
|
||||
table_name, column_name, limit=examples.max_per_column
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Campionamento saltato per %s.%s: %s",
|
||||
table_name, column_name, exc)
|
||||
continue
|
||||
column.examples = [str(value) for value in sampled if value not in (None, "")]
|
||||
|
||||
|
||||
def _load_config_or_exit(config: Path):
|
||||
@@ -64,18 +82,7 @@ def introspect_cmd(
|
||||
try:
|
||||
dwh = build_dwh(cfg)
|
||||
phys = dwh.introspect()
|
||||
for table_name, table in phys.tables.items():
|
||||
for column_name, column in table.columns.items():
|
||||
if is_text_type(column.type):
|
||||
column.examples = [
|
||||
str(value)
|
||||
for value in dwh.sample_column(
|
||||
table_name,
|
||||
column_name,
|
||||
limit=cfg.examples.max_per_column,
|
||||
)
|
||||
if value not in (None, "")
|
||||
]
|
||||
_add_examples(dwh, phys, cfg.examples)
|
||||
classify_all(phys, cfg.eligibility)
|
||||
except Exception as e:
|
||||
typer.secho(f"ERRORE: {e}", fg=typer.colors.RED, err=True)
|
||||
|
||||
Reference in New Issue
Block a user