Part III — Branches, sap and seeds
06 · The branches: services, API and CLI
Few thick branches grow out of the trunk, and many thin ones out of them. In Baobáxia: one big service (
SaberService) and many small services around it.
Files to read
services/saber_service.py—SaberService(≈ 4000 lines: read it by section; the# ==== ESCRITA: … ====comments are the table of contents).api/v3/app.py— how the application is assembled, middlewares, error handlers.api/v3/deps.py— the global service and themocof each request.- One route from each router:
saberes.py,estrutura.py,colecoes.py,sync.py,pedidos.py,historico.py. services/jobs.py,services/cache_leitura.py,infra/vigia_api.py.publica/operacoes.py+cli.py— the same operation serving the CLI and the bot.applications/— the lenses (collection, blog, map, timeline, calendar).
The map of the API
| Router | Routes | What for |
|---|---|---|
saberes.py | 35 | CRUD, attachments, thumb, subtitles, text, preview, copy/move, protection |
estrutura.py | 32 | galaxies, mucuas, territories, mocambolas, natures, tags, licenses |
sync.py | 32 | local mucua, Tor, SSH keys, links, installing a galaxy, sync, discovery, local network |
auth.py | 11 | login, token, sign-up, challenge, identity |
colecoes.py | 10 | collections and stories, members, cover |
pedidos.py | 8 | requests and routes (store-and-forward) |
historico.py, recovery.py | 6 + 6 | git timeline, restoring; identity recovery |
relacoes.py, xmpp.py, activitypub.py | 4 + 3 + 4 | relations; XMPP bridge; Fediverse (only with BBX_FED_AP=on) |
app/<lens>/… | — | routes each application registers |
Live documentation: http://127.0.0.1:8000/docs.
Inside 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 is a "god object": it concentrates a lot. It is the first candidate to split up when the team has the energy — see exercise 5.
The API must not freeze
A mucua runs on modest hardware. Lessons that are in the code:
- Heavy work leaves the event loop. Uploads copy in blocks in the threadpool; sync, installs and requests become jobs (
services/jobs.py): the route answers202with ajob_idand the front followsGET /sync/jobs/{id}. - Repeated reads become one (
cache_leitura.py, single-flight + a short TTL invalidated byindex.geracao()). - The watcher (
vigia_api.py) logs stuck requests and the thread stacks before the healthcheck restarts the service. - 503 with
Retry-Afterwhen a file is being fetched from another mucua: the front waits and tries again (fetchComEspera).
sequenceDiagram
actor P as Person
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 → mirror identity
T->>J: progress("cloning…", "reindexing…")
P->>J: GET /sync/jobs/{id} (until done)
The galaxy as a .zip uses the same mould: POST /galaxias/{uuid}/exportar is a job that writes the zip (and its result carries the download link), and POST /sync/importar-galaxia receives the file in the body and installs it in a job — the same _ativar_galaxia_instalada (reindex, mirror identity) as an install by URL. From the CLI: bbx exportar-galaxia and bbx importar-galaxia.
CLI and bot: a single layer of operations
publica/operacoes.py implements search/publish/edit/delete once. The CLI and the XMPP bot call the same Operacoes, with two "backends": in-process (talks directly to SaberService) or HTTP (bbx --api-url, talks to a remote mucua).
Practice
- Write a new route
GET /saberes/{uuid}/resumothat returns the title, nature and number of attachments, respecting read permissions. Useget_saberwithactor. Test it by calling the function directly, as the tests do. - In
test_jobs_e_instalar_async.py, why does the job keep the real error fromgit clone? - Run
bbx buscar pesca --jsonand thenbbx buscar pesca --api-url http://127.0.0.1:8000/api/v3. Where do the two paths split in the code? - Open a zip made by
bbx exportar-galaxia. Why does it carry agit bundleand loose attachments, and not the whole.gitfolder? (Hint: what lives in.git/config?) And what doesler_manifestorefuse? - Propose how to split
SaberServiceinto three (reading, writing, collections) without breaking the routes. Which private methods would be shared?
To talk about
- Why shouldn't a slow operation hold on to the HTTP request, when the person on the other end has a poor internet connection?
