New skills covering 10 categories: **Security & Audit**: 007 (STRIDE/PASTA/OWASP), cred-omega (secrets management) **AI Personas**: Karpathy, Hinton, Sutskever, LeCun (4 sub-skills), Altman, Musk, Gates, Jobs, Buffett **Multi-agent Orchestration**: agent-orchestrator, task-intelligence, multi-advisor **Code Analysis**: matematico-tao (Terence Tao-inspired mathematical code analysis) **Social & Messaging**: Instagram Graph API, Telegram Bot, WhatsApp Cloud API, social-orchestrator **Image Generation**: AI Studio (Gemini), Stability AI, ComfyUI Gateway, image-studio router **Brazilian Domain**: 6 auction specialist modules, 2 legal advisors, auctioneers data scraper **Product & Growth**: design, invention, monetization, analytics, growth engine **DevOps & LLM Ops**: Docker/CI-CD/AWS, RAG/embeddings/fine-tuning **Skill Governance**: installer, sentinel auditor, context management Each skill includes: - Standardized YAML frontmatter (name, description, risk, source, tags, tools) - Structured sections (Overview, When to Use, How it Works, Best Practices) - Python scripts and reference documentation where applicable - Cross-platform compatibility (Claude Code, Antigravity, Cursor, Gemini CLI, Codex CLI) Co-authored-by: ProgramadorBrasil <214873561+ProgramadorBrasil@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
217 lines
7.6 KiB
Python
217 lines
7.6 KiB
Python
"""
|
|
Camada de persistência SQLite para dados de leiloeiros das Juntas Comerciais.
|
|
|
|
Uso:
|
|
from db import Database
|
|
db = Database() # abre/cria o banco em data/leiloeiros.db
|
|
db.init() # cria tabelas se não existirem
|
|
db.upsert_many(records) # insere/atualiza lista de Leiloeiro
|
|
rows = db.get_all() # retorna todos os registros
|
|
rows = db.get_by_estado("SP")
|
|
stats = db.get_stats()
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
# Caminho padrão do banco — relativo ao diretório pai de scripts/
|
|
_DEFAULT_DB = Path(__file__).parent.parent / "data" / "leiloeiros.db"
|
|
|
|
DDL = """
|
|
CREATE TABLE IF NOT EXISTS leiloeiros (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
estado TEXT NOT NULL,
|
|
junta TEXT NOT NULL,
|
|
matricula TEXT,
|
|
nome TEXT NOT NULL,
|
|
cpf_cnpj TEXT,
|
|
situacao TEXT,
|
|
endereco TEXT,
|
|
municipio TEXT,
|
|
telefone TEXT,
|
|
email TEXT,
|
|
data_registro TEXT,
|
|
data_atualizacao TEXT,
|
|
url_fonte TEXT,
|
|
scraped_at TEXT NOT NULL,
|
|
UNIQUE (estado, matricula) ON CONFLICT REPLACE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_estado ON leiloeiros (estado);
|
|
CREATE INDEX IF NOT EXISTS idx_nome ON leiloeiros (nome);
|
|
CREATE INDEX IF NOT EXISTS idx_situacao ON leiloeiros (situacao);
|
|
CREATE INDEX IF NOT EXISTS idx_scraped ON leiloeiros (scraped_at);
|
|
"""
|
|
|
|
UPSERT_SQL = """
|
|
INSERT INTO leiloeiros
|
|
(estado, junta, matricula, nome, cpf_cnpj, situacao,
|
|
endereco, municipio, telefone, email,
|
|
data_registro, data_atualizacao, url_fonte, scraped_at)
|
|
VALUES
|
|
(:estado, :junta, :matricula, :nome, :cpf_cnpj, :situacao,
|
|
:endereco, :municipio, :telefone, :email,
|
|
:data_registro, :data_atualizacao, :url_fonte, :scraped_at)
|
|
ON CONFLICT(estado, matricula) DO UPDATE SET
|
|
junta = excluded.junta,
|
|
nome = excluded.nome,
|
|
cpf_cnpj = excluded.cpf_cnpj,
|
|
situacao = excluded.situacao,
|
|
endereco = excluded.endereco,
|
|
municipio = excluded.municipio,
|
|
telefone = excluded.telefone,
|
|
email = excluded.email,
|
|
data_registro = excluded.data_registro,
|
|
data_atualizacao = excluded.data_atualizacao,
|
|
url_fonte = excluded.url_fonte,
|
|
scraped_at = excluded.scraped_at
|
|
"""
|
|
|
|
# Para registros sem matrícula, usa INSERT simples (não upsert)
|
|
INSERT_SQL = """
|
|
INSERT INTO leiloeiros
|
|
(estado, junta, matricula, nome, cpf_cnpj, situacao,
|
|
endereco, municipio, telefone, email,
|
|
data_registro, data_atualizacao, url_fonte, scraped_at)
|
|
VALUES
|
|
(:estado, :junta, :matricula, :nome, :cpf_cnpj, :situacao,
|
|
:endereco, :municipio, :telefone, :email,
|
|
:data_registro, :data_atualizacao, :url_fonte, :scraped_at)
|
|
"""
|
|
|
|
|
|
class Database:
|
|
def __init__(self, db_path: Path = _DEFAULT_DB):
|
|
self.db_path = Path(db_path)
|
|
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
def _connect(self) -> sqlite3.Connection:
|
|
conn = sqlite3.connect(self.db_path)
|
|
conn.row_factory = sqlite3.Row
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("PRAGMA synchronous=NORMAL")
|
|
return conn
|
|
|
|
def init(self) -> None:
|
|
"""Cria tabelas e índices se não existirem."""
|
|
with self._connect() as conn:
|
|
conn.executescript(DDL)
|
|
|
|
def upsert_many(self, records: List[Dict[str, Any]]) -> int:
|
|
"""
|
|
Insere ou atualiza registros.
|
|
Registros sem matrícula são inseridos sempre (para não perder dados).
|
|
Retorna o número de registros processados.
|
|
"""
|
|
with_matricula = [r for r in records if r.get("matricula")]
|
|
without_matricula = [r for r in records if not r.get("matricula")]
|
|
|
|
count = 0
|
|
with self._connect() as conn:
|
|
if with_matricula:
|
|
conn.executemany(UPSERT_SQL, with_matricula)
|
|
count += len(with_matricula)
|
|
if without_matricula:
|
|
# Evitar duplicatas exatas por (estado + nome + scraped_at)
|
|
conn.executemany(INSERT_SQL, without_matricula)
|
|
count += len(without_matricula)
|
|
return count
|
|
|
|
def get_all(
|
|
self,
|
|
estado: Optional[str] = None,
|
|
situacao: Optional[str] = None,
|
|
nome_like: Optional[str] = None,
|
|
limit: int = 0,
|
|
offset: int = 0,
|
|
) -> List[Dict[str, Any]]:
|
|
"""Retorna registros com filtros opcionais."""
|
|
conditions = []
|
|
params: List[Any] = []
|
|
|
|
if estado:
|
|
conditions.append("estado = ?")
|
|
params.append(estado.upper())
|
|
if situacao:
|
|
conditions.append("situacao = ?")
|
|
params.append(situacao.upper())
|
|
if nome_like:
|
|
conditions.append("nome LIKE ?")
|
|
params.append(f"%{nome_like}%")
|
|
|
|
where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
|
|
|
|
if limit > 0:
|
|
sql = f"SELECT * FROM leiloeiros {where} ORDER BY estado, nome LIMIT ? OFFSET ?"
|
|
params.extend([limit, offset])
|
|
else:
|
|
sql = f"SELECT * FROM leiloeiros {where} ORDER BY estado, nome"
|
|
|
|
with self._connect() as conn:
|
|
rows = conn.execute(sql, params).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
def get_by_estado(self, estado: str) -> List[Dict[str, Any]]:
|
|
return self.get_all(estado=estado)
|
|
|
|
def get_stats(self) -> List[Dict[str, Any]]:
|
|
"""Retorna contagem de leiloeiros por estado."""
|
|
sql = """
|
|
SELECT
|
|
estado,
|
|
junta,
|
|
COUNT(*) as total,
|
|
SUM(CASE WHEN situacao = 'ATIVO' THEN 1 ELSE 0 END) as ativos,
|
|
MAX(scraped_at) as ultima_coleta
|
|
FROM leiloeiros
|
|
GROUP BY estado, junta
|
|
ORDER BY total DESC
|
|
"""
|
|
with self._connect() as conn:
|
|
rows = conn.execute(sql).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
def get_total(self) -> int:
|
|
with self._connect() as conn:
|
|
return conn.execute("SELECT COUNT(*) FROM leiloeiros").fetchone()[0]
|
|
|
|
def search(self, query: str, limit: int = 50) -> List[Dict[str, Any]]:
|
|
"""Busca full-text por nome, matrícula ou município."""
|
|
sql = """
|
|
SELECT * FROM leiloeiros
|
|
WHERE nome LIKE ? OR matricula LIKE ? OR municipio LIKE ? OR email LIKE ?
|
|
ORDER BY estado, nome
|
|
LIMIT ?
|
|
"""
|
|
q = f"%{query}%"
|
|
with self._connect() as conn:
|
|
rows = conn.execute(sql, [q, q, q, q, limit]).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
def dump_all_json(self) -> str:
|
|
"""Retorna todos os dados como string JSON."""
|
|
return json.dumps(self.get_all(), ensure_ascii=False, indent=2)
|
|
|
|
|
|
# ── CLI rápido para verificação ──────────────────────────────────────────────
|
|
if __name__ == "__main__":
|
|
import sys
|
|
db = Database()
|
|
db.init()
|
|
stats = db.get_stats()
|
|
if not stats:
|
|
print("Banco vazio. Execute run_all.py primeiro.")
|
|
sys.exit(0)
|
|
total = db.get_total()
|
|
print(f"\nTotal de leiloeiros: {total}\n")
|
|
print(f"{'Estado':<8} {'Junta':<12} {'Total':>6} {'Ativos':>6} {'Última Coleta'}")
|
|
print("-" * 60)
|
|
for r in stats:
|
|
print(
|
|
f"{r['estado']:<8} {r['junta']:<12} "
|
|
f"{r['total']:>6} {r['ativos']:>6} {r['ultima_coleta'][:10]}"
|
|
)
|