Projections¶
Projections provide a structured way to read and write data efficiently. They handle event collection, logging, event bus publishing, and transactional commit/rollback automatically.
Data Models¶
from pydantic import BaseModel
class UserSearch(BaseModel):
query: str
page: int = 1
class UserUpdate(BaseModel):
user_id: str
name: str
email: str
ReadProjection¶
ReadProjection(ReadProjectionBase) — Base class for synchronous read projections.
Constructor¶
ReadProjection(**fields)
| Parameter | Type | Description |
|---|---|---|
**fields |
Port \| Session |
Field dependencies. Includes optional session fields (concrete type, e.g. session: PostgresSession) and port dependencies. All fields (except sessions) must be Port subclasses. |
Methods¶
| Method | Signature | Description |
|---|---|---|
read |
read(self, *args, **kwargs) -> Any |
Abstract. Execute a read operation. Auto-wrapped with EventCollector, logging, and event bus publish. |
read Parameters¶
| Parameter | Type | Description |
|---|---|---|
*args |
Any |
Positional arguments for the read operation. |
**kwargs |
Any |
Keyword arguments for the read operation. |
Auto-Wrapping Behavior¶
When read() is called:
- Events are collected via
EventCollectorduring execution. - On success: events are logged on each declared logger, events are published on each declared event bus.
- On failure: the exception is logged on each declared logger and re-raised.
Example¶
from pydantic import BaseModel
from aod.infrastructure import ReadProjection
class UserSearch(BaseModel):
user_id: str
class UserListProjection(ReadProjection):
session: PostgresSession
def read(self, model: UserSearch) -> list[User]:
rows = self.session.query(
"SELECT * FROM users WHERE id = :id",
{"id": model.user_id},
)
return [User(**row) for row in rows]
WriteProjection¶
WriteProjection(WriteProjectionBase) — Base class for synchronous write projections.
Constructor¶
WriteProjection(**fields)
| Parameter | Type | Description |
|---|---|---|
**fields |
Port \| Session |
Field dependencies. Includes optional session fields (concrete type, e.g. session: PostgresSession) and port dependencies. All fields (except sessions) must be Port subclasses. |
Methods¶
| Method | Signature | Description |
|---|---|---|
write |
write(self, *args, **kwargs) -> Any |
Abstract. Execute a write operation. Auto-wrapped with CommitContext, EventCollector, logging, rollback, and event bus publish. |
write Parameters¶
| Parameter | Type | Description |
|---|---|---|
*args |
Any |
Positional arguments for the write operation. |
**kwargs |
Any |
Keyword arguments for the write operation. |
Auto-Wrapping Behavior¶
When write() is called:
- A
CommitContextis set (enablingsession.commit()). - Events are collected via
EventCollectorduring execution. - On success: events are logged on each declared logger, events are published on each declared event bus.
- On failure:
session.rollback()is called (if session is dirty), the exception is logged on each declared logger and re-raised. - The
CommitContextis always reset in thefinallyblock.
Example¶
from pydantic import BaseModel
from aod.infrastructure import WriteProjection
class UpdateUserInput(BaseModel):
user_id: str
name: str
class UserUpdateProjection(WriteProjection):
session: PostgresSession
def write(self, model: UpdateUserInput) -> None:
self.session.execute(
"UPDATE users SET name = :name WHERE id = :id",
{"name": model.name, "id": model.user_id},
)
Projection¶
Projection(ReadProjection, WriteProjection) — Combines both read and write capabilities.
Constructor¶
Projection(**fields)
| Parameter | Type | Description |
|---|---|---|
**fields |
Port \| Session |
Field dependencies. Includes optional session fields (concrete type, e.g. session: PostgresSession) and port dependencies. All fields (except sessions) must be Port subclasses. |
Methods¶
Includes both read(self, model: Any) -> Any and write(self, model: Any) -> Any.
Async Variants¶
| Class | Import | Base |
|---|---|---|
AsyncReadProjection |
from aod.infrastructure import AsyncReadProjection |
AsyncReadProjectionBase |
AsyncWriteProjection |
from aod.infrastructure import AsyncWriteProjection |
AsyncWriteProjectionBase |
AsyncProjection |
from aod.infrastructure import AsyncProjection |
AsyncReadProjection + AsyncWriteProjection |
Constructor¶
Async variants accept the same fields as their sync counterparts. Session fields must use concrete types (e.g., session: AsyncPostgresSession). Multiple session fields are supported.
All async variants expose the same methods but as async:
- async read(self, *args, **kwargs) -> Any
- async write(self, *args, **kwargs) -> Any
Field Validation¶
Projections enforce these rules at class creation time:
- Concrete session types — Session fields must use concrete types (e.g.,
session: PostgresSession), neverSession | None. - No HandlerProtocol — Fields typed as
HandlerProtocolor its subclasses raiseInvalidUseCasePortFieldError. - Multiple sessions allowed — Projections can declare multiple session fields with different types.
- No HandlerProtocol — Fields typed as
HandlerProtocolor its subclasses raiseInvalidUseCasePortFieldError.
Event Collection¶
Events emitted during read() or write() are automatically collected:
- Collected events are stored on
self.eventsafter execution completes. - Events are published on the event bus after a successful operation.
- Write projections require a successful commit before events are published.
Testing with Spy Sessions¶
from aod.testing.doubles import spy_session
from pydantic import BaseModel
class UserSearch(BaseModel):
user_id: int
StubMySession = spy_session(MySession)
class MyReadProjection(ReadProjection):
session: StubMySession
def read(self, model: UserSearch) -> Any:
result = self.session.query("SELECT * FROM users")
...
proj = MyReadProjection(session=StubMySession())
result = proj.read(UserSearch(user_id=42))
assert proj.session.query.called