Parte III — Galhos, seiva e sementes

06 · Os galhos: serviços, API e CLI

Do tronco saem poucos galhos grossos, e deles muitos finos. No Baobáxia, um serviço grande (SaberService) e muitos serviços pequenos ao redor dele.

Arquivos para ler

  1. services/saber_service.py — SaberService (≈ 4000 linhas: leia por seção, os comentários # ==== ESCRITA: … ==== são o sumário).
  2. api/v3/app.py — a montagem da aplicação, middlewares, handlers de erro.
  3. api/v3/deps.py — o serviço global e o moc de cada requisição.
  4. Uma rota 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 — a mesma operação servindo CLI e bot.
  7. applications/ — as lentes (acervo, blog, mapa, timeline, agenda).

O mapa da API

RouterRotasPara quê
saberes.py35CRUD, anexos, thumb, legenda, texto, prévia, cópia/mover, proteção
estrutura.py32galáxias, mucuas, territórios, mocambolas, natures, tags, licenças
sync.py32mucua local, Tor, chaves SSH, links, instalar galáxia, sync, descoberta, rede local
auth.py11login, token, cadastro, desafio, identidade
colecoes.py10coleções e histórias, membros, capa
pedidos.py8pedidos e rotas (store-and-forward)
historico.py, recovery.py6 + 6linha do tempo do git, restaurar; recuperação de identidade
relacoes.py, xmpp.py, activitypub.py4 + 3 + 4relações; ponte XMPP; Fediverso (só com BBX_FED_AP=on)
app/<lente>/…—rotas que cada aplicação registra

Documentação 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 é um "objeto deus": concentra muito. É o primeiro candidato a dividir quando a equipe tiver fôlego — ver exercício 4.

A API não pode travar

Uma mucua roda em hardware modesto. Lições que estão no código:

  • Trabalho pesado sai do event loop. Upload copia em blocos no threadpool; sync, instalação e pedidos viram jobs (services/jobs.py): a rota responde 202 com um job_id e o front acompanha GET /sync/jobs/{id}.
  • Leituras repetidas viram uma só (cache_leitura.py, single-flight + TTL curto invalidado por index.geracao()).
  • O vigia (vigia_api.py) registra no log as requisições presas e a pilha das threads, antes que o healthcheck reinicie o serviço.
  • 503 com Retry-After quando um arquivo está sendo buscado de outra mucua: o front espera e tenta de novo (fetchComEspera).
sequenceDiagram
    actor P as Pessoa
    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 → espelhar identidade
    T->>J: progresso("clonando…", "reindexando…")
    P->>J: GET /sync/jobs/{id} (até concluir)

A galáxia em .zip usa o mesmo molde: POST /galaxias/{uuid}/exportar é um job que grava o zip (e o resultado traz o link para baixar), e POST /sync/importar-galaxia recebe o arquivo no corpo e instala num job — o mesmo _ativar_galaxia_instalada (reindexar, espelhar identidade) da instalação por URL. Pela CLI: bbx exportar-galaxia e bbx importar-galaxia.

CLI e bot: uma só camada de operações

publica/operacoes.py implementa buscar/publicar/editar/deletar uma vez. A CLI e o bot XMPP chamam a mesma Operacoes, com dois "backends": in-process (fala direto com o SaberService) ou HTTP (bbx --api-url, fala com uma mucua remota).

Práticas

  1. Escreva uma rota nova GET /saberes/{uuid}/resumo que devolva título, nature e número de anexos, respeitando a leitura. Use get_saber com actor. Teste chamando a função direto, como os testes fazem.
  2. Em test_jobs_e_instalar_async.py, por que o job guarda o erro real do git clone?
  3. Rode bbx buscar pesca --json e depois bbx buscar pesca --api-url http://127.0.0.1:8000/api/v3. Onde os dois caminhos se separam no código?
  4. Abra um zip feito por bbx exportar-galaxia. Por que ele leva um git bundle e os anexos soltos, e não a pasta .git inteira? (Dica: o que mora em .git/config?) E o que ler_manifesto recusa?
  5. Proponha como dividir SaberService em três (leitura, escrita, coleções) sem quebrar as rotas. Que métodos privados seriam compartilhados?

Para conversar

  • Por que uma operação que demora não deve segurar a requisição HTTP, quando quem está do outro lado tem internet ruim?

vedi questo capitolo come sapere