python-neva 3.1.1__tar.gz → 3.3.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.
- python_neva-3.3.0/CHANGELOG.md +115 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/PKG-INFO +6 -1
- python_neva-3.3.0/README.md +3 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/application.py +11 -6
- python_neva-3.3.0/neva/arch/markers.py +4 -0
- python_neva-3.3.0/neva/arch/scopes.py +5 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/service_provider.py +83 -2
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/events/__init__.py +2 -1
- python_neva-3.3.0/neva/events/contracts/__init__.py +7 -0
- python_neva-3.3.0/neva/events/contracts/dispatcher.py +24 -0
- python_neva-3.3.0/neva/events/contracts/event.py +7 -0
- python_neva-3.3.0/neva/events/contracts/handler.py +14 -0
- python_neva-3.3.0/neva/events/contracts/listener.py +19 -0
- python_neva-3.3.0/neva/events/dispatcher.py +135 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/events/event.py +2 -1
- python_neva-3.3.0/neva/events/event_registry.py +59 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/events/listener.py +10 -33
- python_neva-3.3.0/neva/events/policy.py +8 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/events/provider.py +5 -2
- {python_neva-3.1.1 → python_neva-3.3.0}/pyproject.toml +3 -15
- python_neva-3.3.0/tests/arch/test_cache.py +52 -0
- python_neva-3.3.0/tests/arch/test_extends.py +98 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/conftest.py +2 -1
- python_neva-3.3.0/tests/events/test_binding.py +11 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/test_dispatch.py +3 -2
- python_neva-3.3.0/tests/events/test_listen_on_parent_class.py +77 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/obs/test_correlation.py +4 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/obs/test_profiler.py +4 -0
- python_neva-3.3.0/uv.lock +2436 -0
- python_neva-3.1.1/CHANGELOG.md +0 -48
- python_neva-3.1.1/README.md +0 -0
- python_neva-3.1.1/neva/events/dispatcher.py +0 -89
- python_neva-3.1.1/neva/events/event_registry.py +0 -44
- python_neva-3.1.1/tests/events/test_event_registry.py +0 -101
- python_neva-3.1.1/uv.lock +0 -2440
- {python_neva-3.1.1 → python_neva-3.3.0}/.envrc +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/.gitignore +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/.pre-commit-config.yaml +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/.python-version +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/config.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/facade.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/integrations/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/integrations/faststream.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/arch/py.typed +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/config/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/config/base_providers.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/config/loader.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/config/py.typed +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/config/repository.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/config.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/connection.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/manager.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/provider.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/py.typed +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/database/transaction.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/events/py.typed +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/instrumentation/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/logging/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/logging/manager.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/logging/provider.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/middleware/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/middleware/correlation.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/middleware/profiler.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/obs/py.typed +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/polyfactory/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/polyfactory/factories.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/polyfactory/persistence.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/polyfactory/py.typed +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/encryption/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/encryption/encrypter.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/encryption/protocol.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/config.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/hash_manager.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/provider.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/py.typed +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/tokens/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/tokens/generate_token.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/tokens/hash_token.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/security/tokens/verify_token.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/accessors.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/app.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/app.pyi +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/config.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/config.pyi +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/crypt.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/crypt.pyi +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/db.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/db.pyi +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/event.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/event.pyi +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/hash.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/hash.pyi +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/log.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/facade/log.pyi +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/py.typed +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/results.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/strategy.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/strconv.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/support/time.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/testing/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/testing/fakes.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/testing/fixtures.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/testing/py.typed +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/neva/testing/test_case.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/ruff.toml +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/scripts/retag-with-changelog.sh +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/arch/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/arch/test_scope.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/config/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/config/test_loader.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/config/test_repository.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/conftest.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_connection_manager.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_database_manager.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_edge_cases.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_multi_connection.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_sqlalchemy_integration.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_transaction.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_transaction_context.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/database/test_transaction_registry.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/test_deferred.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/test_event.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/test_function_listener.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/events/test_immediate.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/obs/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/security/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/security/test_encrypter.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/security/test_hash_manager.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/__init__.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/test_event_fake.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/test_facade_restore.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/test_fixtures.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/test_refresh_database.py +0 -0
- {python_neva-3.1.1 → python_neva-3.3.0}/tests/testing/test_test_case.py +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
## 3.3.0 (2026-06-05)
|
|
2
|
+
|
|
3
|
+
### ✨ Features
|
|
4
|
+
|
|
5
|
+
- **tooling**: placeholder commit
|
|
6
|
+
|
|
7
|
+
### 💚👷 CI & Build
|
|
8
|
+
|
|
9
|
+
- **tooling**: default to base tag version for commitizen
|
|
10
|
+
|
|
11
|
+
### 📝💡 Documentation
|
|
12
|
+
|
|
13
|
+
- **project**: update doc
|
|
14
|
+
- **core**: placeholde readme
|
|
15
|
+
|
|
16
|
+
### 🔧🔨📦️ Configuration, Scripts, Packages
|
|
17
|
+
|
|
18
|
+
- **tooling**: update changelog script
|
|
19
|
+
- **project**: update versioning config
|
|
20
|
+
|
|
21
|
+
## 3.2.0 (2026-06-05)
|
|
22
|
+
|
|
23
|
+
### ✨ Features
|
|
24
|
+
|
|
25
|
+
- **tooling**: placeholder commit
|
|
26
|
+
|
|
27
|
+
### 🔧🔨📦️ Configuration, Scripts, Packages
|
|
28
|
+
|
|
29
|
+
- **tooling**: update changelog script
|
|
30
|
+
- **project**: update versioning config
|
|
31
|
+
|
|
32
|
+
### 📝💡 Documentation
|
|
33
|
+
|
|
34
|
+
- **core**: placeholde readme
|
|
35
|
+
|
|
36
|
+
## 3.1.1 (2026-05-11)
|
|
37
|
+
|
|
38
|
+
### 📌➕⬇️➖⬆️ Dependencies
|
|
39
|
+
|
|
40
|
+
- bump opt dep on neva-fastapi
|
|
41
|
+
|
|
42
|
+
## 3.1.0 (2026-05-11)
|
|
43
|
+
|
|
44
|
+
### ✨ Features
|
|
45
|
+
|
|
46
|
+
- fixing db facade
|
|
47
|
+
- Add eq method to Some / Ok
|
|
48
|
+
- Deprecate register_engine, add register_connection
|
|
49
|
+
- RefreshDatabase now properly use the testcase inner app
|
|
50
|
+
- Add neva-fastapi as optional dependency
|
|
51
|
+
- renaming of make / make_async for retrocompatibility
|
|
52
|
+
|
|
53
|
+
### 🐛🚑️ Fixes
|
|
54
|
+
|
|
55
|
+
- Fix provider registration ordering
|
|
56
|
+
|
|
57
|
+
### ♻️ Refactorings
|
|
58
|
+
|
|
59
|
+
- refactor of results / db toolkit
|
|
60
|
+
|
|
61
|
+
## 3.1.0a1 (2026-05-05)
|
|
62
|
+
|
|
63
|
+
### build
|
|
64
|
+
|
|
65
|
+
- add commitizen + versioningit
|
|
66
|
+
|
|
67
|
+
### 💚👷 CI & Build
|
|
68
|
+
|
|
69
|
+
- update perms on tag script
|
|
70
|
+
- tweaking cz tags
|
|
71
|
+
- remove auto-annotated tags
|
|
72
|
+
- configure versioningit + cz
|
|
73
|
+
- Fix cz config >>> ⏰ 1
|
|
74
|
+
- add and configure cz_gitmoji >>> ⏰ 5m
|
|
75
|
+
|
|
76
|
+
### 📝💡 Documentation
|
|
77
|
+
|
|
78
|
+
- clear changelog
|
|
79
|
+
|
|
80
|
+
### 🔧🔨📦️ Configuration, Scripts, Packages
|
|
81
|
+
|
|
82
|
+
- improve auto-annotation of tags
|
|
83
|
+
- Fix tagging script
|
|
84
|
+
|
|
85
|
+
## python-neva-v3.2.0 (2026-06-04)
|
|
86
|
+
|
|
87
|
+
### ✨ Features
|
|
88
|
+
|
|
89
|
+
- **core**: add optional activator on all binding functions
|
|
90
|
+
- **core**: remove scoping option from scoped, default to request scope
|
|
91
|
+
- **core**: introduce conditional activation
|
|
92
|
+
- **core**: add specifying interface in extends method
|
|
93
|
+
- **core**: add extends feature
|
|
94
|
+
|
|
95
|
+
### build
|
|
96
|
+
|
|
97
|
+
- **core**: remove redundant .envrc files
|
|
98
|
+
- **core**: fixing version derivation
|
|
99
|
+
|
|
100
|
+
### docs
|
|
101
|
+
|
|
102
|
+
- **core,-fastapi**: add contributing documentation
|
|
103
|
+
|
|
104
|
+
### feat
|
|
105
|
+
|
|
106
|
+
- **core**: allow passing context data to scope method
|
|
107
|
+
- **core**: add some utility functions for binding
|
|
108
|
+
- **core**: add some utility functions for binding
|
|
109
|
+
- **core**: add the possibility to turn off dependency caching
|
|
110
|
+
- **core,-fastapi**: add .envrc file
|
|
111
|
+
|
|
112
|
+
### refactor
|
|
113
|
+
|
|
114
|
+
- **core**: cleaning up type hints
|
|
115
|
+
- **core**: update type hints on main application
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-neva
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: aiosqlite>=0.20.0
|
|
@@ -20,3 +20,8 @@ Requires-Dist: neva-fastapi>=1.0.0; extra == 'fastapi'
|
|
|
20
20
|
Provides-Extra: testing
|
|
21
21
|
Requires-Dist: pytest-asyncio>=0.25.3; extra == 'testing'
|
|
22
22
|
Requires-Dist: pytest>=9.0.2; extra == 'testing'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# The Neva framework
|
|
26
|
+
|
|
27
|
+
PLACEHOLDER
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from collections import OrderedDict
|
|
5
|
-
from collections.abc import
|
|
5
|
+
from collections.abc import AsyncGenerator, Sequence
|
|
6
6
|
from contextlib import AsyncExitStack, asynccontextmanager
|
|
7
7
|
from contextvars import ContextVar
|
|
8
8
|
from pathlib import Path
|
|
@@ -13,6 +13,7 @@ from dishka.provider import BaseProvider
|
|
|
13
13
|
from typing_extensions import deprecated
|
|
14
14
|
|
|
15
15
|
from neva.arch.facade import Facade
|
|
16
|
+
from neva.arch.scopes import BaseScope, Scope
|
|
16
17
|
from neva.arch.service_provider import Bootable, ServiceProvider
|
|
17
18
|
from neva.config.loader import ConfigLoader
|
|
18
19
|
from neva.support import Err, Ok, Result
|
|
@@ -41,7 +42,7 @@ class Application:
|
|
|
41
42
|
|
|
42
43
|
self.config: ConfigRepository = ConfigRepository()
|
|
43
44
|
self.providers: OrderedDict[type, ServiceProvider] = OrderedDict()
|
|
44
|
-
self.root_provider: dishka.Provider = dishka.Provider(scope=
|
|
45
|
+
self.root_provider: dishka.Provider = dishka.Provider(scope=Scope.APP)
|
|
45
46
|
|
|
46
47
|
configuration_path = (
|
|
47
48
|
config_path
|
|
@@ -112,7 +113,7 @@ class Application:
|
|
|
112
113
|
source: type | Callable[..., Any],
|
|
113
114
|
*,
|
|
114
115
|
interface: type | None = None,
|
|
115
|
-
scope:
|
|
116
|
+
scope: BaseScope | None = None,
|
|
116
117
|
) -> None:
|
|
117
118
|
"""Binds a source to the container."""
|
|
118
119
|
_ = self.root_provider.provide(
|
|
@@ -146,14 +147,18 @@ class Application:
|
|
|
146
147
|
return Err(f"Failed to resolve service '{interface.__name__}': {e}")
|
|
147
148
|
|
|
148
149
|
@asynccontextmanager
|
|
149
|
-
async def scope(
|
|
150
|
+
async def scope(
|
|
151
|
+
self,
|
|
152
|
+
scope: BaseScope | None = None,
|
|
153
|
+
context: dict[type, Any] | None = None,
|
|
154
|
+
) -> AsyncGenerator[Self]:
|
|
150
155
|
"""Enter a new scope.
|
|
151
156
|
|
|
152
157
|
Yields:
|
|
153
158
|
The application instance with the new scope.
|
|
154
159
|
"""
|
|
155
160
|
parent = _current_container.get(self.container)
|
|
156
|
-
async with parent(scope=scope) as container:
|
|
161
|
+
async with parent(scope=scope, context=context) as container:
|
|
157
162
|
token = _current_container.set(container)
|
|
158
163
|
try:
|
|
159
164
|
yield self
|
|
@@ -161,7 +166,7 @@ class Application:
|
|
|
161
166
|
_current_container.reset(token)
|
|
162
167
|
|
|
163
168
|
@asynccontextmanager
|
|
164
|
-
async def lifespan(self) ->
|
|
169
|
+
async def lifespan(self) -> AsyncGenerator[None]:
|
|
165
170
|
"""Wire the facades and providers."""
|
|
166
171
|
Facade.set_facade_application(self)
|
|
167
172
|
|
|
@@ -21,6 +21,8 @@ from typing import (
|
|
|
21
21
|
|
|
22
22
|
import dishka
|
|
23
23
|
|
|
24
|
+
from neva.arch.markers import BaseMarker, Marker
|
|
25
|
+
from neva.arch.scopes import BaseScope, Scope
|
|
24
26
|
from neva.support import Result
|
|
25
27
|
|
|
26
28
|
|
|
@@ -69,6 +71,7 @@ class ServiceProvider(abc.ABC):
|
|
|
69
71
|
|
|
70
72
|
app: "Application"
|
|
71
73
|
listen: ClassVar[dict[type[Event], list[type[EventListener[Any]]]]] = {}
|
|
74
|
+
when: ClassVar[Marker | None] = None
|
|
72
75
|
|
|
73
76
|
def __init__(self, app: "Application") -> None:
|
|
74
77
|
"""Initialize the service provider.
|
|
@@ -77,23 +80,101 @@ class ServiceProvider(abc.ABC):
|
|
|
77
80
|
app: The application instance.
|
|
78
81
|
|
|
79
82
|
"""
|
|
80
|
-
self.provider: dishka.Provider = dishka.Provider(
|
|
83
|
+
self.provider: dishka.Provider = dishka.Provider(
|
|
84
|
+
scope=dishka.Scope.APP,
|
|
85
|
+
when=self.when,
|
|
86
|
+
)
|
|
81
87
|
self.app = app
|
|
82
88
|
|
|
89
|
+
def activator(
|
|
90
|
+
self,
|
|
91
|
+
activation_fn: Callable[..., bool],
|
|
92
|
+
*markers: Marker | type[Marker],
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Registers an activator function for the given markers.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
activation_fn: The activator function.
|
|
98
|
+
markers: Markers triggered by this function.
|
|
99
|
+
"""
|
|
100
|
+
_ = self.provider.activate(activation_fn, *markers)
|
|
101
|
+
|
|
83
102
|
def bind(
|
|
84
103
|
self,
|
|
85
104
|
source: type | Callable[..., Any],
|
|
86
105
|
*,
|
|
87
106
|
interface: type | None = None,
|
|
88
|
-
scope:
|
|
107
|
+
scope: BaseScope | None = None,
|
|
108
|
+
when: BaseMarker | None = None,
|
|
109
|
+
cache: bool = True,
|
|
89
110
|
) -> None:
|
|
90
111
|
"""Binds a source to the container."""
|
|
91
112
|
_ = self.provider.provide(
|
|
92
113
|
source=source,
|
|
93
114
|
scope=scope,
|
|
94
115
|
provides=interface,
|
|
116
|
+
cache=cache,
|
|
117
|
+
when=when,
|
|
95
118
|
)
|
|
96
119
|
|
|
120
|
+
def scoped(
|
|
121
|
+
self,
|
|
122
|
+
source: type | Callable[..., Any],
|
|
123
|
+
*,
|
|
124
|
+
interface: type | None = None,
|
|
125
|
+
when: BaseMarker | None = None,
|
|
126
|
+
) -> Self:
|
|
127
|
+
"""Binds the source to the container.
|
|
128
|
+
|
|
129
|
+
Scope is REQUEST by default but a custom scope be provided.
|
|
130
|
+
Dependency declared with this are cached no matter what.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
the provider itself for chaining purposes
|
|
134
|
+
"""
|
|
135
|
+
self.bind(source, interface=interface, scope=Scope.REQUEST, when=when)
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def transient(
|
|
139
|
+
self,
|
|
140
|
+
source: type | Callable[..., Any],
|
|
141
|
+
*,
|
|
142
|
+
interface: type | None = None,
|
|
143
|
+
when: BaseMarker | None = None,
|
|
144
|
+
) -> Self:
|
|
145
|
+
"""Binds the source to the container as a transient dependency.
|
|
146
|
+
|
|
147
|
+
Dependencies declared this way have global scope and are never
|
|
148
|
+
cached.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
the provider itself for chaining purposes
|
|
152
|
+
"""
|
|
153
|
+
self.bind(source, interface=interface, when=when, cache=False)
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
def extend(
|
|
157
|
+
self,
|
|
158
|
+
source: Callable[..., Any],
|
|
159
|
+
*,
|
|
160
|
+
interface: type | None = None,
|
|
161
|
+
scope: dishka.BaseScope | None = None,
|
|
162
|
+
when: BaseMarker | None = None,
|
|
163
|
+
) -> Self:
|
|
164
|
+
"""Extends a dependency declared by another provider.
|
|
165
|
+
|
|
166
|
+
This allows to 'extend' a dependency, maybe even override it entirely.
|
|
167
|
+
Particularly useful if you want to run some code on a dependency declared
|
|
168
|
+
by another provider / package.
|
|
169
|
+
|
|
170
|
+
You may also provide a scope for this.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
the provider itself for chaining purposes
|
|
174
|
+
"""
|
|
175
|
+
_ = self.provider.decorate(source, provides=interface, scope=scope, when=when)
|
|
176
|
+
return self
|
|
177
|
+
|
|
97
178
|
@abc.abstractmethod
|
|
98
179
|
def register(self) -> Result[Self, str]:
|
|
99
180
|
"""Register services into the application container.
|
|
@@ -9,7 +9,8 @@ Three components are of particular interest:
|
|
|
9
9
|
|
|
10
10
|
from neva.events.dispatcher import EventDispatcher
|
|
11
11
|
from neva.events.event import Event
|
|
12
|
-
from neva.events.listener import EventListener,
|
|
12
|
+
from neva.events.listener import EventListener, listener
|
|
13
|
+
from neva.events.policy import HandlingPolicy
|
|
13
14
|
from neva.events.provider import EventServiceProvider
|
|
14
15
|
|
|
15
16
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from neva.events.contracts.dispatcher import EventDispatcher
|
|
2
|
+
from neva.events.contracts.event import Event
|
|
3
|
+
from neva.events.contracts.handler import EventHandler
|
|
4
|
+
from neva.events.contracts.listener import EventListener
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
__all__ = ["Event", "EventDispatcher", "EventHandler", "EventListener"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from neva.events.contracts.event import Event
|
|
4
|
+
from neva.events.contracts.listener import EventListener
|
|
5
|
+
from neva.support import Result
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EventDispatcher(Protocol):
|
|
9
|
+
"""Event dispatcher protocol."""
|
|
10
|
+
|
|
11
|
+
async def dispatch(
|
|
12
|
+
self,
|
|
13
|
+
event: Event,
|
|
14
|
+
) -> list[Result[None, str]]:
|
|
15
|
+
"""Dispatch an event to all registered listeners."""
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
def listen[T: Event](
|
|
19
|
+
self,
|
|
20
|
+
event_cls: type[T],
|
|
21
|
+
listener_cls: type[EventListener[T]],
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Register a listener for an event."""
|
|
24
|
+
...
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from neva.events.contracts.event import Event
|
|
4
|
+
from neva.support import Result
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EventHandler[T: Event](Protocol):
|
|
8
|
+
"""Define a valid function for event handling."""
|
|
9
|
+
|
|
10
|
+
__name__: str
|
|
11
|
+
|
|
12
|
+
async def __call__(self, event: T) -> Result[None, str]:
|
|
13
|
+
"""Handle an event."""
|
|
14
|
+
...
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from neva.events.contracts.event import Event
|
|
4
|
+
from neva.events.policy import HandlingPolicy
|
|
5
|
+
from neva.support import Result
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EventListener[T: Event](Protocol):
|
|
9
|
+
"""Event listener protocol."""
|
|
10
|
+
|
|
11
|
+
policy: HandlingPolicy
|
|
12
|
+
|
|
13
|
+
async def handle(self, event: T) -> Result[None, str]:
|
|
14
|
+
"""Handle an event.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
A result indicating whether the event was handled successfully.
|
|
18
|
+
"""
|
|
19
|
+
...
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Base implementation of the event dispatcher."""
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Protocol, override, runtime_checkable
|
|
4
|
+
|
|
5
|
+
from neva.arch.application import Application
|
|
6
|
+
from neva.database.manager import DatabaseManager
|
|
7
|
+
from neva.database.transaction import TransactionCallback
|
|
8
|
+
from neva.events import contracts
|
|
9
|
+
from neva.events.event_registry import EventRegistry
|
|
10
|
+
from neva.support import Err, Nothing, Result, Some
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class AsyncBeforeDispatchHook(Protocol):
|
|
15
|
+
async def __call__(self, context: contracts.Event) -> None: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class SyncBeforeDispatchHook(Protocol):
|
|
20
|
+
def __call__(self, context: contracts.Event) -> None: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
BeforeDispatchHook = AsyncBeforeDispatchHook | SyncBeforeDispatchHook
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EventDispatcher(contracts.EventDispatcher):
|
|
27
|
+
"""Event dispatcher implementation."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
app: Application,
|
|
32
|
+
db: DatabaseManager,
|
|
33
|
+
registry: EventRegistry,
|
|
34
|
+
) -> None:
|
|
35
|
+
self._registry: EventRegistry = registry
|
|
36
|
+
self._app: Application = app
|
|
37
|
+
self._db: DatabaseManager = db
|
|
38
|
+
self._before_dispatch_hooks: list[BeforeDispatchHook] = []
|
|
39
|
+
|
|
40
|
+
async def _apply_before_dispatch(self, event: contracts.Event) -> None:
|
|
41
|
+
"""Extension hook called before listeners are invoked.
|
|
42
|
+
|
|
43
|
+
Override in a subclass to add cross-cutting behaviour such as
|
|
44
|
+
persisting the event to an event store. The default implementation
|
|
45
|
+
is a no-op.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
event: The event about to be dispatched.
|
|
49
|
+
"""
|
|
50
|
+
for hook in self._before_dispatch_hooks:
|
|
51
|
+
match hook:
|
|
52
|
+
case AsyncBeforeDispatchHook():
|
|
53
|
+
await hook(event)
|
|
54
|
+
case SyncBeforeDispatchHook():
|
|
55
|
+
hook(event)
|
|
56
|
+
|
|
57
|
+
async def before_dispatch(self, hook: BeforeDispatchHook) -> None:
|
|
58
|
+
"""Register a hook to be called before listeners are invoked."""
|
|
59
|
+
self._before_dispatch_hooks.append(hook)
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
async def dispatch(self, event: contracts.Event) -> list[Result[None, str]]:
|
|
63
|
+
"""Dispatch an event to all registered listeners.
|
|
64
|
+
|
|
65
|
+
Listeners are resolved from the DI container when available,
|
|
66
|
+
falling back to direct instantiation otherwise.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
event: The event to dispatch.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
A list of results, one per listener invocation.
|
|
73
|
+
"""
|
|
74
|
+
await self._apply_before_dispatch(event)
|
|
75
|
+
results: list[Result[None, str]] = []
|
|
76
|
+
listeners = self._registry.resolve_listeners(event)
|
|
77
|
+
|
|
78
|
+
immediate: list[type[contracts.EventListener[contracts.Event]]] = []
|
|
79
|
+
deferred: list[type[contracts.EventListener[contracts.Event]]] = []
|
|
80
|
+
[immediate.extend(listener.immediate.copy()) for listener in listeners]
|
|
81
|
+
[deferred.extend(listener.deferred.copy()) for listener in listeners]
|
|
82
|
+
|
|
83
|
+
match self._db.current():
|
|
84
|
+
case Some(tx):
|
|
85
|
+
for listener_cls in deferred:
|
|
86
|
+
_ = tx.on_commit(self._build_callback(event, listener_cls))
|
|
87
|
+
case Nothing():
|
|
88
|
+
immediate.extend(deferred)
|
|
89
|
+
|
|
90
|
+
for listener_cls in immediate:
|
|
91
|
+
listener = self._resolve_listener(listener_cls)
|
|
92
|
+
try:
|
|
93
|
+
results.append(await listener.handle(event))
|
|
94
|
+
except Exception as e:
|
|
95
|
+
results.append(Err(f"Listener raised: {e}"))
|
|
96
|
+
|
|
97
|
+
return results
|
|
98
|
+
|
|
99
|
+
@override
|
|
100
|
+
def listen[T: contracts.Event](
|
|
101
|
+
self,
|
|
102
|
+
event_cls: type[T] | list[type[T]],
|
|
103
|
+
listener_cls: type[contracts.EventListener[T]],
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Register a listener for an event."""
|
|
106
|
+
if not isinstance(event_cls, list):
|
|
107
|
+
event_cls = [event_cls]
|
|
108
|
+
self._registry.register(event_cls, listener_cls)
|
|
109
|
+
|
|
110
|
+
def _resolve_listener[T: contracts.Event](
|
|
111
|
+
self, listener_cls: type[contracts.EventListener[T]]
|
|
112
|
+
) -> contracts.EventListener[T]:
|
|
113
|
+
"""Resolve a listener from the container.
|
|
114
|
+
|
|
115
|
+
Listeners are resolved from the DI container when available,
|
|
116
|
+
falling back to direct instantiation otherwise.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
EventListener[Any]: The resolved listener.
|
|
120
|
+
"""
|
|
121
|
+
result = self._app.make(listener_cls)
|
|
122
|
+
if result.is_ok:
|
|
123
|
+
return result.unwrap()
|
|
124
|
+
return listener_cls()
|
|
125
|
+
|
|
126
|
+
def _build_callback[T: contracts.Event](
|
|
127
|
+
self,
|
|
128
|
+
event: T,
|
|
129
|
+
listener_cls: type[contracts.EventListener[T]],
|
|
130
|
+
) -> TransactionCallback:
|
|
131
|
+
async def callback() -> Result[None, str]:
|
|
132
|
+
listener = self._resolve_listener(listener_cls)
|
|
133
|
+
return await listener.handle(event)
|
|
134
|
+
|
|
135
|
+
return callback
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Event/listener registry."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any, TypeVar
|
|
5
|
+
|
|
6
|
+
from neva.events import contracts, policy
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EventListenerRegistry[T: contracts.Event]:
|
|
10
|
+
"""Event listener registry for a given event."""
|
|
11
|
+
|
|
12
|
+
def __init__(self) -> None:
|
|
13
|
+
self.immediate: list[type[contracts.EventListener[T]]] = []
|
|
14
|
+
self.deferred: list[type[contracts.EventListener[T]]] = []
|
|
15
|
+
|
|
16
|
+
def add_listener(self, listener_cls: type[contracts.EventListener[T]]) -> None:
|
|
17
|
+
"""Add a listener to the correct category in the registry."""
|
|
18
|
+
match listener_cls.policy:
|
|
19
|
+
case policy.HandlingPolicy.IMMEDIATE:
|
|
20
|
+
self.immediate.append(listener_cls)
|
|
21
|
+
case policy.HandlingPolicy.DEFERRED:
|
|
22
|
+
self.deferred.append(listener_cls)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
T = TypeVar("T", bound=contracts.Event, covariant=True)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class EventRegistry:
|
|
29
|
+
"""Event/listener registry."""
|
|
30
|
+
|
|
31
|
+
__listeners: dict[type[Any], EventListenerRegistry[Any]]
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
"""Initialize the registry."""
|
|
35
|
+
self.__listeners = {}
|
|
36
|
+
|
|
37
|
+
def register(
|
|
38
|
+
self,
|
|
39
|
+
event_cls: list[type[T]],
|
|
40
|
+
listener_cls: type[contracts.EventListener[T]],
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Register a listener for an event."""
|
|
43
|
+
for cls in event_cls:
|
|
44
|
+
self.__listeners.setdefault(cls, EventListenerRegistry()).add_listener(
|
|
45
|
+
listener_cls
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def resolve_listeners(self, event: T) -> list[EventListenerRegistry[T]]:
|
|
49
|
+
"""Return all listeners registries for an event type."""
|
|
50
|
+
registries = [
|
|
51
|
+
self.__listeners[cls]
|
|
52
|
+
for cls in inspect.getmro(type(event))
|
|
53
|
+
if cls in self.__listeners
|
|
54
|
+
]
|
|
55
|
+
return registries
|
|
56
|
+
|
|
57
|
+
def get_listeners(self, event: T) -> EventListenerRegistry[T]:
|
|
58
|
+
"""Return all listeners registered for an event type."""
|
|
59
|
+
return self.__listeners.get(type(event), EventListenerRegistry[T]())
|