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.
Files changed (139) hide show
  1. python_neva-3.1.1/CHANGELOG.md +48 -0
  2. {python_neva-3.0.0 → python_neva-3.1.1}/PKG-INFO +3 -1
  3. {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/__init__.py +0 -2
  4. {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/application.py +33 -22
  5. {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/facade.py +3 -3
  6. {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/integrations/faststream.py +1 -1
  7. {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/service_provider.py +1 -1
  8. {python_neva-3.0.0 → python_neva-3.1.1}/neva/config/base_providers.py +5 -5
  9. {python_neva-3.0.0 → python_neva-3.1.1}/neva/config/loader.py +1 -1
  10. python_neva-3.1.1/neva/config/py.typed +0 -0
  11. {python_neva-3.0.0 → python_neva-3.1.1}/neva/config/repository.py +1 -1
  12. {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/connection.py +33 -51
  13. {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/manager.py +41 -45
  14. {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/provider.py +4 -7
  15. python_neva-3.1.1/neva/database/py.typed +0 -0
  16. {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/transaction.py +3 -22
  17. {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/dispatcher.py +2 -2
  18. {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/listener.py +1 -1
  19. {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/provider.py +1 -1
  20. python_neva-3.1.1/neva/events/py.typed +0 -0
  21. {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/logging/provider.py +1 -1
  22. python_neva-3.1.1/neva/obs/py.typed +0 -0
  23. python_neva-3.1.1/neva/polyfactory/__init__.py +4 -0
  24. python_neva-3.1.1/neva/polyfactory/factories.py +20 -0
  25. python_neva-3.1.1/neva/polyfactory/persistence.py +22 -0
  26. python_neva-3.1.1/neva/polyfactory/py.typed +0 -0
  27. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/encryption/encrypter.py +1 -1
  28. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/encryption/protocol.py +1 -1
  29. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/hash_manager.py +1 -1
  30. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/provider.py +2 -1
  31. python_neva-3.1.1/neva/security/py.typed +0 -0
  32. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/app.pyi +1 -1
  33. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/config.pyi +1 -1
  34. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/crypt.pyi +1 -1
  35. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/db.pyi +17 -23
  36. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/event.pyi +2 -1
  37. python_neva-3.1.1/neva/support/py.typed +0 -0
  38. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/results.py +64 -51
  39. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/strategy.py +1 -1
  40. {python_neva-3.0.0 → python_neva-3.1.1}/neva/testing/fakes.py +1 -1
  41. {python_neva-3.0.0 → python_neva-3.1.1}/neva/testing/fixtures.py +1 -11
  42. python_neva-3.1.1/neva/testing/py.typed +0 -0
  43. {python_neva-3.0.0 → python_neva-3.1.1}/neva/testing/test_case.py +2 -3
  44. {python_neva-3.0.0 → python_neva-3.1.1}/pyproject.toml +37 -13
  45. {python_neva-3.0.0 → python_neva-3.1.1}/ruff.toml +2 -1
  46. python_neva-3.1.1/scripts/retag-with-changelog.sh +16 -0
  47. {python_neva-3.0.0 → python_neva-3.1.1}/tests/arch/test_scope.py +74 -21
  48. python_neva-3.1.1/tests/conftest.py +45 -0
  49. python_neva-3.1.1/tests/database/test_connection_manager.py +50 -0
  50. python_neva-3.1.1/tests/database/test_database_manager.py +97 -0
  51. {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_edge_cases.py +11 -20
  52. {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_multi_connection.py +23 -18
  53. {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_sqlalchemy_integration.py +11 -25
  54. {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_transaction.py +1 -1
  55. {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_transaction_context.py +17 -41
  56. {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/test_transaction_registry.py +18 -10
  57. {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/conftest.py +0 -12
  58. {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_deferred.py +8 -8
  59. {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_dispatch.py +2 -2
  60. {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_event_registry.py +16 -6
  61. {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_function_listener.py +5 -5
  62. {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_immediate.py +2 -2
  63. {python_neva-3.0.0 → python_neva-3.1.1}/tests/security/test_encrypter.py +3 -3
  64. {python_neva-3.0.0 → python_neva-3.1.1}/tests/security/test_hash_manager.py +1 -1
  65. {python_neva-3.0.0 → python_neva-3.1.1}/tests/testing/test_fixtures.py +6 -6
  66. {python_neva-3.0.0 → python_neva-3.1.1}/tests/testing/test_refresh_database.py +14 -8
  67. {python_neva-3.0.0 → python_neva-3.1.1}/tests/testing/test_test_case.py +7 -7
  68. {python_neva-3.0.0 → python_neva-3.1.1}/uv.lock +237 -5
  69. python_neva-3.0.0/neva/__init__.py +0 -24
  70. python_neva-3.0.0/neva/arch/integrations/fastapi.py +0 -112
  71. python_neva-3.0.0/neva/testing/http.py +0 -22
  72. python_neva-3.0.0/tests/conftest.py +0 -1
  73. python_neva-3.0.0/tests/database/conftest.py +0 -10
  74. python_neva-3.0.0/tests/database/test_connection_manager.py +0 -97
  75. python_neva-3.0.0/tests/database/test_database_manager.py +0 -98
  76. {python_neva-3.0.0 → python_neva-3.1.1}/.envrc +0 -0
  77. {python_neva-3.0.0 → python_neva-3.1.1}/.gitignore +0 -0
  78. {python_neva-3.0.0 → python_neva-3.1.1}/.pre-commit-config.yaml +0 -0
  79. {python_neva-3.0.0 → python_neva-3.1.1}/.python-version +0 -0
  80. {python_neva-3.0.0 → python_neva-3.1.1}/README.md +0 -0
  81. {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/config.py +0 -0
  82. {python_neva-3.0.0 → python_neva-3.1.1}/neva/arch/integrations/__init__.py +0 -0
  83. {python_neva-3.0.0/neva → python_neva-3.1.1/neva/arch}/py.typed +0 -0
  84. {python_neva-3.0.0 → python_neva-3.1.1}/neva/config/__init__.py +0 -0
  85. {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/__init__.py +0 -0
  86. {python_neva-3.0.0 → python_neva-3.1.1}/neva/database/config.py +0 -0
  87. {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/__init__.py +0 -0
  88. {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/event.py +0 -0
  89. {python_neva-3.0.0 → python_neva-3.1.1}/neva/events/event_registry.py +0 -0
  90. {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/__init__.py +0 -0
  91. {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/instrumentation/__init__.py +0 -0
  92. {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/instrumentation/sqlalchemy.py +0 -0
  93. {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/logging/__init__.py +0 -0
  94. {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/logging/manager.py +0 -0
  95. {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/middleware/__init__.py +0 -0
  96. {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/middleware/correlation.py +0 -0
  97. {python_neva-3.0.0 → python_neva-3.1.1}/neva/obs/middleware/profiler.py +0 -0
  98. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/__init__.py +0 -0
  99. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/encryption/__init__.py +0 -0
  100. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/__init__.py +0 -0
  101. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/config.py +0 -0
  102. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/hashers/__init__.py +0 -0
  103. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/hashers/argon2.py +0 -0
  104. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/hashers/bcrypt.py +0 -0
  105. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/hashing/hashers/protocol.py +0 -0
  106. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/tokens/__init__.py +0 -0
  107. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/tokens/generate_token.py +0 -0
  108. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/tokens/hash_token.py +0 -0
  109. {python_neva-3.0.0 → python_neva-3.1.1}/neva/security/tokens/verify_token.py +0 -0
  110. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/__init__.py +0 -0
  111. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/accessors.py +0 -0
  112. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/__init__.py +0 -0
  113. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/app.py +0 -0
  114. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/config.py +0 -0
  115. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/crypt.py +0 -0
  116. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/db.py +0 -0
  117. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/event.py +0 -0
  118. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/hash.py +0 -0
  119. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/hash.pyi +0 -0
  120. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/log.py +0 -0
  121. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/facade/log.pyi +0 -0
  122. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/strconv.py +0 -0
  123. {python_neva-3.0.0 → python_neva-3.1.1}/neva/support/time.py +0 -0
  124. {python_neva-3.0.0 → python_neva-3.1.1}/neva/testing/__init__.py +0 -0
  125. {python_neva-3.0.0 → python_neva-3.1.1}/tests/__init__.py +0 -0
  126. {python_neva-3.0.0 → python_neva-3.1.1}/tests/arch/__init__.py +0 -0
  127. {python_neva-3.0.0 → python_neva-3.1.1}/tests/config/__init__.py +0 -0
  128. {python_neva-3.0.0 → python_neva-3.1.1}/tests/config/test_loader.py +0 -0
  129. {python_neva-3.0.0 → python_neva-3.1.1}/tests/config/test_repository.py +0 -0
  130. {python_neva-3.0.0 → python_neva-3.1.1}/tests/database/__init__.py +0 -0
  131. {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/__init__.py +0 -0
  132. {python_neva-3.0.0 → python_neva-3.1.1}/tests/events/test_event.py +0 -0
  133. {python_neva-3.0.0 → python_neva-3.1.1}/tests/obs/__init__.py +0 -0
  134. {python_neva-3.0.0 → python_neva-3.1.1}/tests/obs/test_correlation.py +0 -0
  135. {python_neva-3.0.0 → python_neva-3.1.1}/tests/obs/test_profiler.py +0 -0
  136. {python_neva-3.0.0 → python_neva-3.1.1}/tests/security/__init__.py +0 -0
  137. {python_neva-3.0.0 → python_neva-3.1.1}/tests/testing/__init__.py +0 -0
  138. {python_neva-3.0.0 → python_neva-3.1.1}/tests/testing/test_event_fake.py +0 -0
  139. {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.0.0
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.abc import AsyncIterator
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: dict[type, ServiceProvider] = {}
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
- providers_from_file: list[type[ServiceProvider]] = self.config.get(
59
- "providers.providers", type_=list[type[ServiceProvider]]
60
- ).unwrap_or([])
61
- providers_from_app = self.config.get(
62
- "app.providers", type_=list[type[ServiceProvider]]
63
- ).unwrap_or([])
64
- providers: set[type[ServiceProvider]] = set(providers_from_file).union(
65
- 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([])
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(base_providers().union(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: set[type[ServiceProvider]]) -> None:
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 make[T](self, interface: type[T]) -> Result[T, str]:
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 self.container.get(interface))
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 make_sync[T](self, interface: type[T]) -> Result[T, str]:
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(self.container.get_sync(interface))
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 self.container(scope=scope) as container:
146
- self.container = container
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
- self.container = parent
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.make(EventDispatcher)
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.make_sync(cls.get_facade_accessor()))
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.make_sync(interface)
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):
@@ -21,7 +21,7 @@ from typing import (
21
21
 
22
22
  import dishka
23
23
 
24
- from neva import Result
24
+ from neva.support import Result
25
25
 
26
26
 
27
27
  if TYPE_CHECKING:
@@ -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
@@ -9,7 +9,7 @@ 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:
@@ -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()
@@ -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.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
7
+ from sqlalchemy.ext import asyncio
8
+ from typing_extensions import deprecated
8
9
 
9
- from neva import Nothing, Option, Some
10
+ from neva.database.config import ConnectionConfig
10
11
  from neva.database.connection import ConnectionManager, TransactionContext
11
- from neva.database.transaction import BoundTransaction, Transaction
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
- def register_engine(self, name: str, engine: AsyncEngine) -> None:
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._engines[name] = engine
36
- self._session_factories[name] = async_sessionmaker(
37
- bind=engine,
38
- expire_on_commit=False,
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 connection(self, name: str) -> ConnectionManager:
43
- """Returns a connection manager for the given name."""
44
- return self._connections.setdefault(
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
- ConnectionManager(
47
- name,
48
- self._tx_context,
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 current(self, connection: str | None = None) -> Option[Transaction]:
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
- """ # noqa: DOC502 explicit documentation of the possible exception raised by 'begin'
113
- async with self.connection(name).begin() as tx:
114
- yield tx
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 engine in self._engines.values():
119
- await engine.dispose()
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.make(LogManager)).unwrap()
30
- db: DatabaseManager = (await self.app.make(DatabaseManager)).unwrap()
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
- engine = create_async_engine(**conn_config)
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