python-neva 3.0.0__tar.gz → 3.1.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.
- python_neva-3.1.1/CHANGELOG.md +48 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/PKG-INFO +3 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/__init__.py +0 -2
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/application.py +33 -22
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/facade.py +3 -3
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/integrations/faststream.py +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/service_provider.py +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/config/base_providers.py +5 -5
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/config/loader.py +1 -1
- python_neva-3.1.1/neva/config/py.typed +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/config/repository.py +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/connection.py +33 -51
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/manager.py +41 -45
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/provider.py +4 -7
- python_neva-3.1.1/neva/database/py.typed +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/transaction.py +3 -22
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/dispatcher.py +2 -2
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/listener.py +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/provider.py +1 -1
- python_neva-3.1.1/neva/events/py.typed +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/logging/provider.py +1 -1
- python_neva-3.1.1/neva/obs/py.typed +0 -0
- python_neva-3.1.1/neva/polyfactory/__init__.py +4 -0
- python_neva-3.1.1/neva/polyfactory/factories.py +20 -0
- python_neva-3.1.1/neva/polyfactory/persistence.py +22 -0
- python_neva-3.1.1/neva/polyfactory/py.typed +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/encryption/encrypter.py +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/encryption/protocol.py +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/hash_manager.py +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/provider.py +2 -1
- python_neva-3.1.1/neva/security/py.typed +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/app.pyi +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/config.pyi +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/crypt.pyi +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/db.pyi +17 -23
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/event.pyi +2 -1
- python_neva-3.1.1/neva/support/py.typed +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/results.py +64 -51
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/strategy.py +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/testing/fakes.py +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/testing/fixtures.py +1 -11
- python_neva-3.1.1/neva/testing/py.typed +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/testing/test_case.py +2 -3
- {python_neva-3.0.0 → python_neva-3.1.1}/pyproject.toml +37 -13
- {python_neva-3.0.0 → python_neva-3.1.1}/ruff.toml +2 -1
- python_neva-3.1.1/scripts/retag-with-changelog.sh +16 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/arch/test_scope.py +74 -21
- python_neva-3.1.1/tests/conftest.py +45 -0
- python_neva-3.1.1/tests/database/test_connection_manager.py +50 -0
- python_neva-3.1.1/tests/database/test_database_manager.py +97 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_edge_cases.py +11 -20
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_multi_connection.py +23 -18
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_sqlalchemy_integration.py +11 -25
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_transaction.py +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_transaction_context.py +17 -41
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_transaction_registry.py +18 -10
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/conftest.py +0 -12
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_deferred.py +8 -8
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_dispatch.py +2 -2
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_event_registry.py +16 -6
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_function_listener.py +5 -5
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_immediate.py +2 -2
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/security/test_encrypter.py +3 -3
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/security/test_hash_manager.py +1 -1
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/testing/test_fixtures.py +6 -6
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/testing/test_refresh_database.py +14 -8
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/testing/test_test_case.py +7 -7
- {python_neva-3.0.0 → python_neva-3.1.1}/uv.lock +237 -5
- python_neva-3.0.0/neva/__init__.py +0 -24
- python_neva-3.0.0/neva/arch/integrations/fastapi.py +0 -112
- python_neva-3.0.0/neva/testing/http.py +0 -22
- python_neva-3.0.0/tests/conftest.py +0 -1
- python_neva-3.0.0/tests/database/conftest.py +0 -10
- python_neva-3.0.0/tests/database/test_connection_manager.py +0 -97
- python_neva-3.0.0/tests/database/test_database_manager.py +0 -98
- {python_neva-3.0.0 → python_neva-3.1.1}/.envrc +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/.gitignore +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/.pre-commit-config.yaml +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/.python-version +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/README.md +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/config.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/integrations/__init__.py +0 -0
- {python_neva-3.0.0/neva → python_neva-3.1.1/neva/arch}/py.typed +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/config/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/config.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/event.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/event_registry.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/instrumentation/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/instrumentation/sqlalchemy.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/logging/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/logging/manager.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/middleware/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/middleware/correlation.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/middleware/profiler.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/encryption/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/config.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/hashers/bcrypt.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/tokens/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/tokens/generate_token.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/tokens/hash_token.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/tokens/verify_token.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/accessors.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/app.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/config.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/crypt.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/db.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/event.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/hash.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/hash.pyi +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/log.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/log.pyi +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/strconv.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/time.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/neva/testing/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/arch/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/config/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/config/test_loader.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/config/test_repository.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_event.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/obs/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/obs/test_correlation.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/obs/test_profiler.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/security/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/testing/__init__.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/testing/test_event_fake.py +0 -0
- {python_neva-3.0.0 → python_neva-3.1.1}/tests/testing/test_facade_restore.py +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
## 3.1.1 (2026-05-11)
|
|
2
|
+
|
|
3
|
+
### 📌➕⬇️➖⬆️ Dependencies
|
|
4
|
+
|
|
5
|
+
- bump opt dep on neva-fastapi
|
|
6
|
+
|
|
7
|
+
## 3.1.0 (2026-05-11)
|
|
8
|
+
|
|
9
|
+
### ✨ Features
|
|
10
|
+
|
|
11
|
+
- fixing db facade
|
|
12
|
+
- Add eq method to Some / Ok
|
|
13
|
+
- Deprecate register_engine, add register_connection
|
|
14
|
+
- RefreshDatabase now properly use the testcase inner app
|
|
15
|
+
- Add neva-fastapi as optional dependency
|
|
16
|
+
- renaming of make / make_async for retrocompatibility
|
|
17
|
+
|
|
18
|
+
### 🐛🚑️ Fixes
|
|
19
|
+
|
|
20
|
+
- Fix provider registration ordering
|
|
21
|
+
|
|
22
|
+
### ♻️ Refactorings
|
|
23
|
+
|
|
24
|
+
- refactor of results / db toolkit
|
|
25
|
+
|
|
26
|
+
## 3.1.0a1 (2026-05-05)
|
|
27
|
+
|
|
28
|
+
### build
|
|
29
|
+
|
|
30
|
+
- add commitizen + versioningit
|
|
31
|
+
|
|
32
|
+
### 💚👷 CI & Build
|
|
33
|
+
|
|
34
|
+
- update perms on tag script
|
|
35
|
+
- tweaking cz tags
|
|
36
|
+
- remove auto-annotated tags
|
|
37
|
+
- configure versioningit + cz
|
|
38
|
+
- Fix cz config >>> ⏰ 1
|
|
39
|
+
- add and configure cz_gitmoji >>> ⏰ 5m
|
|
40
|
+
|
|
41
|
+
### 📝💡 Documentation
|
|
42
|
+
|
|
43
|
+
- clear changelog
|
|
44
|
+
|
|
45
|
+
### 🔧🔨📦️ Configuration, Scripts, Packages
|
|
46
|
+
|
|
47
|
+
- improve auto-annotation of tags
|
|
48
|
+
- Fix tagging script
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-neva
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.1.1
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: aiosqlite>=0.20.0
|
|
@@ -15,6 +15,8 @@ Requires-Dist: pyinstrument>=5.1.1
|
|
|
15
15
|
Requires-Dist: sqlalchemy[asyncio]>=2.0.0
|
|
16
16
|
Requires-Dist: structlog>=25.5.0
|
|
17
17
|
Requires-Dist: typer>=0.21.1
|
|
18
|
+
Provides-Extra: fastapi
|
|
19
|
+
Requires-Dist: neva-fastapi>=1.0.0; extra == 'fastapi'
|
|
18
20
|
Provides-Extra: testing
|
|
19
21
|
Requires-Dist: pytest-asyncio>=0.25.3; extra == 'testing'
|
|
20
22
|
Requires-Dist: pytest>=9.0.2; extra == 'testing'
|
|
@@ -6,7 +6,6 @@ and facade implementations.
|
|
|
6
6
|
|
|
7
7
|
from neva.arch.application import Application
|
|
8
8
|
from neva.arch.facade import Facade
|
|
9
|
-
from neva.arch.integrations.fastapi import App
|
|
10
9
|
from neva.arch.service_provider import (
|
|
11
10
|
Bootable,
|
|
12
11
|
ServiceProvider,
|
|
@@ -14,7 +13,6 @@ from neva.arch.service_provider import (
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
__all__ = [
|
|
17
|
-
"App",
|
|
18
16
|
"Application",
|
|
19
17
|
"Bootable",
|
|
20
18
|
"Facade",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Base application for DI and facade injection."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from collections
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from collections.abc import AsyncIterator, Sequence
|
|
5
6
|
from contextlib import AsyncExitStack, asynccontextmanager
|
|
7
|
+
from contextvars import ContextVar
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
from typing import Any, Callable, Self
|
|
8
10
|
|
|
@@ -10,10 +12,15 @@ import dishka
|
|
|
10
12
|
from dishka.provider import BaseProvider
|
|
11
13
|
from typing_extensions import deprecated
|
|
12
14
|
|
|
13
|
-
from neva import Err, Ok, Result
|
|
14
15
|
from neva.arch.facade import Facade
|
|
15
16
|
from neva.arch.service_provider import Bootable, ServiceProvider
|
|
16
17
|
from neva.config.loader import ConfigLoader
|
|
18
|
+
from neva.support import Err, Ok, Result
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_current_container: ContextVar["dishka.AsyncContainer"] = ContextVar(
|
|
22
|
+
"_current_container"
|
|
23
|
+
)
|
|
17
24
|
|
|
18
25
|
|
|
19
26
|
class Application:
|
|
@@ -33,7 +40,7 @@ class Application:
|
|
|
33
40
|
from neva.config.repository import ConfigRepository
|
|
34
41
|
|
|
35
42
|
self.config: ConfigRepository = ConfigRepository()
|
|
36
|
-
self.providers:
|
|
43
|
+
self.providers: OrderedDict[type, ServiceProvider] = OrderedDict()
|
|
37
44
|
self.root_provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
|
|
38
45
|
|
|
39
46
|
configuration_path = (
|
|
@@ -55,17 +62,19 @@ class Application:
|
|
|
55
62
|
lambda: self.config, provides=ConfigRepository
|
|
56
63
|
)
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
providers
|
|
65
|
-
|
|
65
|
+
providers: list[type[ServiceProvider]] = base_providers()
|
|
66
|
+
providers.extend(
|
|
67
|
+
self.config.get(
|
|
68
|
+
"providers.providers", type_=list[type[ServiceProvider]]
|
|
69
|
+
).unwrap_or([])
|
|
70
|
+
)
|
|
71
|
+
providers.extend(
|
|
72
|
+
self.config.get(
|
|
73
|
+
"app.providers", type_=list[type[ServiceProvider]]
|
|
74
|
+
).unwrap_or([])
|
|
66
75
|
)
|
|
67
76
|
_ = self.root_provider.provide(source=lambda: self, provides=Application)
|
|
68
|
-
self.register_providers(
|
|
77
|
+
self.register_providers(providers)
|
|
69
78
|
self._bind_event_listeners()
|
|
70
79
|
self.build_container(self.root_provider)
|
|
71
80
|
|
|
@@ -92,7 +101,7 @@ class Application:
|
|
|
92
101
|
.map(lambda p: self.providers.setdefault(provider, p))
|
|
93
102
|
)
|
|
94
103
|
|
|
95
|
-
def register_providers(self, providers:
|
|
104
|
+
def register_providers(self, providers: Sequence[type[ServiceProvider]]) -> None:
|
|
96
105
|
"""Registers a set of providers."""
|
|
97
106
|
for provider in providers:
|
|
98
107
|
_ = self.register(provider)
|
|
@@ -112,25 +121,27 @@ class Application:
|
|
|
112
121
|
provides=interface,
|
|
113
122
|
)
|
|
114
123
|
|
|
115
|
-
async def
|
|
124
|
+
async def make_async[T](self, interface: type[T]) -> Result[T, str]:
|
|
116
125
|
"""Resolve and instanciate a type from the container.
|
|
117
126
|
|
|
118
127
|
Returns:
|
|
119
128
|
Result containing the resolved type instance or an error message.
|
|
120
129
|
"""
|
|
130
|
+
container = _current_container.get(self.container)
|
|
121
131
|
try:
|
|
122
|
-
return Ok(await
|
|
132
|
+
return Ok(await container.get(interface))
|
|
123
133
|
except Exception as e:
|
|
124
134
|
return Err(f"Failed to resolve service '{interface.__name__}': {e}")
|
|
125
135
|
|
|
126
|
-
def
|
|
136
|
+
def make[T](self, interface: type[T]) -> Result[T, str]:
|
|
127
137
|
"""Synchronous version of make.
|
|
128
138
|
|
|
129
139
|
Returns:
|
|
130
140
|
Result containing the resolved type instance or an error message.
|
|
131
141
|
"""
|
|
142
|
+
container = _current_container.get(self.container)
|
|
132
143
|
try:
|
|
133
|
-
return Ok(
|
|
144
|
+
return Ok(container.get_sync(interface))
|
|
134
145
|
except Exception as e:
|
|
135
146
|
return Err(f"Failed to resolve service '{interface.__name__}': {e}")
|
|
136
147
|
|
|
@@ -141,13 +152,13 @@ class Application:
|
|
|
141
152
|
Yields:
|
|
142
153
|
The application instance with the new scope.
|
|
143
154
|
"""
|
|
144
|
-
parent = self.container
|
|
145
|
-
async with
|
|
146
|
-
|
|
155
|
+
parent = _current_container.get(self.container)
|
|
156
|
+
async with parent(scope=scope) as container:
|
|
157
|
+
token = _current_container.set(container)
|
|
147
158
|
try:
|
|
148
159
|
yield self
|
|
149
160
|
finally:
|
|
150
|
-
|
|
161
|
+
_current_container.reset(token)
|
|
151
162
|
|
|
152
163
|
@asynccontextmanager
|
|
153
164
|
async def lifespan(self) -> AsyncIterator[None]:
|
|
@@ -176,7 +187,7 @@ class Application:
|
|
|
176
187
|
"""Wire event-listener mappings from all providers onto the dispatcher."""
|
|
177
188
|
from neva.events.dispatcher import EventDispatcher
|
|
178
189
|
|
|
179
|
-
result = await self.
|
|
190
|
+
result = await self.make_async(EventDispatcher)
|
|
180
191
|
if result.is_err:
|
|
181
192
|
return
|
|
182
193
|
|
|
@@ -11,7 +11,7 @@ from contextlib import contextmanager
|
|
|
11
11
|
from typing import TYPE_CHECKING, Any, ClassVar, cast
|
|
12
12
|
from unittest.mock import AsyncMock, MagicMock
|
|
13
13
|
|
|
14
|
-
from neva import Ok, Option, Result, from_optional
|
|
14
|
+
from neva.support import Ok, Option, Result, from_optional
|
|
15
15
|
from neva.support.accessors import get_attr
|
|
16
16
|
|
|
17
17
|
|
|
@@ -153,7 +153,7 @@ class FacadeMeta(ABCMeta):
|
|
|
153
153
|
real: object = (
|
|
154
154
|
cls._get_app()
|
|
155
155
|
.ok_or(f"Cannot spy on {cls.__name__}: no application set.")
|
|
156
|
-
.and_then(lambda a: a.
|
|
156
|
+
.and_then(lambda a: a.make(cls.get_facade_accessor()))
|
|
157
157
|
.unwrap()
|
|
158
158
|
)
|
|
159
159
|
spy_obj = MagicMock(spec=type(real), wraps=real)
|
|
@@ -198,7 +198,7 @@ class FacadeMeta(ABCMeta):
|
|
|
198
198
|
"""
|
|
199
199
|
if cls in FacadeMeta._fake_instances:
|
|
200
200
|
return Ok(cast(T, FacadeMeta._fake_instances[cls]))
|
|
201
|
-
return app.
|
|
201
|
+
return app.make(interface)
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
class Facade(ABC, metaclass=FacadeMeta):
|
|
@@ -9,8 +9,8 @@ from faststream._internal.broker import BrokerUsecase
|
|
|
9
9
|
from faststream._internal.configs import BrokerConfig
|
|
10
10
|
from starlette.types import StatelessLifespan
|
|
11
11
|
|
|
12
|
-
from neva import Result
|
|
13
12
|
from neva.arch import Application, ServiceProvider
|
|
13
|
+
from neva.support import Result
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class FastStream(faststream.FastStream):
|
|
@@ -11,7 +11,7 @@ from neva.events.provider import EventServiceProvider
|
|
|
11
11
|
from neva.obs import LogServiceProvider
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def base_providers() ->
|
|
14
|
+
def base_providers() -> list[type[ServiceProvider]]:
|
|
15
15
|
"""Return the list of base service providers.
|
|
16
16
|
|
|
17
17
|
These providers are automatically registered during application
|
|
@@ -24,8 +24,8 @@ def base_providers() -> set[type[ServiceProvider]]:
|
|
|
24
24
|
Set of service provider classes to register.
|
|
25
25
|
|
|
26
26
|
"""
|
|
27
|
-
return
|
|
28
|
-
DatabaseServiceProvider,
|
|
29
|
-
EventServiceProvider,
|
|
27
|
+
return [
|
|
30
28
|
LogServiceProvider,
|
|
31
|
-
|
|
29
|
+
EventServiceProvider,
|
|
30
|
+
DatabaseServiceProvider,
|
|
31
|
+
]
|
|
File without changes
|
|
@@ -6,21 +6,21 @@ from contextvars import ContextVar
|
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from typing import final
|
|
8
8
|
|
|
9
|
-
from sqlalchemy.ext
|
|
9
|
+
from sqlalchemy.ext import asyncio
|
|
10
10
|
|
|
11
|
-
from neva import
|
|
12
|
-
from neva.database.transaction import BoundTransaction, Transaction, TransactionState
|
|
11
|
+
from neva.database.transaction import BoundTransaction, TransactionState
|
|
13
12
|
from neva.obs import LogManager
|
|
13
|
+
from neva.support import Nothing, Option, Some, from_optional
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass
|
|
17
17
|
class TransactionRegistry:
|
|
18
18
|
"""A registry of ongoing transactions."""
|
|
19
19
|
|
|
20
|
-
by_connection: dict[str,
|
|
21
|
-
stack: list[
|
|
20
|
+
by_connection: dict[str, BoundTransaction] = field(default_factory=dict)
|
|
21
|
+
stack: list[BoundTransaction] = field(default_factory=list)
|
|
22
22
|
|
|
23
|
-
def extend(self, transaction:
|
|
23
|
+
def extend(self, transaction: BoundTransaction) -> "TransactionRegistry":
|
|
24
24
|
"""Adds a new transaction to the registry.
|
|
25
25
|
|
|
26
26
|
Returns:
|
|
@@ -45,7 +45,7 @@ class TransactionContext:
|
|
|
45
45
|
if _tx_registry.get() is None:
|
|
46
46
|
_ = _tx_registry.set(TransactionRegistry())
|
|
47
47
|
|
|
48
|
-
def current(self, connection: str | None = None) -> Option[
|
|
48
|
+
def current(self, connection: str | None = None) -> Option[BoundTransaction]:
|
|
49
49
|
"""Get the current transaction.
|
|
50
50
|
|
|
51
51
|
Returns:
|
|
@@ -73,15 +73,28 @@ class ConnectionManager:
|
|
|
73
73
|
name: str,
|
|
74
74
|
tx_context: TransactionContext,
|
|
75
75
|
logger: LogManager | None,
|
|
76
|
-
|
|
76
|
+
engine: asyncio.AsyncEngine,
|
|
77
77
|
) -> None:
|
|
78
78
|
self.name = name
|
|
79
79
|
self.tx_context = tx_context
|
|
80
80
|
self.logger = logger
|
|
81
|
-
self.
|
|
81
|
+
self._engine = engine
|
|
82
|
+
self.session_factory = asyncio.async_sessionmaker(
|
|
83
|
+
bind=engine,
|
|
84
|
+
expire_on_commit=False,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def engine(self) -> asyncio.AsyncEngine:
|
|
89
|
+
"""Returns the underlying engine."""
|
|
90
|
+
return self._engine
|
|
91
|
+
|
|
92
|
+
async def dispose(self) -> None:
|
|
93
|
+
"""Clear the connection manager."""
|
|
94
|
+
await self._engine.dispose()
|
|
82
95
|
|
|
83
96
|
@asynccontextmanager
|
|
84
|
-
async def _scoped(self, tx:
|
|
97
|
+
async def _scoped(self, tx: BoundTransaction) -> AsyncIterator[None]:
|
|
85
98
|
"""Manage registry, state transitions, and callbacks for a transaction.
|
|
86
99
|
|
|
87
100
|
Yields:
|
|
@@ -129,22 +142,6 @@ class ConnectionManager:
|
|
|
129
142
|
finally:
|
|
130
143
|
_tx_registry.reset(token)
|
|
131
144
|
|
|
132
|
-
@asynccontextmanager
|
|
133
|
-
async def transaction(self) -> AsyncIterator[Transaction]:
|
|
134
|
-
"""Open a new unbound transaction (no database session).
|
|
135
|
-
|
|
136
|
-
Intended for use in unit tests where transaction lifecycle, callbacks,
|
|
137
|
-
and context isolation need to be exercised without a real database.
|
|
138
|
-
|
|
139
|
-
Yields:
|
|
140
|
-
Transaction: An unbound transaction with no associated session.
|
|
141
|
-
"""
|
|
142
|
-
parent = self.tx_context.current(self.name)
|
|
143
|
-
tx = Transaction(self.name)
|
|
144
|
-
tx.parent = parent
|
|
145
|
-
async with self._scoped(tx):
|
|
146
|
-
yield tx
|
|
147
|
-
|
|
148
145
|
@asynccontextmanager
|
|
149
146
|
async def begin(self) -> AsyncIterator[BoundTransaction]:
|
|
150
147
|
"""Open a new bound transaction with an active database session.
|
|
@@ -154,31 +151,18 @@ class ConnectionManager:
|
|
|
154
151
|
|
|
155
152
|
Yields:
|
|
156
153
|
BoundTransaction: A transaction with a guaranteed non-null session.
|
|
157
|
-
|
|
158
|
-
Raises:
|
|
159
|
-
RuntimeError: If no engine has been registered for this connection.
|
|
160
154
|
"""
|
|
161
|
-
if self.session_factory is None:
|
|
162
|
-
raise RuntimeError(
|
|
163
|
-
f"No engine registered for connection '{self.name}'. "
|
|
164
|
-
+ "Call register_engine() before calling begin()."
|
|
165
|
-
)
|
|
166
|
-
|
|
167
155
|
parent = self.tx_context.current(self.name)
|
|
168
156
|
|
|
169
157
|
match parent:
|
|
170
|
-
case Some(parent_tx) if
|
|
171
|
-
|
|
172
|
-
and parent_tx.is_accessible_from_current_task
|
|
173
|
-
):
|
|
174
|
-
tx = Transaction(self.name)
|
|
175
|
-
tx.parent = parent
|
|
176
|
-
bound = tx.begin(parent_tx.session)
|
|
158
|
+
case Some(parent_tx) if parent_tx.is_accessible_from_current_task:
|
|
159
|
+
tx = BoundTransaction(self.name, parent_tx.session)
|
|
177
160
|
sp = await parent_tx.session.begin_nested()
|
|
161
|
+
tx.parent = parent
|
|
178
162
|
try:
|
|
179
|
-
async with self._scoped(
|
|
180
|
-
yield
|
|
181
|
-
if
|
|
163
|
+
async with self._scoped(tx):
|
|
164
|
+
yield tx
|
|
165
|
+
if tx.rollback_requested:
|
|
182
166
|
await sp.rollback()
|
|
183
167
|
else:
|
|
184
168
|
await sp.commit()
|
|
@@ -186,14 +170,12 @@ class ConnectionManager:
|
|
|
186
170
|
await sp.rollback()
|
|
187
171
|
raise
|
|
188
172
|
case _:
|
|
189
|
-
tx = Transaction(self.name)
|
|
190
|
-
tx.parent = Nothing()
|
|
191
173
|
async with self.session_factory() as session:
|
|
192
|
-
|
|
174
|
+
tx = BoundTransaction(self.name, session)
|
|
193
175
|
try:
|
|
194
|
-
async with self._scoped(
|
|
195
|
-
yield
|
|
196
|
-
if
|
|
176
|
+
async with self._scoped(tx):
|
|
177
|
+
yield tx
|
|
178
|
+
if tx.rollback_requested:
|
|
197
179
|
await session.rollback()
|
|
198
180
|
else:
|
|
199
181
|
await session.commit()
|
|
@@ -4,13 +4,15 @@ from collections.abc import AsyncIterator
|
|
|
4
4
|
from contextlib import asynccontextmanager
|
|
5
5
|
from typing import final
|
|
6
6
|
|
|
7
|
-
from sqlalchemy.ext
|
|
7
|
+
from sqlalchemy.ext import asyncio
|
|
8
|
+
from typing_extensions import deprecated
|
|
8
9
|
|
|
9
|
-
from neva import
|
|
10
|
+
from neva.database.config import ConnectionConfig
|
|
10
11
|
from neva.database.connection import ConnectionManager, TransactionContext
|
|
11
|
-
from neva.database.transaction import BoundTransaction
|
|
12
|
+
from neva.database.transaction import BoundTransaction
|
|
12
13
|
from neva.obs import LogManager
|
|
13
14
|
from neva.obs.instrumentation.sqlalchemy import instrument
|
|
15
|
+
from neva.support import Nothing, Option, Some, from_optional
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
@final
|
|
@@ -21,10 +23,9 @@ class DatabaseManager:
|
|
|
21
23
|
self._tx_context = tx_context
|
|
22
24
|
self._logger = logger
|
|
23
25
|
self._connections: dict[str, ConnectionManager] = {}
|
|
24
|
-
self._engines: dict[str, AsyncEngine] = {}
|
|
25
|
-
self._session_factories: dict[str, async_sessionmaker[AsyncSession]] = {}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
@deprecated("Please use DatabaseManager.register_connection instead.")
|
|
28
|
+
def register_engine(self, name: str, engine: asyncio.AsyncEngine) -> None:
|
|
28
29
|
"""Register an engine and create a session factory for a connection.
|
|
29
30
|
|
|
30
31
|
Args:
|
|
@@ -32,26 +33,34 @@ class DatabaseManager:
|
|
|
32
33
|
engine: The async engine.
|
|
33
34
|
"""
|
|
34
35
|
instrument(engine)
|
|
35
|
-
self.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
self._connections[name] = ConnectionManager(
|
|
37
|
+
name,
|
|
38
|
+
self._tx_context,
|
|
39
|
+
self._logger,
|
|
40
|
+
engine,
|
|
39
41
|
)
|
|
40
|
-
_ = self._connections.pop(name, None)
|
|
41
42
|
|
|
42
|
-
def
|
|
43
|
-
"""
|
|
44
|
-
|
|
43
|
+
def register_connection(self, name: str, config: ConnectionConfig) -> None:
|
|
44
|
+
"""Register a connection. This initializes the engine and session factory.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
name: The connection name.
|
|
48
|
+
config: The connection configuration.
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
engine = asyncio.create_async_engine(**config)
|
|
52
|
+
self._connections[name] = ConnectionManager(
|
|
45
53
|
name,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
self._logger,
|
|
50
|
-
self._session_factories.get(name),
|
|
51
|
-
),
|
|
54
|
+
self._tx_context,
|
|
55
|
+
self._logger,
|
|
56
|
+
engine,
|
|
52
57
|
)
|
|
53
58
|
|
|
54
|
-
def
|
|
59
|
+
def connection(self, name: str) -> Option[ConnectionManager]:
|
|
60
|
+
"""Returns a connection manager for the given name."""
|
|
61
|
+
return from_optional(self._connections.get(name))
|
|
62
|
+
|
|
63
|
+
def current(self, connection: str | None = None) -> Option[BoundTransaction]:
|
|
55
64
|
"""Returns the current transaction.
|
|
56
65
|
|
|
57
66
|
If no connection is specified, this will return the most recent transaction on
|
|
@@ -59,7 +68,7 @@ class DatabaseManager:
|
|
|
59
68
|
"""
|
|
60
69
|
return self._tx_context.current(connection)
|
|
61
70
|
|
|
62
|
-
def session(self, connection: str | None = None) -> Option[AsyncSession]:
|
|
71
|
+
def session(self, connection: str | None = None) -> Option[asyncio.AsyncSession]:
|
|
63
72
|
"""Returns the current session for a connection.
|
|
64
73
|
|
|
65
74
|
Only returns a session if the active transaction was opened by the
|
|
@@ -77,26 +86,11 @@ class DatabaseManager:
|
|
|
77
86
|
transaction was opened by a different asyncio task.
|
|
78
87
|
"""
|
|
79
88
|
match self.current(connection):
|
|
80
|
-
case Some(tx) if
|
|
81
|
-
isinstance(tx, BoundTransaction) and tx.is_accessible_from_current_task
|
|
82
|
-
):
|
|
89
|
+
case Some(tx) if tx.is_accessible_from_current_task:
|
|
83
90
|
return Some(tx.session)
|
|
84
91
|
case _:
|
|
85
92
|
return Nothing()
|
|
86
93
|
|
|
87
|
-
@asynccontextmanager
|
|
88
|
-
async def transaction(self, name: str = "default") -> AsyncIterator[Transaction]:
|
|
89
|
-
"""Open an unbound transaction on the named connection.
|
|
90
|
-
|
|
91
|
-
Intended for unit tests that exercise transaction lifecycle and callbacks
|
|
92
|
-
without a real database engine.
|
|
93
|
-
|
|
94
|
-
Yields:
|
|
95
|
-
Transaction: An unbound transaction with no associated session.
|
|
96
|
-
"""
|
|
97
|
-
async with self.connection(name).transaction() as tx:
|
|
98
|
-
yield tx
|
|
99
|
-
|
|
100
94
|
@asynccontextmanager
|
|
101
95
|
async def begin(self, name: str = "default") -> AsyncIterator[BoundTransaction]:
|
|
102
96
|
"""Open a bound transaction on the named connection.
|
|
@@ -109,14 +103,16 @@ class DatabaseManager:
|
|
|
109
103
|
|
|
110
104
|
Raises:
|
|
111
105
|
RuntimeError: If no engine has been registered for the connection.
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
"""
|
|
107
|
+
match self.connection(name):
|
|
108
|
+
case Nothing():
|
|
109
|
+
raise RuntimeError(f"No connection registered with name {name}")
|
|
110
|
+
case Some(conn):
|
|
111
|
+
async with conn.begin() as tx:
|
|
112
|
+
yield tx
|
|
115
113
|
|
|
116
114
|
async def close(self) -> None:
|
|
117
115
|
"""Dispose all engines and clear caches."""
|
|
118
|
-
for
|
|
119
|
-
await
|
|
120
|
-
self._engines.clear()
|
|
121
|
-
self._session_factories.clear()
|
|
116
|
+
for conn in self._connections.values():
|
|
117
|
+
await conn.dispose()
|
|
122
118
|
self._connections.clear()
|
|
@@ -4,14 +4,12 @@ from collections.abc import AsyncIterator
|
|
|
4
4
|
from contextlib import asynccontextmanager
|
|
5
5
|
from typing import Self, override
|
|
6
6
|
|
|
7
|
-
from sqlalchemy.ext.asyncio import create_async_engine
|
|
8
|
-
|
|
9
|
-
from neva import Err, Ok, Result
|
|
10
7
|
from neva.arch import ServiceProvider
|
|
11
8
|
from neva.database.config import ConnectionConfig, DatabaseConfig
|
|
12
9
|
from neva.database.connection import TransactionContext
|
|
13
10
|
from neva.database.manager import DatabaseManager
|
|
14
11
|
from neva.obs import LogManager
|
|
12
|
+
from neva.support import Err, Ok, Result
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
class DatabaseServiceProvider(ServiceProvider):
|
|
@@ -26,15 +24,14 @@ class DatabaseServiceProvider(ServiceProvider):
|
|
|
26
24
|
@asynccontextmanager
|
|
27
25
|
async def lifespan(self) -> AsyncIterator[None]:
|
|
28
26
|
"""Initialize and cleanup database connections."""
|
|
29
|
-
logger: LogManager = (await self.app.
|
|
30
|
-
db: DatabaseManager = (await self.app.
|
|
27
|
+
logger: LogManager = (await self.app.make_async(LogManager)).unwrap()
|
|
28
|
+
db: DatabaseManager = (await self.app.make_async(DatabaseManager)).unwrap()
|
|
31
29
|
logger.info("Beginning SQLAlchemy initialization...")
|
|
32
30
|
match self.app.config.get("database", type_=DatabaseConfig):
|
|
33
31
|
case Ok(config):
|
|
34
32
|
connections: dict[str, ConnectionConfig] = config["connections"]
|
|
35
33
|
for name, conn_config in connections.items():
|
|
36
|
-
|
|
37
|
-
db.register_engine(name, engine)
|
|
34
|
+
db.register_connection(name, conn_config)
|
|
38
35
|
logger.info(f"Registered engine for connection '{name}'.")
|
|
39
36
|
logger.info("SQLAlchemy initialization complete.")
|
|
40
37
|
yield
|
|
File without changes
|