py-mediatR 6.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ayhan Öztemel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,547 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-mediatR
3
+ Version: 6.7.0
4
+ Summary: .NET MediatR-inspired high-performance CQRS/Mediator for Python: async pipeline, notifications, streaming, DI (singleton/scoped/transient), CancellationToken, auto-discovery, free-threaded (no-GIL) ready.
5
+ Author-email: Ayhan Öztemel <ayhanoztemel@hotmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Ayhan Öztemel
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/AyhanOztemel/py_mediatR
29
+ Project-URL: Source, https://github.com/AyhanOztemel/py_mediatR
30
+ Project-URL: Issues, https://github.com/AyhanOztemel/py_mediatR/issues
31
+ Project-URL: Changelog, https://github.com/AyhanOztemel/py_mediatR/blob/main/README.md
32
+ Keywords: mediatr,cqrs,mediator,pipeline,di,dependency-injection,cancellation,nogil,free-threaded
33
+ Classifier: Development Status :: 5 - Production/Stable
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Programming Language :: Python :: 3.14
43
+ Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
44
+ Classifier: Programming Language :: Python :: Implementation :: CPython
45
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
46
+ Classifier: Typing :: Typed
47
+ Requires-Python: >=3.10
48
+ Description-Content-Type: text/markdown
49
+ License-File: LICENSE
50
+ Provides-Extra: pydantic
51
+ Requires-Dist: pydantic>=1.10; extra == "pydantic"
52
+ Provides-Extra: test
53
+ Requires-Dist: pytest>=7; extra == "test"
54
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
55
+ Provides-Extra: dev
56
+ Requires-Dist: pytest>=7; extra == "dev"
57
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
58
+ Requires-Dist: build; extra == "dev"
59
+ Requires-Dist: twine; extra == "dev"
60
+ Dynamic: license-file
61
+
62
+ # py_mediatR
63
+
64
+ > Türkçe sürüm: [README.tr.md](https://github.com/AyhanOztemel/py_mediatR/blob/main/README.tr.md)
65
+
66
+ High-performance **CQRS / Mediator** implementation for Python, inspired by
67
+ [.NET MediatR](https://github.com/jbogard/MediatR) (partial semantic parity,
68
+ plus extras like built-in caching/retry/transaction behaviors) — zero
69
+ dependencies, free-threaded (no-GIL) ready.
70
+
71
+ **v6.7:** call-chain tracing (`mediator.trace()`), no more silent failures in
72
+ transaction cleanup or the sync-over-async bridge, captive dependencies
73
+ rejected, `Optional[T]` autowiring, `py.typed` shipped. See
74
+ [What's new in 6.7](#whats-new-in-67).
75
+
76
+ **v6.4:** single-event-loop async pipeline, notification errors propagate by
77
+ default, explicit `ExceptionHandlerState`, typed `send() -> TResponse`,
78
+ JSON discovery cache (no pickle), real LRU caching, thread-safe registries.
79
+
80
+ ## What's new in 6.7
81
+
82
+ Nothing was removed and no signature became stricter — 6.7 is a drop-in
83
+ replacement for 6.6.
84
+
85
+ ### See the call chain
86
+
87
+ A mediator hides who calls what, which makes "why didn't my handler run?" hard
88
+ to answer. `trace()` records one dispatch as a tree:
89
+
90
+ ```python
91
+ with mediator.trace() as flow:
92
+ mediator.send(SearchProducts(keyword="laptop"))
93
+ mediator.send(SearchProducts(keyword="laptop")) # served from cache
94
+ print(flow.render())
95
+ ```
96
+
97
+ ```
98
+ send(SearchProducts) (0.35 ms)
99
+ └─ behavior: LoggingBehavior (0.29 ms)
100
+ └─ behavior: ValidationBehavior (0.22 ms)
101
+ └─ behavior: CachingBehavior [cache miss] (0.20 ms)
102
+ ├─ pre: AuditPreProcessor
103
+ ├─ HANDLER: SearchProductsHandler (0.07 ms)
104
+ └─ post: MetricsPostProcessor
105
+ send(SearchProducts) (0.14 ms)
106
+ └─ behavior: LoggingBehavior (0.11 ms)
107
+ └─ behavior: ValidationBehavior (0.06 ms)
108
+ └─ behavior: CachingBehavior [CACHE HIT - handler NOT called]
109
+ ```
110
+
111
+ Retries show up as repeated handler nodes, and an exception is spelled out at
112
+ the node that raised it while ancestors are marked `!! (propagated)`. Tracing is
113
+ off by default and costs one `ContextVar` lookup per step when inactive.
114
+ `flow.steps()` and `flow.find(label)` expose the same data for assertions;
115
+ `render(unicode=False)` forces ASCII glyphs.
116
+
117
+ ### Failures are no longer silent
118
+
119
+ - `TransactionBehavior` no longer swallows a failing `rollback()`/`close()`.
120
+ The cleanup error is logged and attached to the original exception via
121
+ `add_note()`, or raised as `TransactionCleanupError` with
122
+ `raise_on_cleanup_failure=True`.
123
+ - Dispatching an async session through the synchronous `send()` from inside a
124
+ running loop is now refused up front with a `TypeError` instead of rolling
125
+ back on the wrong event loop.
126
+ - The sync-over-async bridge joins with a 30s budget
127
+ (`MEDIATR_SYNC_BRIDGE_TIMEOUT`, `<=0` disables) and raises
128
+ `SyncBridgeTimeoutError` rather than freezing the calling thread forever.
129
+ - Fire-and-forget notification tasks are kept strongly referenced, so
130
+ `PARALLEL_NOWAIT` subscribers can no longer be garbage-collected mid-flight.
131
+ - Auto-discovery import errors are logged as warnings on the
132
+ `mediatr.discovery` logger (silence with `MEDIATR_DISCOVERY_WARNINGS=0`).
133
+
134
+ ### DI correctness
135
+
136
+ - A singleton consuming a scoped service is now rejected with
137
+ `DIResolutionError` — .NET's captive-dependency rule.
138
+ - `Optional[T]`, `T | None` and `Annotated[...]` constructor hints are
139
+ unwrapped; an optional dependency that cannot be built is injected as `None`.
140
+
141
+ ### Packaging
142
+
143
+ - `py.typed` ships, so type checkers use the inline annotations.
144
+ - `import py_mediatr` (all lowercase) also works, for case-sensitive
145
+ filesystems where the normalised distribution name misleads.
146
+ - The implementation was split from one 4 600-line module into a package of
147
+ focused modules. Nothing you import changes — see
148
+ [Package layout](#package-layout).
149
+
150
+ ## Features
151
+
152
+ - **Request/Response** — `IRequest[TResponse]` generics, sync `send()` and async `send_async()`
153
+ - **Notifications (pub/sub)** — multiple handlers, ordering, SEQUENTIAL / PARALLEL_WHENALL / PARALLEL_NOWAIT strategies, custom publisher
154
+ - **Pipeline behaviors** — `IPipelineBehavior` middleware + 8 built-ins (Logging, Performance, Validation, Caching, Retry, Transaction, Authorization, Tracing)
155
+ - **Streaming** — `IStreamRequest` + `create_stream()` (`IAsyncEnumerable<T>` equivalent)
156
+ - **Pre/Post processors, exception handlers & actions**
157
+ - **Dependency Injection** — `ServiceContainer` with **singleton / scoped / transient** lifetimes, type-hint based constructor auto-wiring, `ServiceScope` (sync+async), request-scoped mediator, FastAPI bridge
158
+ - **CancellationToken** — `CancellationTokenSource` with `cancel_after`, linked tokens, handler injection (`handle(self, req, cancellation_token=...)`), streaming cancellation
159
+ - **Auto-discovery** — handlers found by scanning the project (with cache), or explicit `@handler` / `@behavior` decorators
160
+ - **Call-chain tracing** — `mediator.trace()` renders behaviors, handler and subscribers as a tree
161
+ - **Free-threaded ready** — safe under Python 3.13t/3.14t (no-GIL)
162
+
163
+ ## Install
164
+
165
+ ```bash
166
+ pip install py-mediatR # core (zero dependencies)
167
+ pip install "py-mediatR[pydantic]" # optional pydantic model coercion
168
+ ```
169
+
170
+ ## Quick start
171
+
172
+ ```python
173
+ from py_mediatR import Mediator, IRequest
174
+
175
+ class Ping(IRequest):
176
+ pass
177
+
178
+ class PingHandler:
179
+ def handle(self, req: Ping) -> str:
180
+ return "pong"
181
+
182
+ mediator = Mediator(auto_discover=False)
183
+ mediator.register_handler(Ping, PingHandler())
184
+ print(mediator.send(Ping())) # pong
185
+ ```
186
+
187
+ ### DI + scoped lifetime (per-request DB session)
188
+
189
+ ```python
190
+ from py_mediatR import Mediator, ServiceContainer, scoped_mediator
191
+
192
+ container = ServiceContainer()
193
+ container.register_singleton(Config)
194
+ container.register_scoped(DbSession) # one per scope/request
195
+ container.register_transient(UserRepository)
196
+
197
+ mediator = Mediator(handler_factory=container)
198
+ with scoped_mediator(mediator, container) as m:
199
+ m.send(CreateUser(name="Ada")) # handlers share the scoped session
200
+ # scope disposed -> session.close()
201
+ ```
202
+
203
+ ### CancellationToken
204
+
205
+ ```python
206
+ from py_mediatR import CancellationTokenSource, OperationCancelledError
207
+
208
+ cts = CancellationTokenSource(cancel_after=2.0) # timeout
209
+ try:
210
+ result = await mediator.send_async(SlowQuery(), cancellation_token=cts.token)
211
+ except OperationCancelledError:
212
+ ...
213
+ ```
214
+
215
+ ### FastAPI
216
+
217
+ ```python
218
+ from py_mediatR import make_fastapi_mediator_dependency
219
+ get_mediator = make_fastapi_mediator_dependency(mediator, container)
220
+
221
+ @app.post("/orders")
222
+ async def create_order(cmd: CreateOrderDto, m=Depends(get_mediator)):
223
+ return await m.send_async(CreateOrder(**cmd.model_dump()))
224
+ ```
225
+
226
+ ## How a handler is found
227
+
228
+ There is **no naming convention**. A class becomes a handler because of the
229
+ *type annotation* on its `handle()` parameter — the class name is irrelevant:
230
+
231
+ ```python
232
+ class GetUser(IRequest):
233
+ user_id: str
234
+
235
+ class ThisNameDoesNotMatter: # not "GetUserHandler" — still found
236
+ def handle(self, req: GetUser): # <- the annotation is the contract
237
+ return {"user_id": req.user_id}
238
+ ```
239
+
240
+ The rules, exactly:
241
+
242
+ | Kind | Detected by |
243
+ |---|---|
244
+ | Request handler | `handle()` has a parameter annotated with an `IRequest` subclass |
245
+ | Stream handler | same, but the annotation is an `IStreamRequest` subclass |
246
+ | Notification handler | `handle()` has a parameter annotated with an `INotification` subclass |
247
+
248
+ Details that bite:
249
+
250
+ - The annotation must be **resolvable** at import time. A wrong or unimportable
251
+ forward reference makes the class invisible rather than raising.
252
+ - Exactly **one** `IRequest` parameter is allowed; two raises `TypeError`.
253
+ - Extra parameters are fine — `cancellation_token` is injected by name.
254
+ - A handler whose `__init__` takes arguments is registered *deferred* and
255
+ resolved through `handler_factory` (your `ServiceContainer`) at dispatch time.
256
+ - One handler per request type; notifications may have many.
257
+
258
+ ### Response coercion — the one place naming matters
259
+
260
+ If a handler returns a `dict`, py_mediatR coerces it into a response class
261
+ found **by name in the same module**: `GetUser` → `GetUserResponse`
262
+ (or `GetUserRequest` → `GetUserResponse`). An explicit `-> GetUserResponse`
263
+ return annotation takes priority. With neither, the `dict` is returned as-is.
264
+
265
+ ### Discovery scope
266
+
267
+ `Mediator()` scans `project_root` recursively for `*.py`, skipping `venv`,
268
+ `.venv`, `env`, `site-packages`, `__pycache__`, `.git`, `node_modules`, `.tox`,
269
+ `.nox`, `.eggs`, `build`, `dist`, `migrations` and the various `.*_cache` dirs.
270
+ Narrow it with `Mediator(scan_paths=[...])`. Import errors are logged as
271
+ warnings on the `mediatr.discovery` logger — they are never silent.
272
+
273
+ Prefer explicitness on large codebases:
274
+
275
+ ```python
276
+ mediator = Mediator(auto_discover=False)
277
+ mediator.register_handler(GetUser, GetUserHandler())
278
+ ```
279
+
280
+ or decorate and let discovery collect them:
281
+
282
+ ```python
283
+ from py_mediatR import handler, behavior
284
+
285
+ @handler
286
+ class GetUserHandler:
287
+ def handle(self, req: GetUser): ...
288
+ ```
289
+
290
+ ## The pipeline
291
+
292
+ Every `send()` runs through the same onion. Behaviors are sorted by `order`,
293
+ **smallest first, outermost** — a low `order` starts first and finishes last:
294
+
295
+ ```
296
+ send(request)
297
+ └─ behavior (order -100) ← outermost
298
+ └─ behavior (order -50)
299
+ ├─ pre-processor ← side effects only
300
+ ├─ HANDLER ← your business logic
301
+ └─ post-processor ← sees the response, cannot change it
302
+ ```
303
+
304
+ On an exception: every matching `IExceptionAction` observes it (without
305
+ swallowing), then the first matching `IExceptionHandler` may replace it with a
306
+ fallback response.
307
+
308
+ Use `mediator.trace()` to print the actual tree for a real request — that is
309
+ the authoritative answer to "which behavior called what".
310
+
311
+ ### Built-in behaviors and their contracts
312
+
313
+ Each built-in reads an attribute off the **request**. If the attribute is
314
+ absent the behavior is a no-op, so a request opts in to exactly what it needs.
315
+
316
+ | Behavior | `order` | Opt-in on the request | Effect |
317
+ |---|---|---|---|
318
+ | `LoggingBehavior` | -100 | — (always) | logs entry/exit |
319
+ | `TracingBehavior` | -95 | — (always) | correlation id per request |
320
+ | `PerformanceBehavior` | -90 | — (always) | warns past a duration threshold |
321
+ | `AuthorizationBehavior` | -85 | `requires_permission` | denies with `UnauthorizedError`; handler never runs |
322
+ | `ValidationBehavior` | -80 | `validate()` method | raises before the handler |
323
+ | `CachingBehavior` | -70 | `cacheable = True` | TTL + LRU; a hit skips the handler entirely |
324
+ | `RetryBehavior` | -60 | — (always) | re-runs on exception, exponential backoff + jitter |
325
+ | `TransactionBehavior` | -50 | `transactional = True` | commit on success, rollback on error |
326
+
327
+ ```python
328
+ from dataclasses import dataclass
329
+ from py_mediatR import IRequest
330
+
331
+ @dataclass
332
+ class PlaceOrder(IRequest):
333
+ sku: str
334
+ qty: int = 1
335
+
336
+ transactional = True # TransactionBehavior engages
337
+ requires_permission = "orders.write" # AuthorizationBehavior checks this
338
+
339
+ def validate(self) -> None: # ValidationBehavior calls this
340
+ if self.qty < 1:
341
+ raise ValueError("qty must be positive")
342
+
343
+ @dataclass
344
+ class SearchProducts(IRequest):
345
+ keyword: str
346
+ cacheable = True # CachingBehavior stores the response
347
+ ```
348
+
349
+ Wiring them up — note that `AuthorizationBehavior` and `TransactionBehavior`
350
+ need a callback, so they are constructed, not just listed:
351
+
352
+ ```python
353
+ from py_mediatR import (
354
+ Mediator, LoggingBehavior, ValidationBehavior, CachingBehavior,
355
+ RetryBehavior, TransactionBehavior, AuthorizationBehavior,
356
+ )
357
+
358
+ mediator = Mediator(behaviors=[
359
+ LoggingBehavior(),
360
+ AuthorizationBehavior(lambda req, perm: current_user.has(perm)),
361
+ ValidationBehavior(),
362
+ CachingBehavior(ttl_seconds=60, max_size=1000),
363
+ RetryBehavior(max_attempts=3, delay=0.1, backoff=2.0, jitter=0.05),
364
+ TransactionBehavior(session_factory=lambda: SessionLocal()),
365
+ ])
366
+ ```
367
+
368
+ ### Writing your own behavior
369
+
370
+ `next_handler()` runs the rest of the chain. Not calling it short-circuits the
371
+ handler entirely — that is exactly how `CachingBehavior` serves a hit:
372
+
373
+ ```python
374
+ from py_mediatR import IPipelineBehavior
375
+
376
+ class AuditBehavior(IPipelineBehavior):
377
+ order = -110 # lower than LoggingBehavior -> runs outermost
378
+ applies_to = PlaceOrder # optional; None (default) means every request
379
+
380
+ def handle(self, request, next_handler):
381
+ audit.write(f"-> {type(request).__name__}")
382
+ response = next_handler()
383
+ audit.write(f"<- {type(request).__name__}")
384
+ return response
385
+ ```
386
+
387
+ `applies_to` narrows a behavior to one request type. In the async pipeline
388
+ `handle()` may be `async def`, and `next_handler()` returns an awaitable.
389
+
390
+ ### Pre/post processors
391
+
392
+ Both are side-effect only and support `order` and `applies_to`. A post
393
+ processor *sees* the response but cannot replace it — return a different value
394
+ from a behavior if you need that.
395
+
396
+ ```python
397
+ from py_mediatR import IRequestPreProcessor, IRequestPostProcessor
398
+
399
+ class AuditPre(IRequestPreProcessor):
400
+ def process(self, request): # may be async def
401
+ audit.write(type(request).__name__)
402
+
403
+ class MetricsPost(IRequestPostProcessor):
404
+ def process(self, request, response): # may be async def
405
+ metrics.increment(type(request).__name__)
406
+ ```
407
+
408
+ ### Exception handlers vs actions
409
+
410
+ | | Purpose | Return value |
411
+ |---|---|---|
412
+ | `IExceptionHandler.handle` | replace the error with a fallback response | returning a value swallows the exception; `raise` propagates |
413
+ | `IExceptionAction.execute` | observe only (log, alert, metric) | ignored — the exception keeps propagating |
414
+
415
+ Both filter on `exception_type` (default `Exception`), `applies_to` and
416
+ `order`. All matching actions run; the **first** matching handler that returns
417
+ a value wins.
418
+
419
+ ```python
420
+ from py_mediatR import IExceptionHandler, IExceptionAction
421
+
422
+ class InvoiceNotFound(IExceptionHandler):
423
+ exception_type = KeyError
424
+ applies_to = FindInvoice
425
+ def handle(self, request, exc):
426
+ return FindInvoiceResponse(invoice_id=request.invoice_id, found=False)
427
+
428
+ class AlertAction(IExceptionAction):
429
+ exception_type = Exception
430
+ def execute(self, request, exc):
431
+ alerting.notify(f"{type(request).__name__} failed: {exc}")
432
+ ```
433
+
434
+ ### Validators (FluentValidation style)
435
+
436
+ Keep validation out of the request when it needs dependencies:
437
+
438
+ ```python
439
+ from py_mediatR import IValidator, ValidationBehavior
440
+
441
+ class CreateUserValidator(IValidator):
442
+ applies_to = CreateUser # or a tuple of types; None = all
443
+ def validate(self, request):
444
+ if "@" not in request.email:
445
+ raise ValueError(f"invalid e-mail: {request.email}")
446
+
447
+ mediator = Mediator(behaviors=[ValidationBehavior(validators=[CreateUserValidator()])])
448
+ ```
449
+
450
+ Matching validators run in `order`, then the request's own `validate()`.
451
+
452
+ ### Notifications
453
+
454
+ Multiple subscribers per event, ordered by `order`. Choose how they run:
455
+
456
+ ```python
457
+ from py_mediatR import PublishStrategy
458
+
459
+ mediator.publish(UserRegistered(user_id="U-1")) # SEQUENTIAL
460
+ await mediator.publish_async(evt, strategy=PublishStrategy.PARALLEL_WHENALL)
461
+ await mediator.publish_async(evt, strategy=PublishStrategy.PARALLEL_NOWAIT)
462
+ ```
463
+
464
+ | Strategy | Semantics |
465
+ |---|---|
466
+ | `SEQUENTIAL` (default) | one after another, in `order`; first error propagates |
467
+ | `PARALLEL_WHENALL` | concurrent, waits for all |
468
+ | `PARALLEL_NOWAIT` | fire-and-forget; tasks are strongly referenced so they cannot be GC'd |
469
+
470
+ Subscriber errors propagate by default. Pass
471
+ `Mediator(swallow_notification_errors=True)` for the older forgiving behavior.
472
+ With `polymorphic_publish=True`, publishing a subclass also triggers base-type
473
+ subscribers (.NET covariance).
474
+
475
+ ### Streaming
476
+
477
+ `create_stream()` yields items as they are produced, and
478
+ `IStreamPipelineBehavior` wraps the generator:
479
+
480
+ ```python
481
+ from py_mediatR import IStreamRequest, IStreamPipelineBehavior
482
+
483
+ class StreamOrders(IStreamRequest):
484
+ count: int = 100
485
+
486
+ class StreamOrdersHandler:
487
+ async def handle(self, req: StreamOrders):
488
+ for i in range(req.count):
489
+ yield {"seq": i}
490
+
491
+ class StreamAudit(IStreamPipelineBehavior):
492
+ async def handle(self, request, next_handler):
493
+ async for item in next_handler():
494
+ yield item
495
+
496
+ async for order in mediator.create_stream(StreamOrders(count=10)):
497
+ ...
498
+ ```
499
+
500
+ ## Examples
501
+
502
+ Four runnable apps sharing one layered core live in
503
+ [`examples/`](https://github.com/AyhanOztemel/py_mediatR/blob/main/examples/README.md)
504
+ — console, FastAPI, Flask and Django, each with its own README:
505
+
506
+ - [`examples/console_app`](https://github.com/AyhanOztemel/py_mediatR/blob/main/examples/console_app/README.md)
507
+ — start here: ten steps, each printing its call-chain tree
508
+ - [`examples/fastapi_app`](https://github.com/AyhanOztemel/py_mediatR/blob/main/examples/fastapi_app/README.md)
509
+ — async routes, one DI scope per request
510
+ - [`examples/flask_app`](https://github.com/AyhanOztemel/py_mediatR/blob/main/examples/flask_app/README.md)
511
+ — synchronous routes, explicit scopes
512
+ - [`examples/django_app`](https://github.com/AyhanOztemel/py_mediatR/blob/main/examples/django_app/README.md)
513
+ — views that do not know their handlers
514
+
515
+ The examples are not shipped in the wheel; they live in the repository.
516
+
517
+ ## Package layout
518
+
519
+ Import from the top-level package. That is the only supported surface:
520
+
521
+ ```python
522
+ from py_mediatR import Mediator, IRequest, IPipelineBehavior
523
+ ```
524
+
525
+ Every name in `__all__` is re-exported there, so the layout below is an
526
+ implementation detail — it is documented to help you read the source, not to
527
+ be imported from.
528
+
529
+ | Module | Holds |
530
+ |---|---|
531
+ | `contracts.py` | `IRequest`, `IResponse`, `INotification`, the `I*` interfaces, `PublishStrategy`, `@handler` / `@behavior` |
532
+ | `mediator.py` | `Mediator`, `ISender` / `IPublisher`, pipeline compilation, `send` / `publish` / `create_stream` |
533
+ | `di.py` | `ServiceContainer`, `ServiceScope`, `scoped_mediator`, `make_fastapi_mediator_dependency` |
534
+ | `discovery.py` | project scanning, type-hint based handler resolution, `discover_handlers` |
535
+ | `behaviors.py` | the eight built-in behaviors |
536
+ | `cancellation.py` | `CancellationToken`, `CancellationTokenSource`, `current_cancellation_token` |
537
+ | `tracing.py` | `FlowNode`, `FlowTrace`, `trace_flow` — what `mediator.trace()` renders |
538
+ | `coercion.py` | dict → dataclass/pydantic coercion, the sync-over-async bridge |
539
+ | `_config.py`, `_typechecks.py` | flags, sentinels, internal type predicates |
540
+
541
+ `py_mediatR.py_mediatR` remains importable and still exposes every name it did
542
+ before the split, so code written against the single-module layout keeps
543
+ working unchanged.
544
+
545
+ ## License
546
+
547
+ MIT — Ayhan Öztemel