python-neva 2.4.1__tar.gz → 3.1.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-2.4.1 → python_neva-3.1.0}/.pre-commit-config.yaml +16 -0
- python_neva-3.1.0/CHANGELOG.md +42 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/PKG-INFO +4 -2
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/arch/__init__.py +0 -2
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/arch/application.py +66 -35
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/arch/facade.py +2 -2
- python_neva-3.1.0/neva/arch/integrations/__init__.py +1 -0
- {python_neva-2.4.1/neva/arch → python_neva-3.1.0/neva/arch/integrations}/faststream.py +3 -26
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/arch/service_provider.py +27 -2
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/config/base_providers.py +5 -5
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/config/loader.py +1 -1
- python_neva-3.1.0/neva/config/py.typed +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/config/repository.py +9 -15
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/connection.py +33 -51
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/manager.py +41 -45
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/provider.py +9 -12
- python_neva-3.1.0/neva/database/py.typed +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/transaction.py +3 -22
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/dispatcher.py +1 -1
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/listener.py +1 -1
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/provider.py +2 -2
- python_neva-3.1.0/neva/events/py.typed +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/logging/provider.py +2 -2
- python_neva-3.1.0/neva/obs/py.typed +0 -0
- python_neva-3.1.0/neva/polyfactory/__init__.py +4 -0
- python_neva-3.1.0/neva/polyfactory/factories.py +20 -0
- python_neva-3.1.0/neva/polyfactory/persistence.py +22 -0
- python_neva-3.1.0/neva/polyfactory/py.typed +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/encryption/encrypter.py +2 -2
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/encryption/protocol.py +1 -1
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/hash_manager.py +11 -11
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/provider.py +4 -3
- python_neva-3.1.0/neva/security/py.typed +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/app.pyi +1 -1
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/config.pyi +1 -1
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/crypt.pyi +1 -1
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/db.pyi +17 -23
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/event.pyi +2 -1
- python_neva-3.1.0/neva/support/py.typed +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/results.py +64 -51
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/strategy.py +1 -1
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/testing/fakes.py +1 -1
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/testing/fixtures.py +1 -11
- python_neva-3.1.0/neva/testing/py.typed +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/testing/test_case.py +2 -3
- {python_neva-2.4.1 → python_neva-3.1.0}/pyproject.toml +40 -15
- {python_neva-2.4.1 → python_neva-3.1.0}/ruff.toml +2 -1
- python_neva-3.1.0/scripts/retag-with-changelog.sh +16 -0
- python_neva-3.1.0/tests/arch/test_scope.py +197 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/config/test_repository.py +4 -15
- python_neva-3.1.0/tests/conftest.py +45 -0
- python_neva-3.1.0/tests/database/test_connection_manager.py +50 -0
- python_neva-3.1.0/tests/database/test_database_manager.py +97 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_edge_cases.py +11 -20
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_multi_connection.py +23 -18
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_sqlalchemy_integration.py +11 -25
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_transaction.py +1 -1
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_transaction_context.py +17 -41
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_transaction_registry.py +18 -10
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/conftest.py +0 -12
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_deferred.py +8 -8
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_dispatch.py +4 -4
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_event_registry.py +16 -6
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_function_listener.py +5 -5
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_immediate.py +2 -2
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/security/test_encrypter.py +6 -5
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/security/test_hash_manager.py +1 -1
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/testing/test_fixtures.py +8 -8
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/testing/test_refresh_database.py +14 -8
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/testing/test_test_case.py +10 -10
- {python_neva-2.4.1 → python_neva-3.1.0}/uv.lock +241 -9
- python_neva-2.4.1/neva/__init__.py +0 -24
- python_neva-2.4.1/neva/arch/app.py +0 -129
- python_neva-2.4.1/neva/testing/http.py +0 -22
- python_neva-2.4.1/tests/arch/test_scope.py +0 -144
- python_neva-2.4.1/tests/conftest.py +0 -1
- python_neva-2.4.1/tests/database/conftest.py +0 -10
- python_neva-2.4.1/tests/database/test_connection_manager.py +0 -97
- python_neva-2.4.1/tests/database/test_database_manager.py +0 -98
- {python_neva-2.4.1 → python_neva-3.1.0}/.envrc +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/.gitignore +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/.python-version +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/README.md +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/arch/config.py +0 -0
- {python_neva-2.4.1/neva → python_neva-3.1.0/neva/arch}/py.typed +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/config/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/config.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/event.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/event_registry.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/instrumentation/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/logging/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/logging/manager.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/middleware/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/middleware/correlation.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/middleware/profiler.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/encryption/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/config.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/tokens/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/tokens/generate_token.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/tokens/hash_token.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/tokens/verify_token.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/accessors.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/app.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/config.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/crypt.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/db.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/event.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/hash.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/hash.pyi +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/log.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/log.pyi +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/strconv.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/time.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/neva/testing/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/arch/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/config/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/config/test_loader.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_event.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/obs/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/obs/test_correlation.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/obs/test_profiler.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/security/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/testing/__init__.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/testing/test_event_fake.py +0 -0
- {python_neva-2.4.1 → python_neva-3.1.0}/tests/testing/test_facade_restore.py +0 -0
|
@@ -27,3 +27,19 @@ repos:
|
|
|
27
27
|
hooks:
|
|
28
28
|
- id: mypy
|
|
29
29
|
args: [--enable-incomplete-feature=TypeForm]
|
|
30
|
+
additional_dependencies:
|
|
31
|
+
[
|
|
32
|
+
cryptography>=46.0.3,
|
|
33
|
+
dishka>=1.10.0,
|
|
34
|
+
"fastapi[all]>=0.129.0",
|
|
35
|
+
faststream>=0.6.6,
|
|
36
|
+
"pwdlib[argon2,bcrypt]>=0.3.0",
|
|
37
|
+
pyinstrument>=5.1.1,
|
|
38
|
+
pytest>=9.0.2,
|
|
39
|
+
structlog>=25.5.0,
|
|
40
|
+
"sqlalchemy[asyncio]>=2.0.0",
|
|
41
|
+
asyncpg>=0.30.0,
|
|
42
|
+
aiosqlite>=0.20.0,
|
|
43
|
+
typer>=0.21.1,
|
|
44
|
+
opentelemetry-instrumentation-sqlalchemy>=0.62b0,
|
|
45
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
## 3.1.0 (2026-05-11)
|
|
2
|
+
|
|
3
|
+
### ✨ Features
|
|
4
|
+
|
|
5
|
+
- fixing db facade
|
|
6
|
+
- Add eq method to Some / Ok
|
|
7
|
+
- Deprecate register_engine, add register_connection
|
|
8
|
+
- RefreshDatabase now properly use the testcase inner app
|
|
9
|
+
- Add neva-fastapi as optional dependency
|
|
10
|
+
- renaming of make / make_async for retrocompatibility
|
|
11
|
+
|
|
12
|
+
### 🐛🚑️ Fixes
|
|
13
|
+
|
|
14
|
+
- Fix provider registration ordering
|
|
15
|
+
|
|
16
|
+
### ♻️ Refactorings
|
|
17
|
+
|
|
18
|
+
- refactor of results / db toolkit
|
|
19
|
+
|
|
20
|
+
## 3.1.0a1 (2026-05-05)
|
|
21
|
+
|
|
22
|
+
### build
|
|
23
|
+
|
|
24
|
+
- add commitizen + versioningit
|
|
25
|
+
|
|
26
|
+
### 💚👷 CI & Build
|
|
27
|
+
|
|
28
|
+
- update perms on tag script
|
|
29
|
+
- tweaking cz tags
|
|
30
|
+
- remove auto-annotated tags
|
|
31
|
+
- configure versioningit + cz
|
|
32
|
+
- Fix cz config >>> ⏰ 1
|
|
33
|
+
- add and configure cz_gitmoji >>> ⏰ 5m
|
|
34
|
+
|
|
35
|
+
### 📝💡 Documentation
|
|
36
|
+
|
|
37
|
+
- clear changelog
|
|
38
|
+
|
|
39
|
+
### 🔧🔨📦️ Configuration, Scripts, Packages
|
|
40
|
+
|
|
41
|
+
- improve auto-annotation of tags
|
|
42
|
+
- Fix tagging script
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-neva
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: aiosqlite>=0.20.0
|
|
7
7
|
Requires-Dist: asyncpg>=0.30.0
|
|
8
8
|
Requires-Dist: cryptography>=46.0.3
|
|
9
|
-
Requires-Dist: dishka>=1.
|
|
9
|
+
Requires-Dist: dishka>=1.10.0
|
|
10
10
|
Requires-Dist: fastapi[all]>=0.129.0
|
|
11
11
|
Requires-Dist: faststream>=0.6.6
|
|
12
12
|
Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.62b0
|
|
@@ -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>=0.1.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'
|
|
@@ -4,7 +4,6 @@ This module contains the main application class, service provider pattern,
|
|
|
4
4
|
and facade implementations.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from neva.arch.app import App
|
|
8
7
|
from neva.arch.application import Application
|
|
9
8
|
from neva.arch.facade import Facade
|
|
10
9
|
from neva.arch.service_provider import (
|
|
@@ -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,18 +1,26 @@
|
|
|
1
1
|
"""Base application for DI and facade injection."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from collections
|
|
5
|
-
from
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from collections.abc import AsyncIterator, Sequence
|
|
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
|
|
|
9
11
|
import dishka
|
|
10
|
-
from dishka.
|
|
12
|
+
from dishka.provider import BaseProvider
|
|
13
|
+
from typing_extensions import deprecated
|
|
11
14
|
|
|
12
|
-
from neva import Err, Ok, Result
|
|
13
15
|
from neva.arch.facade import Facade
|
|
14
16
|
from neva.arch.service_provider import Bootable, ServiceProvider
|
|
15
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
|
+
)
|
|
16
24
|
|
|
17
25
|
|
|
18
26
|
class Application:
|
|
@@ -32,8 +40,8 @@ class Application:
|
|
|
32
40
|
from neva.config.repository import ConfigRepository
|
|
33
41
|
|
|
34
42
|
self.config: ConfigRepository = ConfigRepository()
|
|
35
|
-
self.providers:
|
|
36
|
-
self.
|
|
43
|
+
self.providers: OrderedDict[type, ServiceProvider] = OrderedDict()
|
|
44
|
+
self.root_provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
|
|
37
45
|
|
|
38
46
|
configuration_path = (
|
|
39
47
|
config_path
|
|
@@ -50,25 +58,33 @@ class Application:
|
|
|
50
58
|
):
|
|
51
59
|
raise RuntimeError("Failed to register config")
|
|
52
60
|
|
|
53
|
-
self.
|
|
61
|
+
_ = self.root_provider.provide(
|
|
62
|
+
lambda: self.config, provides=ConfigRepository
|
|
63
|
+
)
|
|
54
64
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
).unwrap_or([])
|
|
61
|
-
providers: set[type[ServiceProvider]] = set(providers_from_file).union(
|
|
62
|
-
set(providers_from_app)
|
|
65
|
+
providers: list[type[ServiceProvider]] = base_providers()
|
|
66
|
+
providers.extend(
|
|
67
|
+
self.config.get(
|
|
68
|
+
"providers.providers", type_=list[type[ServiceProvider]]
|
|
69
|
+
).unwrap_or([])
|
|
63
70
|
)
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
providers.extend(
|
|
72
|
+
self.config.get(
|
|
73
|
+
"app.providers", type_=list[type[ServiceProvider]]
|
|
74
|
+
).unwrap_or([])
|
|
75
|
+
)
|
|
76
|
+
_ = self.root_provider.provide(source=lambda: self, provides=Application)
|
|
77
|
+
self.register_providers(providers)
|
|
66
78
|
self._bind_event_listeners()
|
|
67
|
-
self.
|
|
68
|
-
|
|
69
|
-
def
|
|
70
|
-
"""
|
|
71
|
-
self.container = dishka.
|
|
79
|
+
self.build_container(self.root_provider)
|
|
80
|
+
|
|
81
|
+
def build_container(self, *providers: BaseProvider) -> None:
|
|
82
|
+
"""Build the application container."""
|
|
83
|
+
self.container: dishka.AsyncContainer = dishka.make_async_container(
|
|
84
|
+
self.root_provider,
|
|
85
|
+
*[p.provider for p in self.providers.values()],
|
|
86
|
+
*providers,
|
|
87
|
+
)
|
|
72
88
|
|
|
73
89
|
def register(self, provider: type[ServiceProvider]) -> Result[ServiceProvider, str]:
|
|
74
90
|
"""Registers a service provider with the application.
|
|
@@ -85,11 +101,12 @@ class Application:
|
|
|
85
101
|
.map(lambda p: self.providers.setdefault(provider, p))
|
|
86
102
|
)
|
|
87
103
|
|
|
88
|
-
def register_providers(self, providers:
|
|
104
|
+
def register_providers(self, providers: Sequence[type[ServiceProvider]]) -> None:
|
|
89
105
|
"""Registers a set of providers."""
|
|
90
106
|
for provider in providers:
|
|
91
107
|
_ = self.register(provider)
|
|
92
108
|
|
|
109
|
+
@deprecated("Use the new bind() method directly with a Service Provider.")
|
|
93
110
|
def bind(
|
|
94
111
|
self,
|
|
95
112
|
source: type | Callable[..., Any],
|
|
@@ -98,37 +115,50 @@ class Application:
|
|
|
98
115
|
scope: dishka.BaseScope | None = None,
|
|
99
116
|
) -> None:
|
|
100
117
|
"""Binds a source to the container."""
|
|
101
|
-
_ = self.
|
|
118
|
+
_ = self.root_provider.provide(
|
|
102
119
|
source=source,
|
|
103
120
|
scope=scope,
|
|
104
121
|
provides=interface,
|
|
105
122
|
)
|
|
106
123
|
|
|
107
|
-
def
|
|
124
|
+
async def make_async[T](self, interface: type[T]) -> Result[T, str]:
|
|
108
125
|
"""Resolve and instanciate a type from the container.
|
|
109
126
|
|
|
110
127
|
Returns:
|
|
111
128
|
Result containing the resolved type instance or an error message.
|
|
112
129
|
"""
|
|
130
|
+
container = _current_container.get(self.container)
|
|
131
|
+
try:
|
|
132
|
+
return Ok(await container.get(interface))
|
|
133
|
+
except Exception as e:
|
|
134
|
+
return Err(f"Failed to resolve service '{interface.__name__}': {e}")
|
|
135
|
+
|
|
136
|
+
def make[T](self, interface: type[T]) -> Result[T, str]:
|
|
137
|
+
"""Synchronous version of make.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Result containing the resolved type instance or an error message.
|
|
141
|
+
"""
|
|
142
|
+
container = _current_container.get(self.container)
|
|
113
143
|
try:
|
|
114
|
-
return Ok(
|
|
144
|
+
return Ok(container.get_sync(interface))
|
|
115
145
|
except Exception as e:
|
|
116
146
|
return Err(f"Failed to resolve service '{interface.__name__}': {e}")
|
|
117
147
|
|
|
118
|
-
@
|
|
119
|
-
def scope(self, scope: dishka.BaseScope | None = None) ->
|
|
148
|
+
@asynccontextmanager
|
|
149
|
+
async def scope(self, scope: dishka.BaseScope | None = None) -> AsyncIterator[Self]:
|
|
120
150
|
"""Enter a new scope.
|
|
121
151
|
|
|
122
152
|
Yields:
|
|
123
153
|
The application instance with the new scope.
|
|
124
154
|
"""
|
|
125
|
-
parent = self.container
|
|
126
|
-
with
|
|
127
|
-
|
|
155
|
+
parent = _current_container.get(self.container)
|
|
156
|
+
async with parent(scope=scope) as container:
|
|
157
|
+
token = _current_container.set(container)
|
|
128
158
|
try:
|
|
129
159
|
yield self
|
|
130
160
|
finally:
|
|
131
|
-
|
|
161
|
+
_current_container.reset(token)
|
|
132
162
|
|
|
133
163
|
@asynccontextmanager
|
|
134
164
|
async def lifespan(self) -> AsyncIterator[None]:
|
|
@@ -140,9 +170,10 @@ class Application:
|
|
|
140
170
|
if isinstance(provider, Bootable):
|
|
141
171
|
await stack.enter_async_context(provider.lifespan())
|
|
142
172
|
|
|
143
|
-
self._wire_event_listeners()
|
|
173
|
+
await self._wire_event_listeners()
|
|
144
174
|
yield
|
|
145
175
|
|
|
176
|
+
await self.container.close()
|
|
146
177
|
Facade.reset_facade_application()
|
|
147
178
|
|
|
148
179
|
def _bind_event_listeners(self) -> None:
|
|
@@ -152,11 +183,11 @@ class Application:
|
|
|
152
183
|
for listener_cls in listeners:
|
|
153
184
|
self.bind(listener_cls)
|
|
154
185
|
|
|
155
|
-
def _wire_event_listeners(self) -> None:
|
|
186
|
+
async def _wire_event_listeners(self) -> None:
|
|
156
187
|
"""Wire event-listener mappings from all providers onto the dispatcher."""
|
|
157
188
|
from neva.events.dispatcher import EventDispatcher
|
|
158
189
|
|
|
159
|
-
result = self.
|
|
190
|
+
result = await self.make_async(EventDispatcher)
|
|
160
191
|
if result.is_err:
|
|
161
192
|
return
|
|
162
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
|
|
|
@@ -162,7 +162,7 @@ class FacadeMeta(ABCMeta):
|
|
|
162
162
|
|
|
163
163
|
def restore(cls) -> None:
|
|
164
164
|
"""Restore the facade to its real service, removing any fake or spy."""
|
|
165
|
-
FacadeMeta._fake_instances.pop(cls, None)
|
|
165
|
+
_ = FacadeMeta._fake_instances.pop(cls, None)
|
|
166
166
|
|
|
167
167
|
@contextmanager
|
|
168
168
|
def faking(cls) -> Iterator[AsyncMock]:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Integrations module providing integrations with external frameworks."""
|
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import AsyncGenerator, AsyncIterator
|
|
4
4
|
from contextlib import asynccontextmanager
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
|
-
import dishka
|
|
8
7
|
import faststream
|
|
9
8
|
from faststream._internal.broker import BrokerUsecase
|
|
10
9
|
from faststream._internal.configs import BrokerConfig
|
|
11
10
|
from starlette.types import StatelessLifespan
|
|
12
11
|
|
|
13
|
-
from neva import Result
|
|
14
12
|
from neva.arch import Application, ServiceProvider
|
|
13
|
+
from neva.support import Result
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class FastStream(faststream.FastStream):
|
|
@@ -37,28 +36,6 @@ class FastStream(faststream.FastStream):
|
|
|
37
36
|
"""
|
|
38
37
|
return self.application.register(provider=provider)
|
|
39
38
|
|
|
40
|
-
def bind(
|
|
41
|
-
self,
|
|
42
|
-
source: type | Callable[..., Any],
|
|
43
|
-
*,
|
|
44
|
-
interface: type | None = None,
|
|
45
|
-
scope: dishka.BaseScope | None = None,
|
|
46
|
-
) -> None:
|
|
47
|
-
"""Binds a source to the container."""
|
|
48
|
-
self.application.bind(
|
|
49
|
-
source=source,
|
|
50
|
-
interface=interface,
|
|
51
|
-
scope=scope,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
def make[T](self, interface: type[T]) -> Result[T, str]:
|
|
55
|
-
"""Resolve and instanciate a type from the container.
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
Result containing the resolved type instance or an error message.
|
|
59
|
-
"""
|
|
60
|
-
return self.application.make(interface=interface)
|
|
61
|
-
|
|
62
39
|
@asynccontextmanager
|
|
63
40
|
async def lifespan(self) -> AsyncGenerator[None, None]:
|
|
64
41
|
"""Async context manager for the application lifespan."""
|
|
@@ -69,7 +46,7 @@ class FastStream(faststream.FastStream):
|
|
|
69
46
|
self,
|
|
70
47
|
) -> StatelessLifespan["FastStream"]:
|
|
71
48
|
@asynccontextmanager
|
|
72
|
-
async def composed_lifespan(
|
|
49
|
+
async def composed_lifespan(_: faststream.FastStream) -> AsyncIterator[None]:
|
|
73
50
|
async with self.lifespan():
|
|
74
51
|
yield
|
|
75
52
|
|
|
@@ -9,9 +9,19 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import abc
|
|
11
11
|
from contextlib import AbstractAsyncContextManager
|
|
12
|
-
from typing import
|
|
12
|
+
from typing import (
|
|
13
|
+
TYPE_CHECKING,
|
|
14
|
+
Any,
|
|
15
|
+
Callable,
|
|
16
|
+
ClassVar,
|
|
17
|
+
Protocol,
|
|
18
|
+
Self,
|
|
19
|
+
runtime_checkable,
|
|
20
|
+
)
|
|
13
21
|
|
|
14
|
-
|
|
22
|
+
import dishka
|
|
23
|
+
|
|
24
|
+
from neva.support import Result
|
|
15
25
|
|
|
16
26
|
|
|
17
27
|
if TYPE_CHECKING:
|
|
@@ -67,8 +77,23 @@ class ServiceProvider(abc.ABC):
|
|
|
67
77
|
app: The application instance.
|
|
68
78
|
|
|
69
79
|
"""
|
|
80
|
+
self.provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
|
|
70
81
|
self.app = app
|
|
71
82
|
|
|
83
|
+
def bind(
|
|
84
|
+
self,
|
|
85
|
+
source: type | Callable[..., Any],
|
|
86
|
+
*,
|
|
87
|
+
interface: type | None = None,
|
|
88
|
+
scope: dishka.BaseScope | None = None,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Binds a source to the container."""
|
|
91
|
+
_ = self.provider.provide(
|
|
92
|
+
source=source,
|
|
93
|
+
scope=scope,
|
|
94
|
+
provides=interface,
|
|
95
|
+
)
|
|
96
|
+
|
|
72
97
|
@abc.abstractmethod
|
|
73
98
|
def register(self) -> Result[Self, str]:
|
|
74
99
|
"""Register services into the application container.
|
|
@@ -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
|
|
@@ -5,11 +5,11 @@ store for all application configuration values. It supports dot notation for
|
|
|
5
5
|
nested access and can be frozen to prevent modifications after initialization.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any, cast
|
|
9
9
|
|
|
10
10
|
from typing_extensions import TypeForm
|
|
11
11
|
|
|
12
|
-
from neva import Err, Ok, Result
|
|
12
|
+
from neva.support import Err, Ok, Result
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class ConfigRepository:
|
|
@@ -65,14 +65,12 @@ class ConfigRepository:
|
|
|
65
65
|
def get[T](
|
|
66
66
|
self,
|
|
67
67
|
key: str,
|
|
68
|
-
default: T | None = None,
|
|
69
68
|
type_: TypeForm[T] | None = None,
|
|
70
69
|
) -> Result[T, str]:
|
|
71
70
|
"""Get a configuration value using dot notation.
|
|
72
71
|
|
|
73
72
|
Args:
|
|
74
73
|
key: Dot-notated key path (e.g., "database.host").
|
|
75
|
-
default: Default value to return if key is not found.
|
|
76
74
|
type_: Type to cast the value to.
|
|
77
75
|
|
|
78
76
|
Returns:
|
|
@@ -82,18 +80,14 @@ class ConfigRepository:
|
|
|
82
80
|
keys = key.split(".")
|
|
83
81
|
current = self._items
|
|
84
82
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return Ok(default)
|
|
90
|
-
return Err(f"Config key '{key}' not found")
|
|
83
|
+
for k in keys:
|
|
84
|
+
if not isinstance(current, dict):
|
|
85
|
+
return Err(f"Config key '{key}' not found")
|
|
86
|
+
try:
|
|
91
87
|
current = current[k]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return Ok(default)
|
|
96
|
-
return Err(f"Config key '{key}' not found")
|
|
88
|
+
except KeyError:
|
|
89
|
+
return Err(f"Config key '{key}' not found")
|
|
90
|
+
return Ok(cast(T, current))
|
|
97
91
|
|
|
98
92
|
def has(self, key: str) -> bool:
|
|
99
93
|
"""Check if a configuration key exists.
|
|
@@ -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()
|