flowevent 1.2__tar.gz → 1.2.1__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.
- {flowevent-1.2 → flowevent-1.2.1}/CHANGELOG.md +20 -0
- {flowevent-1.2/flowevent.egg-info → flowevent-1.2.1}/PKG-INFO +14 -12
- {flowevent-1.2 → flowevent-1.2.1}/README-TR.md +11 -10
- {flowevent-1.2 → flowevent-1.2.1}/README.md +13 -11
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/__init__.py +1 -1
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/async_event_bus.py +51 -31
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/base.py +26 -25
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/contracts.py +2 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/engine.py +5 -1
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/event_bus.py +68 -40
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/stream.py +3 -2
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/subscription.py +18 -3
- {flowevent-1.2 → flowevent-1.2.1/flowevent.egg-info}/PKG-INFO +14 -12
- {flowevent-1.2 → flowevent-1.2.1}/pyproject.toml +1 -1
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_bus.py +136 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_engine.py +36 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_public_contracts.py +2 -2
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_scopes.py +85 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_stream.py +11 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_subscription.py +20 -7
- {flowevent-1.2 → flowevent-1.2.1}/LICENSE +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/MANIFEST.in +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/__init__.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/_measure.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/benchmark_compare.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/benchmark_pipeline.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/benchmark_publish.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/benchmark_replay.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/benchmark_runtime.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/benchmark_stream.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/README.md +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/callback_errors.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/extended_publish.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/interleaved_publish.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/paired_baseline.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/paired_extended.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/pipeline.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/pipeline_diagnostics.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/process_peak_rss.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/replay_after.txt +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/replay_before.txt +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/runtime_load.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/stream.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/benchmarks/results/v1_1_publish.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/_callbacks.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/_lifecycle.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/actions.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/callback.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/compatibility.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/context.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/errors.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/filtering.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/history.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/observability.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/pipeline/__init__.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/pipeline/async_.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/pipeline/base.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/pipeline/contracts.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/pipeline/registry.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/pipeline/sync.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/py.typed +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/eventflow/scopes.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/examples/__init__.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/examples/bus_stream.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/examples/integrated.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/flowevent.egg-info/SOURCES.txt +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/flowevent.egg-info/dependency_links.txt +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/flowevent.egg-info/requires.txt +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/flowevent.egg-info/top_level.txt +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/setup.cfg +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/api_v1_1.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/api_v1_2.json +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_benchmarks.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_callback.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_context.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_delivery_limits.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_filtering.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_integration.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_lifecycle.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_middleware_limits.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_observability.py +0 -0
- {flowevent-1.2 → flowevent-1.2.1}/tests/test_pipeline.py +0 -0
|
@@ -4,6 +4,26 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [v1.2.1] - 2026-11-02
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Isolate sync and async subscriber matching in focused bus helpers while
|
|
12
|
+
keeping their cancellation and replay behavior separate.
|
|
13
|
+
- Share finite timeout validation and replay-history window selection across
|
|
14
|
+
both buses without merging their independent delivery paths.
|
|
15
|
+
- Count a publication only after a configured history append succeeds, and
|
|
16
|
+
reject non-string engine channel names instead of coercing them.
|
|
17
|
+
- Skip repeated event-loop lookups on stream reads after the owning consumer
|
|
18
|
+
task has been established.
|
|
19
|
+
- Run Twine metadata checks in strict mode in CI.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Claim engine-created channels for their owning engine, preserve finite
|
|
24
|
+
subscription deliveries when cancellation or executor submission fails, and
|
|
25
|
+
coordinate concurrent synchronous subscription cancellation retries.
|
|
26
|
+
|
|
7
27
|
## [v1.2] - 2026-11-02
|
|
8
28
|
|
|
9
29
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowevent
|
|
3
|
-
Version: 1.2
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: EventFlow: synchronous and asynchronous event delivery for Python.
|
|
5
5
|
Author-email: TahsinCr <Tahsincr@outlook.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,7 +30,7 @@ Requires-Dist: pyright<2,>=1.1.400; extra == "dev"
|
|
|
30
30
|
Requires-Dist: ruff<1,>=0.12; extra == "dev"
|
|
31
31
|
Dynamic: license-file
|
|
32
32
|
|
|
33
|
-
[![Version: v1.2][version-shield]](https://github.com/TahsinCr/python-eventflow/blob/main/CHANGELOG.md)
|
|
33
|
+
[![Version: v1.2.1][version-shield]](https://github.com/TahsinCr/python-eventflow/blob/main/CHANGELOG.md)
|
|
34
34
|
[![Python 3.10+][python-shield]](https://www.python.org/downloads/)
|
|
35
35
|
[![MIT License][license-shield]](https://github.com/TahsinCr/python-eventflow/blob/main/LICENSE)
|
|
36
36
|
|
|
@@ -146,7 +146,7 @@ The installed package exposes its version through `eventflow.__version__`:
|
|
|
146
146
|
~~~python
|
|
147
147
|
from eventflow import __version__
|
|
148
148
|
|
|
149
|
-
print(__version__) # 1.2
|
|
149
|
+
print(__version__) # 1.2.1
|
|
150
150
|
~~~
|
|
151
151
|
|
|
152
152
|
<a id="first-event"></a>
|
|
@@ -327,10 +327,11 @@ directly. The bus attempts delivery to the selected handlers, then raises an
|
|
|
327
327
|
a `PublishResult`. The `bus.stats` property provides cumulative
|
|
328
328
|
`published`, `delivered`, and `failed` counts.
|
|
329
329
|
|
|
330
|
-
These counters are not a per-publish invariant. `published`
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
330
|
+
These counters are not a per-publish invariant. `published` advances after
|
|
331
|
+
history recording succeeds (or is skipped when history is disabled) and before
|
|
332
|
+
callbacks run; `delivered` and `failed` are finalized afterward. An in-flight
|
|
333
|
+
or cancelled async publish can leave them different. `failed` counts collected
|
|
334
|
+
errors from handlers, filters, and actions, not only failed handlers.
|
|
334
335
|
|
|
335
336
|
To catch grouped failures with the same import on Python 3.10 and newer, import
|
|
336
337
|
`ExceptionGroup` from `eventflow.compatibility`. On Python 3.11 and newer, that
|
|
@@ -607,10 +608,11 @@ with EventEngine(history=50) as engine:
|
|
|
607
608
|
print(result.delivered)
|
|
608
609
|
~~~
|
|
609
610
|
|
|
610
|
-
Channel names are stripped and lowercased, so `" Orders "`
|
|
611
|
-
refer to the same channel. `engine.channel(name)` returns a
|
|
612
|
-
it when necessary. Use `engine.add(name, bus)` to register a
|
|
613
|
-
settings. `remove(name)` closes one channel; `stop()` closes
|
|
611
|
+
Channel names must be strings and are stripped and lowercased, so `" Orders "`
|
|
612
|
+
and `"orders"` refer to the same channel. `engine.channel(name)` returns a
|
|
613
|
+
channel, creating it when necessary. Use `engine.add(name, bus)` to register a
|
|
614
|
+
bus with custom settings. `remove(name)` closes one channel; `stop()` closes
|
|
615
|
+
every channel.
|
|
614
616
|
An engine can own a bus instance under only one channel name at a time. A
|
|
615
617
|
successful `add()` claims that bus for the engine; another engine cannot claim
|
|
616
618
|
the same instance and receives `ValueError`. Removing or stopping the owner
|
|
@@ -1166,6 +1168,6 @@ Copyright © 2026 TahsinCr. EventFlow is licensed under the MIT License.
|
|
|
1166
1168
|
See [LICENSE](https://github.com/TahsinCr/python-eventflow/blob/main/LICENSE) and
|
|
1167
1169
|
[CHANGELOG](https://github.com/TahsinCr/python-eventflow/blob/main/CHANGELOG.md).
|
|
1168
1170
|
|
|
1169
|
-
[version-shield]: https://img.shields.io/badge/version-v1.2-b7a3e3.svg?style=for-the-badge
|
|
1171
|
+
[version-shield]: https://img.shields.io/badge/version-v1.2.1-b7a3e3.svg?style=for-the-badge
|
|
1170
1172
|
[python-shield]: https://img.shields.io/badge/Python-3.10%2B-3776AB.svg?style=for-the-badge&logo=python&logoColor=white
|
|
1171
1173
|
[license-shield]: https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[![Sürüm: v1.2][version-shield]](https://github.com/TahsinCr/python-eventflow/blob/main/CHANGELOG.md)
|
|
1
|
+
[![Sürüm: v1.2.1][version-shield]](https://github.com/TahsinCr/python-eventflow/blob/main/CHANGELOG.md)
|
|
2
2
|
[![Python 3.10+][python-shield]](https://www.python.org/downloads/)
|
|
3
3
|
[![MIT Lisansı][license-shield]](https://github.com/TahsinCr/python-eventflow/blob/main/LICENSE)
|
|
4
4
|
|
|
@@ -118,7 +118,7 @@ Kurulu paketin sürüm numarasına `eventflow.__version__` üzerinden erişebili
|
|
|
118
118
|
~~~python
|
|
119
119
|
from eventflow import __version__
|
|
120
120
|
|
|
121
|
-
print(__version__) # 1.2
|
|
121
|
+
print(__version__) # 1.2.1
|
|
122
122
|
~~~
|
|
123
123
|
|
|
124
124
|
<a id="first-event"></a>
|
|
@@ -304,11 +304,12 @@ denedikten sonra toplanan hatalar için `ExceptionGroup` yükseltir; o yayın
|
|
|
304
304
|
çağrısı `PublishResult` döndürmez. `bus.stats` toplam `published`,
|
|
305
305
|
`delivered` ve `failed` sayaçlarını verir.
|
|
306
306
|
|
|
307
|
-
Bu sayaçlar her yayın için değişmez bir eşitlik oluşturmaz. `published
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
307
|
+
Bu sayaçlar her yayın için değişmez bir eşitlik oluşturmaz. `published`, history
|
|
308
|
+
kaydı başarılı olduktan (history kapalıysa bu adım atlandıktan) sonra ve
|
|
309
|
+
callback'ler çalışmadan önce artar; `delivered` ve `failed` işlem sonunda
|
|
310
|
+
güncellenir. Devam eden veya iptal edilen async yayınlarda değerler farklı
|
|
311
|
+
kalabilir. `failed`, yalnızca başarısız handler'ları değil; handler, filtre ve
|
|
312
|
+
action hatası kayıtlarını sayar.
|
|
312
313
|
|
|
313
314
|
Python 3.10 ve üzerindeki sürümlerde aynı import ile gruplandırılmış hataları
|
|
314
315
|
yakalamak için `ExceptionGroup` sınıfını `eventflow.compatibility` modülünden
|
|
@@ -590,8 +591,8 @@ with EventEngine(history=50) as engine:
|
|
|
590
591
|
print(result.delivered)
|
|
591
592
|
~~~
|
|
592
593
|
|
|
593
|
-
Kanal
|
|
594
|
-
Bu nedenle `" Orders "` ve `"orders"` aynı kanala gider.
|
|
594
|
+
Kanal adları string olmalı; başındaki ve sonundaki boşluklar silinir, harfler
|
|
595
|
+
küçültülür. Bu nedenle `" Orders "` ve `"orders"` aynı kanala gider.
|
|
595
596
|
`engine.channel(name)` kanalı döndürür veya oluşturur.
|
|
596
597
|
`engine.add(name, bus)` özel ayarlı bus kaydeder. `remove(name)` bir kanalı
|
|
597
598
|
kapatıp kaldırır; `stop()` tüm kanalları kapatır. Engine ayrıca `running` ve
|
|
@@ -1160,6 +1161,6 @@ Copyright © 2026 TahsinCr. EventFlow, MIT Lisansı ile lisanslanmıştır.
|
|
|
1160
1161
|
[CHANGELOG](https://github.com/TahsinCr/python-eventflow/blob/main/CHANGELOG.md)
|
|
1161
1162
|
dosyalarına bakın.
|
|
1162
1163
|
|
|
1163
|
-
[version-shield]: https://img.shields.io/badge/version-v1.2-b7a3e3.svg?style=for-the-badge
|
|
1164
|
+
[version-shield]: https://img.shields.io/badge/version-v1.2.1-b7a3e3.svg?style=for-the-badge
|
|
1164
1165
|
[python-shield]: https://img.shields.io/badge/Python-3.10%2B-3776AB.svg?style=for-the-badge&logo=python&logoColor=white
|
|
1165
1166
|
[license-shield]: https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[![Version: v1.2][version-shield]](https://github.com/TahsinCr/python-eventflow/blob/main/CHANGELOG.md)
|
|
1
|
+
[![Version: v1.2.1][version-shield]](https://github.com/TahsinCr/python-eventflow/blob/main/CHANGELOG.md)
|
|
2
2
|
[![Python 3.10+][python-shield]](https://www.python.org/downloads/)
|
|
3
3
|
[![MIT License][license-shield]](https://github.com/TahsinCr/python-eventflow/blob/main/LICENSE)
|
|
4
4
|
|
|
@@ -114,7 +114,7 @@ The installed package exposes its version through `eventflow.__version__`:
|
|
|
114
114
|
~~~python
|
|
115
115
|
from eventflow import __version__
|
|
116
116
|
|
|
117
|
-
print(__version__) # 1.2
|
|
117
|
+
print(__version__) # 1.2.1
|
|
118
118
|
~~~
|
|
119
119
|
|
|
120
120
|
<a id="first-event"></a>
|
|
@@ -295,10 +295,11 @@ directly. The bus attempts delivery to the selected handlers, then raises an
|
|
|
295
295
|
a `PublishResult`. The `bus.stats` property provides cumulative
|
|
296
296
|
`published`, `delivered`, and `failed` counts.
|
|
297
297
|
|
|
298
|
-
These counters are not a per-publish invariant. `published`
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
298
|
+
These counters are not a per-publish invariant. `published` advances after
|
|
299
|
+
history recording succeeds (or is skipped when history is disabled) and before
|
|
300
|
+
callbacks run; `delivered` and `failed` are finalized afterward. An in-flight
|
|
301
|
+
or cancelled async publish can leave them different. `failed` counts collected
|
|
302
|
+
errors from handlers, filters, and actions, not only failed handlers.
|
|
302
303
|
|
|
303
304
|
To catch grouped failures with the same import on Python 3.10 and newer, import
|
|
304
305
|
`ExceptionGroup` from `eventflow.compatibility`. On Python 3.11 and newer, that
|
|
@@ -575,10 +576,11 @@ with EventEngine(history=50) as engine:
|
|
|
575
576
|
print(result.delivered)
|
|
576
577
|
~~~
|
|
577
578
|
|
|
578
|
-
Channel names are stripped and lowercased, so `" Orders "`
|
|
579
|
-
refer to the same channel. `engine.channel(name)` returns a
|
|
580
|
-
it when necessary. Use `engine.add(name, bus)` to register a
|
|
581
|
-
settings. `remove(name)` closes one channel; `stop()` closes
|
|
579
|
+
Channel names must be strings and are stripped and lowercased, so `" Orders "`
|
|
580
|
+
and `"orders"` refer to the same channel. `engine.channel(name)` returns a
|
|
581
|
+
channel, creating it when necessary. Use `engine.add(name, bus)` to register a
|
|
582
|
+
bus with custom settings. `remove(name)` closes one channel; `stop()` closes
|
|
583
|
+
every channel.
|
|
582
584
|
An engine can own a bus instance under only one channel name at a time. A
|
|
583
585
|
successful `add()` claims that bus for the engine; another engine cannot claim
|
|
584
586
|
the same instance and receives `ValueError`. Removing or stopping the owner
|
|
@@ -1134,6 +1136,6 @@ Copyright © 2026 TahsinCr. EventFlow is licensed under the MIT License.
|
|
|
1134
1136
|
See [LICENSE](https://github.com/TahsinCr/python-eventflow/blob/main/LICENSE) and
|
|
1135
1137
|
[CHANGELOG](https://github.com/TahsinCr/python-eventflow/blob/main/CHANGELOG.md).
|
|
1136
1138
|
|
|
1137
|
-
[version-shield]: https://img.shields.io/badge/version-v1.2-b7a3e3.svg?style=for-the-badge
|
|
1139
|
+
[version-shield]: https://img.shields.io/badge/version-v1.2.1-b7a3e3.svg?style=for-the-badge
|
|
1138
1140
|
[python-shield]: https://img.shields.io/badge/Python-3.10%2B-3776AB.svg?style=for-the-badge&logo=python&logoColor=white
|
|
1139
1141
|
[license-shield]: https://img.shields.io/badge/license-MIT-blue.svg?style=for-the-badge
|
|
@@ -14,9 +14,8 @@ from .base import (
|
|
|
14
14
|
BaseEventBus,
|
|
15
15
|
normalize_replay_capacity,
|
|
16
16
|
validate_nonnegative_count,
|
|
17
|
-
validate_nonnegative_timeout,
|
|
18
17
|
validate_positive_capacity,
|
|
19
|
-
|
|
18
|
+
_validate_timeout,
|
|
20
19
|
)
|
|
21
20
|
from .contracts import DeliveryMode, ErrorPolicy, EventBusStats, PublishResult
|
|
22
21
|
from .compatibility import ExceptionGroup
|
|
@@ -51,6 +50,10 @@ class _AsyncSubscriber(Generic[T]):
|
|
|
51
50
|
self.remaining -= 1
|
|
52
51
|
return True, self.remaining == 0
|
|
53
52
|
|
|
53
|
+
def restore_claim(self) -> None:
|
|
54
|
+
if self.remaining is not None:
|
|
55
|
+
self.remaining += 1
|
|
56
|
+
|
|
54
57
|
def buffer_if_replaying(self, event: T) -> bool:
|
|
55
58
|
if not self.replaying:
|
|
56
59
|
return False
|
|
@@ -178,13 +181,11 @@ class AsyncEventBus(BaseEventBus[T]):
|
|
|
178
181
|
async with self._lock:
|
|
179
182
|
self._ensure_open()
|
|
180
183
|
history = self._history
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
else:
|
|
187
|
-
replay_snapshot = ()
|
|
184
|
+
replay_snapshot = self._query_replay(
|
|
185
|
+
history,
|
|
186
|
+
replay,
|
|
187
|
+
any(option is not None for option in (event_filter, event_type, predicate)),
|
|
188
|
+
)
|
|
188
189
|
subscription, subscriber = self._subscribe_locked(
|
|
189
190
|
callback,
|
|
190
191
|
normalized_filter,
|
|
@@ -278,33 +279,18 @@ class AsyncEventBus(BaseEventBus[T]):
|
|
|
278
279
|
async with self._lock:
|
|
279
280
|
self._ensure_open()
|
|
280
281
|
subscribers = tuple(self._subscribers.values())
|
|
281
|
-
self._published += 1
|
|
282
282
|
self._record(event)
|
|
283
|
+
self._published += 1
|
|
283
284
|
|
|
284
|
-
matched: list[_AsyncSubscriber[T]] = []
|
|
285
285
|
errors: list[Exception] = []
|
|
286
286
|
if self._actions.on_before is not None:
|
|
287
287
|
try:
|
|
288
288
|
await self._actions.on_before(event)
|
|
289
289
|
except Exception as exc:
|
|
290
290
|
errors.append(exc)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
try:
|
|
295
|
-
matches = subscriber.event_filter.matches(event)
|
|
296
|
-
except Exception as exc:
|
|
297
|
-
errors.append(exc)
|
|
298
|
-
continue
|
|
299
|
-
if matches:
|
|
300
|
-
try:
|
|
301
|
-
if subscriber.buffer_if_replaying(event):
|
|
302
|
-
continue
|
|
303
|
-
except BufferError as exc:
|
|
304
|
-
await subscriber.subscription.cancel()
|
|
305
|
-
errors.append(exc)
|
|
306
|
-
continue
|
|
307
|
-
matched.append(subscriber)
|
|
291
|
+
|
|
292
|
+
matched, matching_errors = await self._match_subscribers(subscribers, event)
|
|
293
|
+
errors.extend(matching_errors)
|
|
308
294
|
|
|
309
295
|
delivered = 0
|
|
310
296
|
if self._delivery_mode is DeliveryMode.SEQUENTIAL:
|
|
@@ -353,6 +339,32 @@ class AsyncEventBus(BaseEventBus[T]):
|
|
|
353
339
|
raise ExceptionGroup("Async event delivery failed", errors)
|
|
354
340
|
return result
|
|
355
341
|
|
|
342
|
+
async def _match_subscribers(
|
|
343
|
+
self,
|
|
344
|
+
subscribers: tuple[_AsyncSubscriber[T], ...],
|
|
345
|
+
event: T,
|
|
346
|
+
) -> tuple[list[_AsyncSubscriber[T]], list[Exception]]:
|
|
347
|
+
matched: list[_AsyncSubscriber[T]] = []
|
|
348
|
+
errors: list[Exception] = []
|
|
349
|
+
for subscriber in subscribers:
|
|
350
|
+
if not subscriber.subscription.active:
|
|
351
|
+
continue
|
|
352
|
+
try:
|
|
353
|
+
matches = subscriber.event_filter.matches(event)
|
|
354
|
+
except Exception as exc:
|
|
355
|
+
errors.append(exc)
|
|
356
|
+
continue
|
|
357
|
+
if matches:
|
|
358
|
+
try:
|
|
359
|
+
if subscriber.buffer_if_replaying(event):
|
|
360
|
+
continue
|
|
361
|
+
except BufferError as exc:
|
|
362
|
+
await subscriber.subscription.cancel()
|
|
363
|
+
errors.append(exc)
|
|
364
|
+
continue
|
|
365
|
+
matched.append(subscriber)
|
|
366
|
+
return matched, errors
|
|
367
|
+
|
|
356
368
|
@staticmethod
|
|
357
369
|
def _collect_concurrent_outcomes(
|
|
358
370
|
outcomes: list[tuple[bool, Exception | None] | BaseException],
|
|
@@ -452,7 +464,11 @@ class AsyncEventBus(BaseEventBus[T]):
|
|
|
452
464
|
if not claimed:
|
|
453
465
|
return False, None
|
|
454
466
|
if final:
|
|
455
|
-
|
|
467
|
+
try:
|
|
468
|
+
await subscriber.subscription._consume()
|
|
469
|
+
except BaseException:
|
|
470
|
+
subscriber.restore_claim()
|
|
471
|
+
raise
|
|
456
472
|
try:
|
|
457
473
|
await subscriber.callback(event)
|
|
458
474
|
except Exception as exc:
|
|
@@ -470,7 +486,7 @@ class AsyncEventBus(BaseEventBus[T]):
|
|
|
470
486
|
"""Publish the same event periodically until cancelled or completed."""
|
|
471
487
|
|
|
472
488
|
self._bind_loop()
|
|
473
|
-
interval =
|
|
489
|
+
interval = _validate_timeout(interval, name="Event publish interval")
|
|
474
490
|
if times is not None and (
|
|
475
491
|
isinstance(times, bool) or not isinstance(times, int) or times <= 0
|
|
476
492
|
):
|
|
@@ -536,7 +552,11 @@ class AsyncEventBus(BaseEventBus[T]):
|
|
|
536
552
|
) -> T | None:
|
|
537
553
|
loop = self._bind_loop()
|
|
538
554
|
if timeout is not None:
|
|
539
|
-
timeout =
|
|
555
|
+
timeout = _validate_timeout(
|
|
556
|
+
timeout,
|
|
557
|
+
name="Event wait timeout",
|
|
558
|
+
allow_zero=True,
|
|
559
|
+
)
|
|
540
560
|
normalized_filter = coerce_event_filter(
|
|
541
561
|
event_filter,
|
|
542
562
|
event_type=event_type,
|
|
@@ -33,17 +33,20 @@ def normalize_replay_capacity(value: int) -> int:
|
|
|
33
33
|
return validate_positive_capacity(value, name="Replay buffer limit")
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def
|
|
37
|
-
""
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
def _validate_timeout(value: float, *, name: str, allow_zero: bool = False) -> float:
|
|
37
|
+
qualifier = "non-negative" if allow_zero else "positive"
|
|
38
|
+
message = f"{name} must be {qualifier} and finite"
|
|
39
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
40
|
+
raise ValueError(message)
|
|
41
|
+
try:
|
|
42
|
+
timeout = float(value)
|
|
43
|
+
except OverflowError as exc:
|
|
44
|
+
if allow_zero:
|
|
45
|
+
raise ValueError(message) from exc
|
|
46
|
+
raise ValueError(message) from None
|
|
47
|
+
if not math.isfinite(timeout) or timeout < 0 or (timeout == 0 and not allow_zero):
|
|
48
|
+
raise ValueError(message)
|
|
49
|
+
return timeout
|
|
47
50
|
|
|
48
51
|
|
|
49
52
|
def validate_nonnegative_count(value: int, *, name: str) -> int:
|
|
@@ -54,20 +57,6 @@ def validate_nonnegative_count(value: int, *, name: str) -> int:
|
|
|
54
57
|
return value
|
|
55
58
|
|
|
56
59
|
|
|
57
|
-
def validate_nonnegative_timeout(value: float, *, name: str) -> float:
|
|
58
|
-
"""Validate and normalize a finite timeout that may be zero."""
|
|
59
|
-
|
|
60
|
-
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
61
|
-
raise ValueError(f"{name} must be non-negative and finite")
|
|
62
|
-
try:
|
|
63
|
-
normalized = float(value)
|
|
64
|
-
except OverflowError as exc:
|
|
65
|
-
raise ValueError(f"{name} must be non-negative and finite") from exc
|
|
66
|
-
if not math.isfinite(normalized) or normalized < 0:
|
|
67
|
-
raise ValueError(f"{name} must be non-negative and finite")
|
|
68
|
-
return normalized
|
|
69
|
-
|
|
70
|
-
|
|
71
60
|
class BaseEventBus(Generic[T], ABC):
|
|
72
61
|
def __init__(
|
|
73
62
|
self,
|
|
@@ -125,6 +114,18 @@ class BaseEventBus(Generic[T], ABC):
|
|
|
125
114
|
if self._history is not None:
|
|
126
115
|
self._history.append(event)
|
|
127
116
|
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _query_replay(
|
|
119
|
+
history: EventHistory[T] | None,
|
|
120
|
+
replay: int,
|
|
121
|
+
has_filter: bool,
|
|
122
|
+
) -> tuple[T, ...]:
|
|
123
|
+
if not replay or history is None:
|
|
124
|
+
return ()
|
|
125
|
+
if has_filter:
|
|
126
|
+
return history.query()
|
|
127
|
+
return history.query(limit=replay)
|
|
128
|
+
|
|
128
129
|
@staticmethod
|
|
129
130
|
def _delivery_limit(*, once: bool, times: int | None) -> int | None:
|
|
130
131
|
if times is not None:
|
|
@@ -24,7 +24,9 @@ from .subscription import AsyncSubscription, Subscription
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def _channel_name(name: str) -> str:
|
|
27
|
-
|
|
27
|
+
if not isinstance(name, str):
|
|
28
|
+
raise TypeError("Event channel name must be a string")
|
|
29
|
+
normalized = name.strip().lower()
|
|
28
30
|
if not normalized:
|
|
29
31
|
raise ValueError("Event channel name cannot be empty")
|
|
30
32
|
return normalized
|
|
@@ -95,6 +97,7 @@ class EventEngine:
|
|
|
95
97
|
executor=self._executor,
|
|
96
98
|
actions=self._actions,
|
|
97
99
|
)
|
|
100
|
+
bus._claim_engine_ownership(self)
|
|
98
101
|
self._channels[name] = bus
|
|
99
102
|
self._running = True
|
|
100
103
|
return bus
|
|
@@ -313,6 +316,7 @@ class AsyncEventEngine:
|
|
|
313
316
|
max_concurrency=self._max_concurrency,
|
|
314
317
|
actions=self._actions,
|
|
315
318
|
)
|
|
319
|
+
bus._claim_engine_ownership(self)
|
|
316
320
|
self._channels[name] = bus
|
|
317
321
|
self._running = True
|
|
318
322
|
return bus
|
|
@@ -17,9 +17,8 @@ from .base import (
|
|
|
17
17
|
BaseEventBus,
|
|
18
18
|
normalize_replay_capacity,
|
|
19
19
|
validate_nonnegative_count,
|
|
20
|
-
validate_nonnegative_timeout,
|
|
21
20
|
validate_positive_capacity,
|
|
22
|
-
|
|
21
|
+
_validate_timeout,
|
|
23
22
|
)
|
|
24
23
|
from .contracts import ErrorPolicy, EventBusStats, PublishResult
|
|
25
24
|
from .compatibility import ExceptionGroup
|
|
@@ -73,6 +72,11 @@ class _Subscriber(Generic[T]):
|
|
|
73
72
|
self.remaining -= 1
|
|
74
73
|
return True, self.remaining == 0
|
|
75
74
|
|
|
75
|
+
def restore_claim(self) -> None:
|
|
76
|
+
with self.lock:
|
|
77
|
+
if self.remaining is not None:
|
|
78
|
+
self.remaining += 1
|
|
79
|
+
|
|
76
80
|
def buffer_if_replaying(self, event: T) -> bool:
|
|
77
81
|
with self.lock:
|
|
78
82
|
if not self.replaying:
|
|
@@ -116,10 +120,7 @@ class EventBus(BaseEventBus[T]):
|
|
|
116
120
|
max_schedules,
|
|
117
121
|
name="Maximum periodic schedules",
|
|
118
122
|
)
|
|
119
|
-
shutdown_timeout =
|
|
120
|
-
shutdown_timeout,
|
|
121
|
-
name="Shutdown timeout",
|
|
122
|
-
)
|
|
123
|
+
shutdown_timeout = _validate_timeout(shutdown_timeout, name="Shutdown timeout")
|
|
123
124
|
direct_actions = (on_before, on_after, on_error, on_timeout)
|
|
124
125
|
if actions is not None and any(action is not None for action in direct_actions):
|
|
125
126
|
raise ValueError("Use actions or direct on_* callbacks, not both")
|
|
@@ -193,13 +194,11 @@ class EventBus(BaseEventBus[T]):
|
|
|
193
194
|
history = self._history
|
|
194
195
|
subscription_id = self._next_id
|
|
195
196
|
self._next_id += 1
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
else:
|
|
202
|
-
replay_snapshot = ()
|
|
197
|
+
replay_snapshot = self._query_replay(
|
|
198
|
+
history,
|
|
199
|
+
replay,
|
|
200
|
+
any(option is not None for option in (event_filter, event_type, predicate)),
|
|
201
|
+
)
|
|
203
202
|
|
|
204
203
|
def cancel() -> None:
|
|
205
204
|
with self._lock:
|
|
@@ -277,33 +276,18 @@ class EventBus(BaseEventBus[T]):
|
|
|
277
276
|
with self._lock:
|
|
278
277
|
self._ensure_open()
|
|
279
278
|
subscribers = tuple(self._subscribers.values())
|
|
280
|
-
self._published += 1
|
|
281
279
|
self._record(event)
|
|
280
|
+
self._published += 1
|
|
282
281
|
|
|
283
|
-
matched: list[_Subscriber[T]] = []
|
|
284
282
|
errors: list[Exception] = []
|
|
285
283
|
if self._actions.on_before is not None:
|
|
286
284
|
try:
|
|
287
285
|
synchronous_result(self._actions.on_before(event))
|
|
288
286
|
except Exception as exc:
|
|
289
287
|
errors.append(exc)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
try:
|
|
294
|
-
matches = subscriber.event_filter.matches(event)
|
|
295
|
-
except Exception as exc:
|
|
296
|
-
errors.append(exc)
|
|
297
|
-
continue
|
|
298
|
-
if matches:
|
|
299
|
-
try:
|
|
300
|
-
if subscriber.buffer_if_replaying(event):
|
|
301
|
-
continue
|
|
302
|
-
except BufferError as exc:
|
|
303
|
-
subscriber.subscription.cancel()
|
|
304
|
-
errors.append(exc)
|
|
305
|
-
continue
|
|
306
|
-
matched.append(subscriber)
|
|
288
|
+
|
|
289
|
+
matched, matching_errors = self._match_subscribers(subscribers, event)
|
|
290
|
+
errors.extend(matching_errors)
|
|
307
291
|
|
|
308
292
|
delivered = 0
|
|
309
293
|
active_executors = getattr(_executor_context, "active", frozenset())
|
|
@@ -320,17 +304,20 @@ class EventBus(BaseEventBus[T]):
|
|
|
320
304
|
claimed, final = subscriber.claim()
|
|
321
305
|
if not claimed:
|
|
322
306
|
continue
|
|
323
|
-
if final:
|
|
324
|
-
subscriber.subscription._consume()
|
|
325
307
|
try:
|
|
326
308
|
future = self._executor.submit(
|
|
327
309
|
self._invoke_executor_callback,
|
|
328
|
-
subscriber
|
|
310
|
+
subscriber,
|
|
329
311
|
event,
|
|
330
312
|
active_executors,
|
|
313
|
+
final,
|
|
331
314
|
)
|
|
332
315
|
except Exception as exc:
|
|
316
|
+
subscriber.restore_claim()
|
|
333
317
|
errors.append(exc)
|
|
318
|
+
except BaseException:
|
|
319
|
+
subscriber.restore_claim()
|
|
320
|
+
raise
|
|
334
321
|
else:
|
|
335
322
|
futures.append((subscriber, future))
|
|
336
323
|
for _subscriber, future in futures:
|
|
@@ -368,12 +355,45 @@ class EventBus(BaseEventBus[T]):
|
|
|
368
355
|
raise ExceptionGroup("Event delivery failed", errors)
|
|
369
356
|
return result
|
|
370
357
|
|
|
358
|
+
def _match_subscribers(
|
|
359
|
+
self,
|
|
360
|
+
subscribers: tuple[_Subscriber[T], ...],
|
|
361
|
+
event: T,
|
|
362
|
+
) -> tuple[list[_Subscriber[T]], list[Exception]]:
|
|
363
|
+
matched: list[_Subscriber[T]] = []
|
|
364
|
+
errors: list[Exception] = []
|
|
365
|
+
for subscriber in subscribers:
|
|
366
|
+
if not subscriber.subscription.active:
|
|
367
|
+
continue
|
|
368
|
+
try:
|
|
369
|
+
matches = subscriber.event_filter.matches(event)
|
|
370
|
+
except Exception as exc:
|
|
371
|
+
errors.append(exc)
|
|
372
|
+
continue
|
|
373
|
+
if matches:
|
|
374
|
+
try:
|
|
375
|
+
if subscriber.buffer_if_replaying(event):
|
|
376
|
+
continue
|
|
377
|
+
except BufferError as exc:
|
|
378
|
+
subscriber.subscription.cancel()
|
|
379
|
+
errors.append(exc)
|
|
380
|
+
continue
|
|
381
|
+
matched.append(subscriber)
|
|
382
|
+
return matched, errors
|
|
383
|
+
|
|
371
384
|
def _invoke_executor_callback(
|
|
372
385
|
self,
|
|
373
|
-
|
|
386
|
+
subscriber: _Subscriber[T],
|
|
374
387
|
event: T,
|
|
375
388
|
inherited_executors: frozenset[int],
|
|
389
|
+
final: bool,
|
|
376
390
|
) -> None:
|
|
391
|
+
if final:
|
|
392
|
+
try:
|
|
393
|
+
subscriber.subscription._consume()
|
|
394
|
+
except BaseException:
|
|
395
|
+
subscriber.restore_claim()
|
|
396
|
+
raise
|
|
377
397
|
previous = getattr(_executor_context, "active", None)
|
|
378
398
|
active = set(inherited_executors)
|
|
379
399
|
if previous is not None:
|
|
@@ -382,7 +402,7 @@ class EventBus(BaseEventBus[T]):
|
|
|
382
402
|
active.add(id(self._executor))
|
|
383
403
|
_executor_context.active = frozenset(active)
|
|
384
404
|
try:
|
|
385
|
-
_invoke_sync_callback(callback, event)
|
|
405
|
+
_invoke_sync_callback(subscriber.callback, event)
|
|
386
406
|
finally:
|
|
387
407
|
if previous is None:
|
|
388
408
|
del _executor_context.active
|
|
@@ -414,7 +434,11 @@ class EventBus(BaseEventBus[T]):
|
|
|
414
434
|
if not claimed:
|
|
415
435
|
return False, None
|
|
416
436
|
if final:
|
|
417
|
-
|
|
437
|
+
try:
|
|
438
|
+
subscriber.subscription._consume()
|
|
439
|
+
except BaseException:
|
|
440
|
+
subscriber.restore_claim()
|
|
441
|
+
raise
|
|
418
442
|
try:
|
|
419
443
|
_invoke_sync_callback(subscriber.callback, event)
|
|
420
444
|
except Exception as exc:
|
|
@@ -431,7 +455,7 @@ class EventBus(BaseEventBus[T]):
|
|
|
431
455
|
) -> Subscription:
|
|
432
456
|
"""Publish the same event periodically until cancelled or completed."""
|
|
433
457
|
|
|
434
|
-
interval =
|
|
458
|
+
interval = _validate_timeout(interval, name="Event publish interval")
|
|
435
459
|
if times is not None and (
|
|
436
460
|
isinstance(times, bool) or not isinstance(times, int) or times <= 0
|
|
437
461
|
):
|
|
@@ -499,7 +523,11 @@ class EventBus(BaseEventBus[T]):
|
|
|
499
523
|
timeout: float | None = None,
|
|
500
524
|
) -> T | None:
|
|
501
525
|
if timeout is not None:
|
|
502
|
-
timeout =
|
|
526
|
+
timeout = _validate_timeout(
|
|
527
|
+
timeout,
|
|
528
|
+
name="Event wait timeout",
|
|
529
|
+
allow_zero=True,
|
|
530
|
+
)
|
|
503
531
|
normalized_filter = coerce_event_filter(
|
|
504
532
|
event_filter,
|
|
505
533
|
event_type=event_type,
|