Resumen de operaciones — Sincronización en vivo
6
Proyectos sincronizados
Pipeline Clientes CULTIVA
4
Páginas creadas
create_page() · nuevas entradas
2
Páginas actualizadas
update_page() · sin duplicados
3/s
Rate limit gestionado
backoff exponencial activo
Flujo de sincronización
Autenticación
NOTION_TOKEN env
→
Query BD
detect duplicados
→
Lee datos externos
CSV / HubSpot API
→
Create / Update
upsert inteligente
→
Rate limit
429 + backoff
→
Notion actualizado
6 filas en BD
Base de datos Notion — Pipeline Clientes CULTIVA
Pipeline Clientes CULTIVA
db: a3f8c2d1-9e4b-4a7f-b521-cd8e12345678| Cliente | Servicio | Estado | Valor | Inicio | Cierre | Operación |
|---|---|---|---|---|---|---|
| Clínica DentaPlus | ⚙️ Automatización Lead Scoring | Activo | 2.400 € | 2026-05-01 | 2026-07-31 | update |
| Bodega Ramírez | 💬 Chatbot WhatsApp | Propuesta | 1.800 € | 2026-06-01 | 2026-08-15 | create |
| Grupo Torres Inmob. | 🤖 Agente IA Captación | Activo | 3.600 € | 2026-04-15 | 2026-10-15 | update |
| Academia FitPro | 📧 Automatización Email | Cerrado ✓ | 1.200 € | 2026-03-01 | 2026-04-30 | create |
| Restaurante Casa Nova | 🔍 SEO + Contenidos IA | Propuesta | 950 € | 2026-06-10 | 2026-09-30 | create |
| SaaS Contabilix | 📊 Dashboard Analytics | Negociación | 5.200 € | 2026-05-20 | 2026-12-31 | create |
Patrones de código aplicados
notion_config.py
# Autenticación — token interno CULTIVA IA import os import requests API = "https://api.notion.com/v1" HEADERS = { "Authorization": f"Bearer {os.environ['NOTION_TOKEN']}", "Notion-Version": "2022-06-28", "Content-Type": "application/json", } # DB pipeline de proyectos CULTIVA DB_PIPELINE = "a3f8c2d1-9e4b-4a7f-b521-cd8e12345678"
query_database.py
def query_pipeline(estado=None) -> list: """Consulta BD con filtro opcional por estado.""" body = {} if estado: body["filter"] = { "property": "Estado", "select": {"equals": estado} } pages, cursor = [], None while True: if cursor: body["start_cursor"] = cursor r = requests.post( f"{API}/databases/{DB_PIPELINE}/query", json=body, headers=HEADERS).json() pages.extend(r["results"]) if not r.get("has_more"): break cursor = r["next_cursor"] return pages
create_proyecto.py
def crear_proyecto(p: dict) -> dict: """Crea fila en Pipeline Clientes CULTIVA.""" return requests.post( f"{API}/pages", json={ "parent": {"database_id": DB_PIPELINE}, "properties": { "Nombre": {"title": [{"text": {"content": p["cliente"]}}]}, "Servicio": {"select": {"name": p["servicio"]}}, "Estado": {"select": {"name": p["estado"]}}, "Valor €": {"number": p["valor"]}, "Fecha inicio": {"date": {"start": p["inicio"]}}, } }, headers=HEADERS).json()
safe_request.py
import time def safe_request(method, url, **kw): """Reintento con backoff — 3 req/s Notion API.""" for attempt in range(5): resp = requests.request( method, url, headers=HEADERS, **kw) if resp.status_code == 429: wait = int(resp.headers.get( "Retry-After", 2 ** attempt)) time.sleep(wait) continue resp.raise_for_status() return resp.json() raise Exception("Rate limit: 5 reintentos fallidos")
sync_pipeline_cultiva.py — Script principal de sincronización
def sync_proyectos(proyectos: list[dict]): """Upsert inteligente: crea o actualiza proyectos en Notion sin duplicados. Args: proyectos: Lista de dicts con keys: cliente, servicio, estado, valor, inicio, cierre """ # 1. Obtener páginas existentes y construir índice por nombre de cliente existentes = query_pipeline() indice = {} for page in existentes: titulo = page["properties"]["Nombre"]["title"] if titulo: nombre = titulo[0]["plain_text"] indice[nombre] = page["id"] # 2. Por cada proyecto: crear si es nuevo, actualizar si existe stats = {"created": 0, "updated": 0} for p in proyectos: props = { "Nombre": {"title": [{"text": {"content": p["cliente"]}}]}, "Servicio": {"select": {"name": p["servicio"]}}, "Estado": {"select": {"name": p["estado"]}}, "Valor €": {"number": p["valor"]}, "Fecha inicio": {"date": {"start": p["inicio"], "end": p["cierre"]}}, } if p["cliente"] in indice: # Actualizar existente update_page(indice[p["cliente"]], props) stats["updated"] += 1 else: # Crear nuevo create_page(DB_PIPELINE, props) stats["created"] += 1 print(f"Sync completado: {stats['created']} creados, {stats['updated']} actualizados") return stats # --- Ejecutar --- if __name__ == "__main__": proyectos_cultiva = [ {"cliente": "Clínica DentaPlus", "servicio": "Automatización Lead Scoring", "estado": "Activo", "valor": 2400, "inicio": "2026-05-01", "cierre": "2026-07-31"}, {"cliente": "Bodega Ramírez", "servicio": "Chatbot WhatsApp", "estado": "Propuesta", "valor": 1800, "inicio": "2026-06-01", "cierre": "2026-08-15"}, {"cliente": "Grupo Torres Inmob.", "servicio": "Agente IA Captación", "estado": "Activo", "valor": 3600, "inicio": "2026-04-15", "cierre": "2026-10-15"}, {"cliente": "Academia FitPro", "servicio": "Automatización Email", "estado": "Cerrado", "valor": 1200, "inicio": "2026-03-01", "cierre": "2026-04-30"}, {"cliente": "Restaurante Casa Nova", "servicio": "SEO + Contenidos IA", "estado": "Propuesta", "valor": 950, "inicio": "2026-06-10", "cierre": "2026-09-30"}, {"cliente": "SaaS Contabilix", "servicio": "Dashboard Analytics", "estado": "Negociación","valor": 5200, "inicio": "2026-05-20", "cierre": "2026-12-31"}, ] result = sync_proyectos(proyectos_cultiva) # Output: Sync completado: 4 creados, 2 actualizados
Operaciones API cubiertas
Database Query
Consulta con filtros por estado, paginación automática con cursor hasta agotar resultados (max 100/página).
POST /databases/{id}/query
Create Page
Crea nueva fila en la BD con título, select, number y date. Soporta children blocks opcionales.
POST /pages
Update Page
Actualiza propiedades de páginas existentes via PATCH. Evita duplicados usando el índice por nombre.
PATCH /pages/{id}
Append Blocks
Añade heading, párrafo, bullet list, to-do y code blocks al contenido de cada página de proyecto.
PATCH /blocks/{id}/children
Search
Busca páginas y bases de datos en todo el workspace. Filtro por tipo: "page" o "database".
POST /search
Rate Limit Handler
Backoff exponencial en 429 con Retry-After header. Hasta 5 reintentos antes de lanzar excepción.
429 → sleep(2^n)