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.
Files changed (140) hide show
  1. {python_neva-2.4.1 → python_neva-3.1.0}/.pre-commit-config.yaml +16 -0
  2. python_neva-3.1.0/CHANGELOG.md +42 -0
  3. {python_neva-2.4.1 → python_neva-3.1.0}/PKG-INFO +4 -2
  4. {python_neva-2.4.1 → python_neva-3.1.0}/neva/arch/__init__.py +0 -2
  5. {python_neva-2.4.1 → python_neva-3.1.0}/neva/arch/application.py +66 -35
  6. {python_neva-2.4.1 → python_neva-3.1.0}/neva/arch/facade.py +2 -2
  7. python_neva-3.1.0/neva/arch/integrations/__init__.py +1 -0
  8. {python_neva-2.4.1/neva/arch → python_neva-3.1.0/neva/arch/integrations}/faststream.py +3 -26
  9. {python_neva-2.4.1 → python_neva-3.1.0}/neva/arch/service_provider.py +27 -2
  10. {python_neva-2.4.1 → python_neva-3.1.0}/neva/config/base_providers.py +5 -5
  11. {python_neva-2.4.1 → python_neva-3.1.0}/neva/config/loader.py +1 -1
  12. python_neva-3.1.0/neva/config/py.typed +0 -0
  13. {python_neva-2.4.1 → python_neva-3.1.0}/neva/config/repository.py +9 -15
  14. {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/connection.py +33 -51
  15. {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/manager.py +41 -45
  16. {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/provider.py +9 -12
  17. python_neva-3.1.0/neva/database/py.typed +0 -0
  18. {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/transaction.py +3 -22
  19. {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/dispatcher.py +1 -1
  20. {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/listener.py +1 -1
  21. {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/provider.py +2 -2
  22. python_neva-3.1.0/neva/events/py.typed +0 -0
  23. {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/logging/provider.py +2 -2
  24. python_neva-3.1.0/neva/obs/py.typed +0 -0
  25. python_neva-3.1.0/neva/polyfactory/__init__.py +4 -0
  26. python_neva-3.1.0/neva/polyfactory/factories.py +20 -0
  27. python_neva-3.1.0/neva/polyfactory/persistence.py +22 -0
  28. python_neva-3.1.0/neva/polyfactory/py.typed +0 -0
  29. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/encryption/encrypter.py +2 -2
  30. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/encryption/protocol.py +1 -1
  31. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/hash_manager.py +11 -11
  32. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/provider.py +4 -3
  33. python_neva-3.1.0/neva/security/py.typed +0 -0
  34. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/app.pyi +1 -1
  35. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/config.pyi +1 -1
  36. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/crypt.pyi +1 -1
  37. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/db.pyi +17 -23
  38. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/event.pyi +2 -1
  39. python_neva-3.1.0/neva/support/py.typed +0 -0
  40. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/results.py +64 -51
  41. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/strategy.py +1 -1
  42. {python_neva-2.4.1 → python_neva-3.1.0}/neva/testing/fakes.py +1 -1
  43. {python_neva-2.4.1 → python_neva-3.1.0}/neva/testing/fixtures.py +1 -11
  44. python_neva-3.1.0/neva/testing/py.typed +0 -0
  45. {python_neva-2.4.1 → python_neva-3.1.0}/neva/testing/test_case.py +2 -3
  46. {python_neva-2.4.1 → python_neva-3.1.0}/pyproject.toml +40 -15
  47. {python_neva-2.4.1 → python_neva-3.1.0}/ruff.toml +2 -1
  48. python_neva-3.1.0/scripts/retag-with-changelog.sh +16 -0
  49. python_neva-3.1.0/tests/arch/test_scope.py +197 -0
  50. {python_neva-2.4.1 → python_neva-3.1.0}/tests/config/test_repository.py +4 -15
  51. python_neva-3.1.0/tests/conftest.py +45 -0
  52. python_neva-3.1.0/tests/database/test_connection_manager.py +50 -0
  53. python_neva-3.1.0/tests/database/test_database_manager.py +97 -0
  54. {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_edge_cases.py +11 -20
  55. {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_multi_connection.py +23 -18
  56. {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_sqlalchemy_integration.py +11 -25
  57. {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_transaction.py +1 -1
  58. {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_transaction_context.py +17 -41
  59. {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/test_transaction_registry.py +18 -10
  60. {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/conftest.py +0 -12
  61. {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_deferred.py +8 -8
  62. {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_dispatch.py +4 -4
  63. {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_event_registry.py +16 -6
  64. {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_function_listener.py +5 -5
  65. {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_immediate.py +2 -2
  66. {python_neva-2.4.1 → python_neva-3.1.0}/tests/security/test_encrypter.py +6 -5
  67. {python_neva-2.4.1 → python_neva-3.1.0}/tests/security/test_hash_manager.py +1 -1
  68. {python_neva-2.4.1 → python_neva-3.1.0}/tests/testing/test_fixtures.py +8 -8
  69. {python_neva-2.4.1 → python_neva-3.1.0}/tests/testing/test_refresh_database.py +14 -8
  70. {python_neva-2.4.1 → python_neva-3.1.0}/tests/testing/test_test_case.py +10 -10
  71. {python_neva-2.4.1 → python_neva-3.1.0}/uv.lock +241 -9
  72. python_neva-2.4.1/neva/__init__.py +0 -24
  73. python_neva-2.4.1/neva/arch/app.py +0 -129
  74. python_neva-2.4.1/neva/testing/http.py +0 -22
  75. python_neva-2.4.1/tests/arch/test_scope.py +0 -144
  76. python_neva-2.4.1/tests/conftest.py +0 -1
  77. python_neva-2.4.1/tests/database/conftest.py +0 -10
  78. python_neva-2.4.1/tests/database/test_connection_manager.py +0 -97
  79. python_neva-2.4.1/tests/database/test_database_manager.py +0 -98
  80. {python_neva-2.4.1 → python_neva-3.1.0}/.envrc +0 -0
  81. {python_neva-2.4.1 → python_neva-3.1.0}/.gitignore +0 -0
  82. {python_neva-2.4.1 → python_neva-3.1.0}/.python-version +0 -0
  83. {python_neva-2.4.1 → python_neva-3.1.0}/README.md +0 -0
  84. {python_neva-2.4.1 → python_neva-3.1.0}/neva/arch/config.py +0 -0
  85. {python_neva-2.4.1/neva → python_neva-3.1.0/neva/arch}/py.typed +0 -0
  86. {python_neva-2.4.1 → python_neva-3.1.0}/neva/config/__init__.py +0 -0
  87. {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/__init__.py +0 -0
  88. {python_neva-2.4.1 → python_neva-3.1.0}/neva/database/config.py +0 -0
  89. {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/__init__.py +0 -0
  90. {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/event.py +0 -0
  91. {python_neva-2.4.1 → python_neva-3.1.0}/neva/events/event_registry.py +0 -0
  92. {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/__init__.py +0 -0
  93. {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/instrumentation/__init__.py +0 -0
  94. {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
  95. {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/logging/__init__.py +0 -0
  96. {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/logging/manager.py +0 -0
  97. {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/middleware/__init__.py +0 -0
  98. {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/middleware/correlation.py +0 -0
  99. {python_neva-2.4.1 → python_neva-3.1.0}/neva/obs/middleware/profiler.py +0 -0
  100. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/__init__.py +0 -0
  101. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/encryption/__init__.py +0 -0
  102. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/__init__.py +0 -0
  103. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/config.py +0 -0
  104. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/hashers/__init__.py +0 -0
  105. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/hashers/argon2.py +0 -0
  106. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
  107. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/hashing/hashers/protocol.py +0 -0
  108. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/tokens/__init__.py +0 -0
  109. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/tokens/generate_token.py +0 -0
  110. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/tokens/hash_token.py +0 -0
  111. {python_neva-2.4.1 → python_neva-3.1.0}/neva/security/tokens/verify_token.py +0 -0
  112. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/__init__.py +0 -0
  113. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/accessors.py +0 -0
  114. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/__init__.py +0 -0
  115. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/app.py +0 -0
  116. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/config.py +0 -0
  117. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/crypt.py +0 -0
  118. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/db.py +0 -0
  119. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/event.py +0 -0
  120. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/hash.py +0 -0
  121. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/hash.pyi +0 -0
  122. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/log.py +0 -0
  123. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/facade/log.pyi +0 -0
  124. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/strconv.py +0 -0
  125. {python_neva-2.4.1 → python_neva-3.1.0}/neva/support/time.py +0 -0
  126. {python_neva-2.4.1 → python_neva-3.1.0}/neva/testing/__init__.py +0 -0
  127. {python_neva-2.4.1 → python_neva-3.1.0}/tests/__init__.py +0 -0
  128. {python_neva-2.4.1 → python_neva-3.1.0}/tests/arch/__init__.py +0 -0
  129. {python_neva-2.4.1 → python_neva-3.1.0}/tests/config/__init__.py +0 -0
  130. {python_neva-2.4.1 → python_neva-3.1.0}/tests/config/test_loader.py +0 -0
  131. {python_neva-2.4.1 → python_neva-3.1.0}/tests/database/__init__.py +0 -0
  132. {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/__init__.py +0 -0
  133. {python_neva-2.4.1 → python_neva-3.1.0}/tests/events/test_event.py +0 -0
  134. {python_neva-2.4.1 → python_neva-3.1.0}/tests/obs/__init__.py +0 -0
  135. {python_neva-2.4.1 → python_neva-3.1.0}/tests/obs/test_correlation.py +0 -0
  136. {python_neva-2.4.1 → python_neva-3.1.0}/tests/obs/test_profiler.py +0 -0
  137. {python_neva-2.4.1 → python_neva-3.1.0}/tests/security/__init__.py +0 -0
  138. {python_neva-2.4.1 → python_neva-3.1.0}/tests/testing/__init__.py +0 -0
  139. {python_neva-2.4.1 → python_neva-3.1.0}/tests/testing/test_event_fake.py +0 -0
  140. {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: 2.4.1
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.7.2
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.abc import AsyncIterator, Iterator
5
- from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
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.integrations.fastapi import FastapiProvider
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: dict[type, ServiceProvider] = {}
36
- self.di_provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
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.bind(lambda: self.config, interface=ConfigRepository)
61
+ _ = self.root_provider.provide(
62
+ lambda: self.config, provides=ConfigRepository
63
+ )
54
64
 
55
- providers_from_file: list[type[ServiceProvider]] = self.config.get(
56
- "providers.providers", type_=list[type[ServiceProvider]]
57
- ).unwrap_or([])
58
- providers_from_app = self.config.get(
59
- "app.providers", type_=list[type[ServiceProvider]]
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
- self.register_providers(base_providers().union(providers))
65
- self.bind(source=lambda: self, interface=Application)
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.container: dishka.Container = dishka.make_container(self.di_provider)
68
-
69
- def bind_to_fastapi(self) -> None:
70
- """Setup the FastapiProvider for FastAPI integration."""
71
- self.container = dishka.make_container(self.di_provider, FastapiProvider())
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: set[type[ServiceProvider]]) -> None:
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.di_provider.provide(
118
+ _ = self.root_provider.provide(
102
119
  source=source,
103
120
  scope=scope,
104
121
  provides=interface,
105
122
  )
106
123
 
107
- def make[T](self, interface: type[T]) -> Result[T, str]:
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(self.container.get(interface))
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
- @contextmanager
119
- def scope(self, scope: dishka.BaseScope | None = None) -> Iterator[Self]:
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 self.container(scope=scope) as container:
127
- self.container = container
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
- self.container = parent
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.make(EventDispatcher)
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, Callable
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(app: faststream.FastStream) -> AsyncIterator[None]:
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 TYPE_CHECKING, Any, ClassVar, Protocol, Self, runtime_checkable
12
+ from typing import (
13
+ TYPE_CHECKING,
14
+ Any,
15
+ Callable,
16
+ ClassVar,
17
+ Protocol,
18
+ Self,
19
+ runtime_checkable,
20
+ )
13
21
 
14
- from neva import Result
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() -> set[type[ServiceProvider]]:
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
+ ]
@@ -10,7 +10,7 @@ from pathlib import Path
10
10
  from types import ModuleType
11
11
  from typing import Any
12
12
 
13
- from neva import Err, Ok, Option, Result
13
+ from neva.support import Err, Ok, Option, Result
14
14
  from neva.support.accessors import get_attr
15
15
 
16
16
 
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
- try:
86
- for k in keys:
87
- if not isinstance(current, dict):
88
- if default is not None:
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
- return Ok(current) # type: ignore [arg-type]
93
- except KeyError:
94
- if default is not None:
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.asyncio import AsyncSession, async_sessionmaker
9
+ from sqlalchemy.ext import asyncio
10
10
 
11
- from neva import Nothing, Option, Some, from_optional
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, Transaction] = field(default_factory=dict)
21
- stack: list[Transaction] = field(default_factory=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: Transaction) -> "TransactionRegistry":
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[Transaction]:
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
- session_factory: async_sessionmaker[AsyncSession] | None = None,
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.session_factory = session_factory
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: Transaction) -> AsyncIterator[None]:
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
- isinstance(parent_tx, BoundTransaction)
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(bound):
180
- yield bound
181
- if bound.rollback_requested:
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
- bound = tx.begin(session)
174
+ tx = BoundTransaction(self.name, session)
193
175
  try:
194
- async with self._scoped(bound):
195
- yield bound
196
- if bound.rollback_requested:
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()