Plugging another system into Kewos
The rest of this tour speaks to whoever writes a module. This page speaks to whoever stays outside: a third-party system, a sync script, an in-house connector.
A token, not an account
Another system has no user account. It has a service token: no user_id, permissions coming
from its groups alone.
from kewos.auth.tokens import create_service_token
issued = create_service_token(engine, name="city-hall-software", groups=("library_reader",))token issued : kws_IfsG4HDR... (36 characters, the only plaintext copy)in the database : 618039e46002f1cd...is the plaintext token there? Falseuser_id : None | type : service | groups : ['library_reader']The database holds a hash only: it cannot replay the token. And the groups do all the work:
GET /api/v1/library_title with the token -> 200POST /api/v1/library_title with the same -> 403 (reader group only)GET /api/v1/library_title with no token -> 401So an integrator does not ask for “an admin account”: they ask for a token and the list of groups they need.
Every model is already an API
There is nothing to expose: every model in the registry has its endpoint, and so do Builder modules.
GET /api/v1/library_title?genre=science&sort=name&limit=2
{"count": 1, "next_cursor": null, "results": [{"name": "A Brief History of Time", "genre": "science", ...}]}- Any non-reserved query parameter is a column filter.
limit,offset,sort,cursorandtagare reserved;genre=sciencefilters. - Pagination is
limit/offsetor cursor-based (next_cursor), which is safer when rows are inserted while you page. - An unknown field is refused by name, not silently ignored:
{"field": "order", "reason": "unknown or read-only field"}.
And the export gives the same content as a file:
GET /api/v1/library_title/export?format=csvid,name,author,isbn,published_year,genre,summary,cover,kws_custom_rayon_prefereFormats are csv, jsonl, json, xlsx, xml. Note the last column: a field the customer
added at runtime comes out too, without anyone touching the export.
Writing twice without creating twice
A connector that loses its connection does not know whether its POST landed. Hence the
Idempotency-Key header:
POST /api/v1/library_title Idempotency-Key: cmd-4711 -> 201 {"id": 12}POST /api/v1/library_title Idempotency-Key: cmd-4711 -> 201 {"id": 12}
records created: 1The second call does not replay the write: it returns the first response. Retrying becomes safe, which is the only way to write a reliable connector.
Not overwriting someone else’s work
Every record carries a version number. Sending it back in If-Match makes your write fail if
the record changed meanwhile:
GET /api/v1/library_title/1 -> version 3PATCH /api/v1/library_title/1 If-Match: 3 -> 200 (version becomes 4)PATCH /api/v1/library_title/1 If-Match: 3 -> 409 "version mismatch -- refetch and retry"Without If-Match, the last write wins, silently. That is a choice to make on purpose.
The rate limit
call 1 -> 200call 4 -> 429 retry-after: 36The refusal is a 429 with Retry-After in seconds: the client knows when to retry instead of
guessing. The counter is per token, so one chatty connector does not penalise the others.
JSON-RPC, for actions that are not CRUD
“Confirm an order” is not a PATCH. A module then exposes a method:
from kewos.api.jsonrpc import register_jsonrpc_method
register_jsonrpc_method("library.loan.renew", renew_loan)POST /api/v1/jsonrpc {"jsonrpc":"2.0","method":"kewos.ping","id":1} -> {"jsonrpc":"2.0","result":{"pong":true,"tenant":"main"},"id":1}
unknown method -> {"error":{"code":-32601,"message":"method not found: 'nobody.home'"}} bad envelope -> {"error":{"code":-32600,"message":"invalid JSON-RPC 2.0 envelope"}} no token -> 401kewos.ping returns the resolved tenant: one call checks that the stack answers and that
you are talking to the right customer.
Receiving an inbound call
The reverse exists too: a payment provider or a third-party service calls you. The kernel provides signature verification and the idempotency ledger: the same event replayed (same provider, same event id) is recorded once and has no effect the second time. The route answers 200 even on a replay, because a provider retries on any non-2xx and a 5xx storm is worse than a lost event.