Parte III — Rami, linfa e semi

06 · I rami: servizi, API e CLI

Dal tronco escono pochi rami grossi, e da questi molti sottili. In Baobáxia: un servizio grande (SaberService) e molti servizi piccoli intorno a lui.

File da leggere

  1. services/saber_service.py — SaberService (≈ 4000 righe: leggilo per sezioni; i commenti # ==== ESCRITA: … ==== sono l'indice).
  2. api/v3/app.py — l'assemblaggio dell'applicazione, middleware, gestori di errore.
  3. api/v3/deps.py — il servizio globale e il moc di ogni richiesta.
  4. Una rotta di ogni 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 stessa operazione al servizio della CLI e del bot.
  7. applications/ — le lenti (archivio, blog, mappa, timeline, agenda).

La mappa dell'API

RouterRotteA cosa serve
saberes.py35CRUD, allegati, miniature, sottotitoli, testo, anteprima, copia/sposta, protezione
estrutura.py32galassie, mucua, territori, mocambola, nature, tag, licenze
sync.py32mucua locale, Tor, chiavi SSH, link, installare galassie, sync, scoperta, rete locale
auth.py11login, token, registrazione, sfida, identità
colecoes.py10collezioni e storie, membri, copertina
pedidos.py8richieste e rotte (store-and-forward)
historico.py, recovery.py6 + 6timeline di git, ripristino; recupero dell'identità
relacoes.py, xmpp.py, activitypub.py4 + 3 + 4relazioni; ponte XMPP; Fediverso (solo con BBX_FED_AP=on)
app/<lente>/…—rotte che ogni applicazione registra

Documentazione viva: http://127.0.0.1:8000/docs.

Dentro SaberService

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 è un "oggetto dio": concentra molto. È il primo candidato da dividere quando la squadra avrà energie — vedi l'esercizio 5.

L'API non deve bloccarsi

Una mucua gira su hardware modesto. Lezioni che stanno nel codice:

  • Il lavoro pesante esce dall'event loop. L'upload copia a blocchi nel threadpool; sync, installazioni e richieste diventano job (services/jobs.py): la rotta risponde 202 con un job_id e il front segue GET /sync/jobs/{id}.
  • Letture ripetute diventano una sola (cache_leitura.py, single-flight + TTL breve invalidato da index.geracao()).
  • La sentinella (vigia_api.py) registra nel log le richieste bloccate e lo stack dei thread, prima che l'healthcheck riavvii il servizio.
  • 503 con Retry-After quando un file viene preso da un'altra mucua: il front aspetta e riprova (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 → rispecchiare identità
    T->>J: avanzamento("clonazione…", "reindicizzazione…")
    P->>J: GET /sync/jobs/{id} (fino alla fine)

La galassia in .zip usa lo stesso stampo: POST /galaxias/{uuid}/exportar è un job che scrive lo zip (e il risultato porta il link per scaricarlo), e POST /sync/importar-galaxia riceve il file nel corpo e lo installa in un job — lo stesso _ativar_galaxia_instalada (reindicizzare, rispecchiare l'identità) dell'installazione da URL. Dalla CLI: bbx exportar-galaxia e bbx importar-galaxia.

CLI e bot: un solo strato di operazioni

publica/operacoes.py implementa cerca/pubblica/modifica/cancella una volta sola. La CLI e il bot XMPP chiamano la stessa Operacoes, con due "backend": in-process (parla direttamente con SaberService) o HTTP (bbx --api-url, parla con una mucua remota).

Esercizi

  1. Scrivi una rotta nuova GET /saberes/{uuid}/resumo che restituisca titolo, nature e numero di allegati, rispettando i permessi di lettura. Usa get_saber con actor. Provala chiamando la funzione direttamente, come fanno i test.
  2. In test_jobs_e_instalar_async.py, perché il job conserva l'errore reale di git clone?
  3. Esegui bbx buscar pesca --json e poi bbx buscar pesca --api-url http://127.0.0.1:8000/api/v3. Dove si separano le due strade nel codice?
  4. Apri uno zip fatto da bbx exportar-galaxia. Perché contiene un git bundle e gli allegati sciolti, e non l'intera cartella .git? (Suggerimento: cosa vive in .git/config?) E cosa rifiuta ler_manifesto?
  5. Proponi come dividere SaberService in tre (lettura, scrittura, collezioni) senza rompere le rotte. Quali metodi privati sarebbero condivisi?

Da discutere

  • Perché un'operazione lenta non deve trattenere la richiesta HTTP, quando chi sta dall'altra parte ha una connessione scadente?

ver este capítulo como saber