Spaces:
Sleeping
Sleeping
| """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 | |