@@ -27,6 +27,10 @@ Application subscriptions
2727
2828This module provides an :class: `~eventsourcing.projection.ApplicationSubscription ` class, which can
2929be used to "subscribe" to the domain events of an application.
30+ Please note, the :class: `~eventsourcing.popo.POPOApplicationRecorder ` and
31+ :class: `~eventsourcing.postgres.PostgresApplicationRecorder ` classes implement the
32+ required :func: `~eventsourcing.persistence.ApplicationRecorder.subscribe `
33+ method, but the :class: `~eventsourcing.sqlite.SQLiteApplicationRecorder ` class does not.
3034
3135Application subscription objects are iterators that return domain events from an application sequence.
3236Each domain event is accompanied by a tracking object that identifies the position of the
@@ -99,10 +103,48 @@ method which can be used to stop the subscription to the application recorder in
99103 subscription.stop() # ...so we can continue with the examples
100104
101105
102- Please note, the :class: `~eventsourcing.popo.POPOApplicationRecorder ` and
103- :class: `~eventsourcing.postgres.PostgresApplicationRecorder ` classes implement the
104- required :func: `~eventsourcing.persistence.ApplicationRecorder.subscribe `
105- method, but the :class: `~eventsourcing.sqlite.SQLiteApplicationRecorder ` class does not.
106+ The :class: `~eventsourcing.projection.DCBApplicationSubscription ` class is the equivalent for
107+ :ref: `DCB applications <DCB application >`. It returns :ref: `tagged decisions <DCB Tagged >`.
108+ Please note, the :ref: `DCB recorders <DCB recorders >` :class: `~eventsourcing.dcb.popo.InMemoryDCBRecorder `,
109+ :class: `~eventsourcing.dcb.postgres_tt.PostgresDCBRecorderTT ` and the ``eventsourcing_umadb `` extension
110+ all implement the required :func: `~eventsourcing.dcb.api.DCBRecorder.subscribe ` method.
111+
112+ .. code-block :: python
113+
114+ from uuid import UUID
115+
116+ from eventsourcing.dcb.application import DCBApplication
117+ from eventsourcing.dcb.domain import Perspective, Tagged
118+ from eventsourcing.dcb.msgpack import Decision, InitialDecision, MessagePackMapper
119+ from eventsourcing.projection import DCBApplicationSubscription
120+ from eventsourcing.utils import get_topic
121+
122+
123+ # Define a perspective.
124+ class MyPerspective (Perspective[Decision]):
125+ @ property
126+ def cb (self ) -> Selector | Sequence[Selector]:
127+ return []
128+
129+
130+ # Construct an application object.
131+ app = DCBApplication(env = {" MAPPER_TOPIC" : get_topic(MessagePackMapper)})
132+
133+ # Record an event.
134+ perspective = MyPerspective()
135+ perspective.append_new_decision(
136+ Tagged([], InitialDecision(originator_topic = " " ))
137+ )
138+ app.repository.save(perspective)
139+
140+ # Position in application sequence from which to subscribe.
141+ max_tracking_id = 0
142+
143+ with DCBApplicationSubscription(app, gt = max_tracking_id, topics = ()) as subscription:
144+ for tagged_event, tracking in subscription:
145+ # Process the event and record new state with tracking information.
146+ subscription.stop() # ...so we can continue with the examples
147+
106148
107149 .. _Projection :
108150
@@ -150,7 +192,7 @@ The example below shows how a projection can be defined.
150192 from eventsourcing.projection import Projection
151193 from eventsourcing.utils import get_topic
152194
153- class MyProjection (Projection[" MyMaterialisedViewInterface" ]):
195+ class AggregateEventProjection (Projection[" MyMaterialisedViewInterface" ]):
154196 name = " myprojection"
155197 topics = (get_topic(Aggregate.Event), )
156198
@@ -163,6 +205,35 @@ The example below shows how a projection can be defined.
163205 self .view.my_command(tracking)
164206
165207
208+ For projections that work with :ref: `DCB applications <DCB application >`, you will need to define the dispatching
209+ to work with :ref: `tagged decisions <DCB tagged >`. That is, because the ``process_event() `` method will receive
210+ :class: `~eventsourcing.dcb.domain.Tagged ` objects, and because `singledispatchmethod ` dispatches on the type of
211+ the first argument, you will need to forward ``tagged.decision `` and define handlers for different
212+ types of :class: `~eventsourcing.dcb.domain.Decision `.
213+
214+ .. code-block :: python
215+
216+ from eventsourcing.dcb.domain import Tagged
217+ from eventsourcing.dcb.msgpack import Decision, InitialDecision
218+
219+
220+ class TaggedDecisionProjection (Projection[" MyMaterialisedViewInterface" ]):
221+ name = " myprojection"
222+ topics = (get_topic(Decision), )
223+
224+ @singledispatchmethod
225+ def process_event (self , tagged : Tagged[Decision], tracking : Tracking) -> None :
226+ self .process_decision(tagged.decision, tracking)
227+
228+ @singledispatchmethod
229+ def process_decision (self , _ : Decision, tracking : Tracking) -> None :
230+ pass
231+
232+ @process_decision.register
233+ def process_initial_decision (self , _ : InitialDecision, tracking : Tracking) -> None :
234+ self .view.my_command(tracking)
235+
236+
166237 The example below indicates how the projection's materialised view can be defined.
167238
168239.. code-block :: python
@@ -194,6 +265,7 @@ The example below indicates how the projection's materialised view can be define
194265 def my_command (self , tracking : Tracking) -> None :
195266 ...
196267
268+
197269 .. _Projection runner :
198270
199271Projection runner
@@ -249,7 +321,7 @@ signal after 1s.
249321 with ProjectionRunner(
250322 application_class = Application,
251323 view_class = MyPOPOMaterialisedView,
252- projection_class = MyProjection ,
324+ projection_class = AggregateEventProjection ,
253325 env = {},
254326 ) as projection_runner:
255327
@@ -271,6 +343,37 @@ returned from calls to the event-sourced application's :func:`~eventsourcing.app
271343method can be used by the user interface to :func: `~eventsourcing.persistence.TrackingRecorder.wait ` until
272344the "read model" has been updated.
273345
346+ The projection runner supports :ref: `DCB application classes <DCB application >`.
347+
348+ .. code-block :: python
349+
350+ import os, signal, threading, time
351+
352+ from eventsourcing.projection import ProjectionRunner
353+
354+ # For demonstration purposes, interrupt process with SIGINT after 1s.
355+ def sleep_then_kill () -> None :
356+ time.sleep(1 )
357+ os.kill(os.getpid(), signal.SIGINT )
358+
359+ threading.Thread(target = sleep_then_kill).start()
360+
361+ # Run projection as a context manager.
362+ with ProjectionRunner(
363+ application_class = DCBApplication,
364+ view_class = MyPOPOMaterialisedView,
365+ projection_class = TaggedDecisionProjection,
366+ env = {" MAPPER_TOPIC" : get_topic(MessagePackMapper)},
367+ ) as projection_runner:
368+
369+ # Register signal handler.
370+ signal.signal(signal.SIGINT , lambda * args : projection_runner.stop())
371+
372+ # Run until interrupted.
373+ projection_runner.run_forever()
374+
375+
376+
274377 See :doc: `Tutorial - Part 4 </topics/tutorial/part4 >` for more guidance and examples.
275378
276379
@@ -302,6 +405,8 @@ to increment a ``Counter`` aggregate.
302405.. literalinclude :: ../../tests/projection_tests/test_event_sourced_projection.py
303406 :pyobject: Counter
304407
408+ This library does does not yet support projections event-soured with :ref: `DCB applications <DCB application >`.
409+
305410
306411.. _Event-sourced projection runner :
307412
@@ -363,6 +468,8 @@ currently support application subscriptions.
363468 assert runner.projection.get_count(Aggregate.Event) == 1
364469
365470
471+ The event-sourced projection runner does not yet support projections event-soured with :ref: `DCB applications <DCB application >`.
472+
366473Code reference
367474==============
368475
0 commit comments