Skip to content

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? False
user_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 -> 200
POST /api/v1/library_title with the same -> 403 (reader group only)
GET /api/v1/library_title with no token -> 401

So 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, cursor and tag are reserved; genre=science filters.
  • Pagination is limit/offset or 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=csv
id,name,author,isbn,published_year,genre,summary,cover,kws_custom_rayon_prefere

Formats 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: 1

The 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 3
PATCH /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 -> 200
call 4 -> 429 retry-after: 36

The 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 -> 401

kewos.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.