analytical_force / src /database /turso_client.py
ViniciusKhan's picture
Analytical-Force: API + Docker
8445ed4
Raw
History Blame Contribute Delete
6.5 kB
"""Cliente de conexão com o banco Turso/libSQL.
Este módulo é a ÚNICA porta de entrada para o banco. Ele:
- conecta no Turso usando ``libsql`` e as variáveis TURSO_DATABASE_URL /
TURSO_AUTH_TOKEN;
- nunca imprime credenciais no log;
- expõe ``get_connection``, ``execute_query``, ``execute_many`` e ``fetch_all``.
Importante (separação de responsabilidades): aqui não há regra de negócio.
Apenas execução de SQL parametrizado.
"""
from __future__ import annotations
from typing import Any, Sequence
from ..config import get_settings
from ..utils.logger import get_logger
logger = get_logger("database.turso")
# Tipagem auxiliar para parâmetros de SQL.
Parametros = Sequence[Any]
class TursoClient:
"""Encapsula a conexão e a execução de comandos no Turso/libSQL."""
def __init__(self, database_url: str, auth_token: str) -> None:
"""Inicializa o cliente.
Args:
database_url: URL do banco (TURSO_DATABASE_URL).
auth_token: Token de autenticação (TURSO_AUTH_TOKEN).
Observação: a conexão é preguiçosa (lazy) — só é aberta no primeiro uso.
"""
self._database_url = database_url
self._auth_token = auth_token
self._conn: Any | None = None
# ------------------------------------------------------------------
# Conexão
# ------------------------------------------------------------------
def get_connection(self) -> Any:
"""Retorna a conexão ativa, criando-a se necessário.
Returns:
Objeto de conexão do ``libsql``.
Raises:
RuntimeError: Se a biblioteca ``libsql`` não estiver instalada
ou se a conexão falhar.
"""
if self._conn is not None:
return self._conn
try:
import libsql # import tardio para não exigir a lib em testes unitários
except ImportError as exc: # pragma: no cover - depende do ambiente
raise RuntimeError(
"Biblioteca 'libsql' não instalada. Rode: pip install libsql"
) from exc
if not self._database_url:
raise RuntimeError("TURSO_DATABASE_URL não configurado.")
try:
# Bancos remotos exigem auth_token; arquivos locais (file:) não.
if self._auth_token:
self._conn = libsql.connect(
database=self._database_url, auth_token=self._auth_token
)
else:
self._conn = libsql.connect(database=self._database_url)
# Log seguro: NÃO registra URL completa nem token.
logger.info("Conexão com Turso estabelecida.")
except Exception as exc:
# Mensagem genérica para não vazar credenciais embutidas na URL.
logger.error("Falha ao conectar no Turso: %s", type(exc).__name__)
raise RuntimeError("Não foi possível conectar ao banco Turso.") from exc
return self._conn
# ------------------------------------------------------------------
# Execução de comandos
# ------------------------------------------------------------------
def execute_query(self, sql: str, params: Parametros | None = None) -> Any:
"""Executa um comando SQL único (INSERT/UPDATE/DELETE/DDL).
Args:
sql: Comando SQL com placeholders ``?``.
params: Parâmetros do comando.
Returns:
O cursor resultante da execução.
"""
conn = self.get_connection()
cursor = conn.execute(sql, tuple(params) if params else ())
conn.commit()
return cursor
def execute_many(self, sql: str, seq_params: list[Parametros]) -> None:
"""Executa o mesmo comando para múltiplos conjuntos de parâmetros.
Args:
sql: Comando SQL com placeholders ``?``.
seq_params: Lista de tuplas de parâmetros.
"""
if not seq_params:
return
conn = self.get_connection()
# executemany quando disponível; senão, itera (compatibilidade).
if hasattr(conn, "executemany"):
conn.executemany(sql, [tuple(p) for p in seq_params])
else: # pragma: no cover - fallback de compatibilidade
for p in seq_params:
conn.execute(sql, tuple(p))
conn.commit()
def fetch_all(
self, sql: str, params: Parametros | None = None
) -> list[dict[str, Any]]:
"""Executa um SELECT e retorna todas as linhas como dicionários.
Args:
sql: Comando SELECT com placeholders ``?``.
params: Parâmetros da query.
Returns:
Lista de dicionários (coluna -> valor).
"""
conn = self.get_connection()
cursor = conn.execute(sql, tuple(params) if params else ())
linhas = cursor.fetchall()
# Extrai nomes de coluna de cursor.description (padrão DB-API).
if cursor.description:
colunas = [desc[0] for desc in cursor.description]
else:
colunas = []
return [dict(zip(colunas, linha)) for linha in linhas]
def fetch_one(
self, sql: str, params: Parametros | None = None
) -> dict[str, Any] | None:
"""Executa um SELECT e retorna apenas a primeira linha (ou None)."""
resultado = self.fetch_all(sql, params)
return resultado[0] if resultado else None
def close(self) -> None:
"""Fecha a conexão, se aberta."""
if self._conn is not None:
try:
self._conn.close()
except Exception: # pragma: no cover - best effort
pass
finally:
self._conn = None
# ----------------------------------------------------------------------
# Fábrica com cache simples (singleton por processo)
# ----------------------------------------------------------------------
_cliente_singleton: TursoClient | None = None
def get_turso_client() -> TursoClient:
"""Retorna uma instância única de :class:`TursoClient` para o processo.
Lê as credenciais a partir das configurações (variáveis de ambiente).
"""
global _cliente_singleton
if _cliente_singleton is None:
settings = get_settings()
_cliente_singleton = TursoClient(
database_url=settings.turso.database_url,
auth_token=settings.turso.auth_token,
)
return _cliente_singleton