Add an event¶
Goal: a new typed event that your module publishes, such as
notes.note.archived.v1, so other modules can react to it.
Use it when something happened in your module that other modules may care about, and the producer does not need their answer.
Do not use it when you need an answer now (use a contract), or to react to another module's event (that is a consumer: a view of the other module's channel, never a second producer). To change the payload of an event you already publish, see Change an event's payload.
Prerequisites¶
- The module exists (Add a module).
- If
events.pyalready exists, it must parse and importEventfromsandloom.sdk(andUUIDfromuuidfor the default fields). The generator refuses otherwise and names the missing import.
Steps¶
1. Run the generator¶
sandloom add event notes NoteArchived
sandloom add event notes NoteArchived --field note_id:UUID --field reason:str # explicit fields
- The channel is derived from the module and the class name:
NoteArchivedinnotesgivesnotes.note.archived.v1. - Without
--field, the event gets<first word>_id: UUID(herenote_id). Atenant_id: UUIDfield is always added; a consumer binds the tenant from it. --field name:typetakesstr,int,float,bool,UUID,datetime,dateorDecimal.- The class is appended to
events.py(created when absent). Existing code is never rewritten.
2. Read what it wrote¶
The appended class (the rest of the file is the module's existing events):
class NoteArchived(Event, channel="notes.note.archived.v1"):
note_id: UUID
tenant_id: UUID
The channel's first segment is the module's name, so this class is the
channel's producer: sandloom generate (run last by sandloom add)
snapshots its schema and exports it to the
shared DTO package (<app>/shared/events/notes.note.archived.v1.json), where
consumers in other repositories verify their views against it.
3. Publish it¶
Publish from a service, inside the unit of work that makes the change. The
event is written to the transactional outbox and is delivered only if the
unit commits. The tutorial app's tasks module does it like this:
async def create_item(body: ItemIn, *, owner_id: UUID) -> ItemOut:
item = TasksItem(title=body.title, owner_id=owner_id)
db().add(item)
await db().flush()
.publish() outside a unit of work raises (SANDLOOM-R002). In a route, the
request's unit is already open.
4. Test it¶
published(E) returns the events of E's channel that the code under test
published:
async def test_create_and_read_an_item(client: Any) -> None:
_, headers = as_user(tenant_id=uuid4())
with fake_contract(STATS_QUOTA_V1, Quota(allow=True)):
created = await client.post(
"/api/v1/tasks/items", json={"title": "first"}, headers=headers
)
assert created.status_code == 201, created.text
assert len(published(ItemCreated)) == 1
item = created.json()
fetched = await client.get(f"/api/v1/tasks/items/{item['id']}", headers=headers)
assert fetched.status_code == 200
assert fetched.json()["title"] == "first"
async def test_the_quota_refuses_a_new_item(client: Any) -> None:
_, headers = as_user(tenant_id=uuid4())
with fake_contract(STATS_QUOTA_V1, Quota(allow=False)):
response = await client.post(
"/api/v1/tasks/items", json={"title": "x"}, headers=headers
)
assert response.status_code == 402
assert published(ItemCreated) == []
async def test_items_need_an_authenticated_user(client: Any) -> None:
response = await client.post("/api/v1/tasks/items", json={"title": "x"})
assert response.status_code == 401
Verify¶
sandloom test notes
The first run writes the producer's schema to module.snapshot.json and to
the shared DTO package. Commit both. From then on, only an added optional
field is accepted on the same .v1; anything else fails with SANDLOOM-T005.
Errors you may hit¶
- SANDLOOM-B015: the channel is not
<module>.<entity>.<verb>.vN, a field is not a wire type, two producer classes share a channel, or the producer lives outside its module. - SANDLOOM-R002:
.publish()ran outside a unit of work. - SANDLOOM-R004: the payload does not match the channel's schema.
- SANDLOOM-T005: a breaking change on a
channel that already has a committed schema; publish a
.v2.
Related¶
- Events and consumers: delivery, ordering, the dead-letter queue.
- Add a consumer.
- Change an event's payload.
- Units of Work and
db(): why the event commits with the row.