Parte III — Ramas, savia y semillas

06 · Las ramas: servicios, API y CLI

Del tronco salen pocas ramas gruesas, y de ellas muchas finas. En Baobáxia: un servicio grande (SaberService) y muchos servicios pequeños a su alrededor.

Archivos para leer

  1. services/saber_service.py — SaberService (≈ 4000 líneas: léelo por secciones; los comentarios # ==== ESCRITA: … ==== son el índice).
  2. api/v3/app.py — el armado de la aplicación, middlewares, manejadores de error.
  3. api/v3/deps.py — el servicio global y el moc de cada petición.
  4. Una ruta de cada router: saberes.py, estrutura.py, colecoes.py, sync.py, pedidos.py, historico.py.
  5. services/jobs.py, services/cache_leitura.py, infra/vigia_api.py.
  6. publica/operacoes.py + cli.py — la misma operación sirviendo a la CLI y al bot.
  7. applications/ — las lentes (acervo, blog, mapa, timeline, agenda).

El mapa de la API

RouterRutasPara qué
saberes.py35CRUD, anexos, miniatura, subtítulos, texto, vista previa, copiar/mover, protección
estrutura.py32galaxias, mucuas, territorios, mocambolas, natures, tags, licencias
sync.py32mucua local, Tor, claves SSH, enlaces, instalar galaxia, sync, descubrimiento, red local
auth.py11login, token, registro, desafío, identidad
colecoes.py10colecciones e historias, miembros, portada
pedidos.py8pedidos y rutas (store-and-forward)
historico.py, recovery.py6 + 6línea de tiempo de git, restaurar; recuperación de identidad
relacoes.py, xmpp.py, activitypub.py4 + 3 + 4relaciones; puente XMPP; Fediverso (solo con BBX_FED_AP=on)
app/<lente>/…—rutas que registra cada aplicación

Documentación viva: http://127.0.0.1:8000/docs.

SaberService por dentro

classDiagram
    class SaberService {
        -dict _repos  «uuid → SaberRepository»
        -dict _graphs
        +galaxy_uuids() list
        +repo(galaxia) SaberRepository
        +search(… actor) Page
        +get_saber(uuid, actor) dict
        +create_saber / update_saber / delete_saber
        +add_contents(uuid, files, actor)
        +content_path / thumbnail_path (actor)
        +get_collection / membros_dinamicos / add_collection_member …
        +find_mocambola(username) (repo, saber)
        +effective_role(saber) str
        +galaxias_administradas(uuid) list
        +historico_galaxia / restaurar
        -_find_saber(uuid, galaxia)
        -_descritor(…) dict
        -_pos_escrita(repo, uuid)
    }
    class CopiasService
    class PedidosService
    class Operacoes {
        +buscar / publicar / editar / deletar
    }
    class InProcessBackend
    class HttpBackend
    SaberService "1" o-- "*" SaberRepository
    SaberService ..> CopiasService
    SaberService ..> PedidosService
    Operacoes --> InProcessBackend : CLI local / bot
    Operacoes --> HttpBackend : bbx --api-url
    InProcessBackend --> SaberService

SaberService es un "objeto dios": concentra mucho. Es el primer candidato a dividir cuando el equipo tenga aliento — ver el ejercicio 5.

La API no puede trabarse

Una mucua corre en hardware modesto. Lecciones que están en el código:

  • El trabajo pesado sale del event loop. La subida copia en bloques en el threadpool; sync, instalación y pedidos se vuelven jobs (services/jobs.py): la ruta responde 202 con un job_id y el front sigue GET /sync/jobs/{id}.
  • Lecturas repetidas se vuelven una sola (cache_leitura.py, single-flight + TTL corto invalidado por index.geracao()).
  • El vigía (vigia_api.py) registra en el log las peticiones trabadas y la pila de los hilos, antes de que el healthcheck reinicie el servicio.
  • 503 con Retry-After cuando se está trayendo un archivo de otra mucua: el front espera y lo intenta de nuevo (fetchComEspera).
sequenceDiagram
    actor P as Persona
    participant A as POST /sync/instalar-galaxia
    participant J as jobs
    participant T as thread
    P->>A: url, somente_leitura
    A->>J: criar("instalar-galaxia")
    A->>T: executar_em_thread(job)
    A-->>P: 202 {id, acompanhar}
    T->>T: clone → reindex → reflejar identidad
    T->>J: progreso("clonando…", "reindexando…")
    P->>J: GET /sync/jobs/{id} (hasta terminar)

La galaxia en .zip usa el mismo molde: POST /galaxias/{uuid}/exportar es un job que graba el zip (y el resultado trae el enlace para descargarlo), y POST /sync/importar-galaxia recibe el archivo en el cuerpo y lo instala en un job — el mismo _ativar_galaxia_instalada (reindexar, reflejar identidad) de la instalación por URL. Por la CLI: bbx exportar-galaxia y bbx importar-galaxia.

CLI y bot: una sola capa de operaciones

publica/operacoes.py implementa buscar/publicar/editar/borrar una vez. La CLI y el bot XMPP llaman a la misma Operacoes, con dos "backends": in-process (habla directo con SaberService) o HTTP (bbx --api-url, habla con una mucua remota).

Prácticas

  1. Escribe una ruta nueva GET /saberes/{uuid}/resumo que devuelva título, nature y número de anexos, respetando la lectura. Usa get_saber con actor. Pruébala llamando a la función directamente, como hacen los tests.
  2. En test_jobs_e_instalar_async.py, ¿por qué el job guarda el error real del git clone?
  3. Corre bbx buscar pesca --json y después bbx buscar pesca --api-url http://127.0.0.1:8000/api/v3. ¿Dónde se separan los dos caminos en el código?
  4. Abre un zip hecho con bbx exportar-galaxia. ¿Por qué lleva un git bundle y los anexos sueltos, y no la carpeta .git entera? (Pista: ¿qué vive en .git/config?) ¿Y qué rechaza ler_manifesto?
  5. Propón cómo dividir SaberService en tres (lectura, escritura, colecciones) sin romper las rutas. ¿Qué métodos privados se compartirían?

Para conversar

  • ¿Por qué una operación que tarda no debe retener la petición HTTP, cuando quien está del otro lado tiene mala conexión?

vedi questo capitolo come sapere