python-neva 2.4.0__tar.gz → 3.0.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 (123) hide show
  1. {python_neva-2.4.0 → python_neva-3.0.0}/.pre-commit-config.yaml +17 -0
  2. {python_neva-2.4.0 → python_neva-3.0.0}/PKG-INFO +2 -2
  3. {python_neva-2.4.0 → python_neva-3.0.0}/neva/arch/__init__.py +1 -1
  4. {python_neva-2.4.0 → python_neva-3.0.0}/neva/arch/application.py +40 -20
  5. {python_neva-2.4.0 → python_neva-3.0.0}/neva/arch/facade.py +3 -3
  6. python_neva-3.0.0/neva/arch/integrations/__init__.py +1 -0
  7. python_neva-2.4.0/neva/arch/app.py → python_neva-3.0.0/neva/arch/integrations/fastapi.py +14 -31
  8. {python_neva-2.4.0/neva/arch → python_neva-3.0.0/neva/arch/integrations}/faststream.py +2 -25
  9. {python_neva-2.4.0 → python_neva-3.0.0}/neva/arch/service_provider.py +26 -1
  10. {python_neva-2.4.0 → python_neva-3.0.0}/neva/config/repository.py +11 -15
  11. {python_neva-2.4.0 → python_neva-3.0.0}/neva/database/provider.py +8 -8
  12. {python_neva-2.4.0 → python_neva-3.0.0}/neva/events/dispatcher.py +1 -1
  13. {python_neva-2.4.0 → python_neva-3.0.0}/neva/events/provider.py +1 -1
  14. {python_neva-2.4.0 → python_neva-3.0.0}/neva/obs/logging/provider.py +1 -1
  15. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/encryption/encrypter.py +1 -1
  16. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/hashing/hash_manager.py +10 -10
  17. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/provider.py +2 -2
  18. {python_neva-2.4.0 → python_neva-3.0.0}/neva/testing/test_case.py +1 -1
  19. {python_neva-2.4.0 → python_neva-3.0.0}/pyproject.toml +9 -2
  20. {python_neva-2.4.0 → python_neva-3.0.0}/tests/arch/test_scope.py +19 -19
  21. {python_neva-2.4.0 → python_neva-3.0.0}/tests/config/test_repository.py +4 -15
  22. {python_neva-2.4.0 → python_neva-3.0.0}/tests/events/test_dispatch.py +3 -3
  23. {python_neva-2.4.0 → python_neva-3.0.0}/tests/security/test_encrypter.py +6 -5
  24. {python_neva-2.4.0 → python_neva-3.0.0}/tests/security/test_hash_manager.py +1 -1
  25. {python_neva-2.4.0 → python_neva-3.0.0}/tests/testing/test_fixtures.py +8 -8
  26. {python_neva-2.4.0 → python_neva-3.0.0}/tests/testing/test_refresh_database.py +7 -7
  27. {python_neva-2.4.0 → python_neva-3.0.0}/tests/testing/test_test_case.py +11 -11
  28. {python_neva-2.4.0 → python_neva-3.0.0}/uv.lock +5 -5
  29. {python_neva-2.4.0 → python_neva-3.0.0}/.envrc +0 -0
  30. {python_neva-2.4.0 → python_neva-3.0.0}/.gitignore +0 -0
  31. {python_neva-2.4.0 → python_neva-3.0.0}/.python-version +0 -0
  32. {python_neva-2.4.0 → python_neva-3.0.0}/README.md +0 -0
  33. {python_neva-2.4.0 → python_neva-3.0.0}/neva/__init__.py +0 -0
  34. {python_neva-2.4.0 → python_neva-3.0.0}/neva/arch/config.py +0 -0
  35. {python_neva-2.4.0 → python_neva-3.0.0}/neva/config/__init__.py +0 -0
  36. {python_neva-2.4.0 → python_neva-3.0.0}/neva/config/base_providers.py +0 -0
  37. {python_neva-2.4.0 → python_neva-3.0.0}/neva/config/loader.py +0 -0
  38. {python_neva-2.4.0 → python_neva-3.0.0}/neva/database/__init__.py +0 -0
  39. {python_neva-2.4.0 → python_neva-3.0.0}/neva/database/config.py +0 -0
  40. {python_neva-2.4.0 → python_neva-3.0.0}/neva/database/connection.py +0 -0
  41. {python_neva-2.4.0 → python_neva-3.0.0}/neva/database/manager.py +0 -0
  42. {python_neva-2.4.0 → python_neva-3.0.0}/neva/database/transaction.py +0 -0
  43. {python_neva-2.4.0 → python_neva-3.0.0}/neva/events/__init__.py +0 -0
  44. {python_neva-2.4.0 → python_neva-3.0.0}/neva/events/event.py +0 -0
  45. {python_neva-2.4.0 → python_neva-3.0.0}/neva/events/event_registry.py +0 -0
  46. {python_neva-2.4.0 → python_neva-3.0.0}/neva/events/listener.py +0 -0
  47. {python_neva-2.4.0 → python_neva-3.0.0}/neva/obs/__init__.py +0 -0
  48. {python_neva-2.4.0 → python_neva-3.0.0}/neva/obs/instrumentation/__init__.py +0 -0
  49. {python_neva-2.4.0 → python_neva-3.0.0}/neva/obs/instrumentation/sqlalchemy.py +0 -0
  50. {python_neva-2.4.0 → python_neva-3.0.0}/neva/obs/logging/__init__.py +0 -0
  51. {python_neva-2.4.0 → python_neva-3.0.0}/neva/obs/logging/manager.py +0 -0
  52. {python_neva-2.4.0 → python_neva-3.0.0}/neva/obs/middleware/__init__.py +0 -0
  53. {python_neva-2.4.0 → python_neva-3.0.0}/neva/obs/middleware/correlation.py +0 -0
  54. {python_neva-2.4.0 → python_neva-3.0.0}/neva/obs/middleware/profiler.py +0 -0
  55. {python_neva-2.4.0 → python_neva-3.0.0}/neva/py.typed +0 -0
  56. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/__init__.py +0 -0
  57. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/encryption/__init__.py +0 -0
  58. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/encryption/protocol.py +0 -0
  59. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/hashing/__init__.py +0 -0
  60. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/hashing/config.py +0 -0
  61. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/hashing/hashers/__init__.py +0 -0
  62. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/hashing/hashers/argon2.py +0 -0
  63. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/hashing/hashers/bcrypt.py +0 -0
  64. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/hashing/hashers/protocol.py +0 -0
  65. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/tokens/__init__.py +0 -0
  66. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/tokens/generate_token.py +0 -0
  67. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/tokens/hash_token.py +0 -0
  68. {python_neva-2.4.0 → python_neva-3.0.0}/neva/security/tokens/verify_token.py +0 -0
  69. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/__init__.py +0 -0
  70. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/accessors.py +0 -0
  71. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/__init__.py +0 -0
  72. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/app.py +0 -0
  73. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/app.pyi +0 -0
  74. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/config.py +0 -0
  75. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/config.pyi +0 -0
  76. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/crypt.py +0 -0
  77. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/crypt.pyi +0 -0
  78. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/db.py +0 -0
  79. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/db.pyi +0 -0
  80. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/event.py +0 -0
  81. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/event.pyi +0 -0
  82. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/hash.py +0 -0
  83. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/hash.pyi +0 -0
  84. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/log.py +0 -0
  85. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/facade/log.pyi +0 -0
  86. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/results.py +0 -0
  87. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/strategy.py +0 -0
  88. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/strconv.py +0 -0
  89. {python_neva-2.4.0 → python_neva-3.0.0}/neva/support/time.py +0 -0
  90. {python_neva-2.4.0 → python_neva-3.0.0}/neva/testing/__init__.py +0 -0
  91. {python_neva-2.4.0 → python_neva-3.0.0}/neva/testing/fakes.py +0 -0
  92. {python_neva-2.4.0 → python_neva-3.0.0}/neva/testing/fixtures.py +0 -0
  93. {python_neva-2.4.0 → python_neva-3.0.0}/neva/testing/http.py +0 -0
  94. {python_neva-2.4.0 → python_neva-3.0.0}/ruff.toml +0 -0
  95. {python_neva-2.4.0 → python_neva-3.0.0}/tests/__init__.py +0 -0
  96. {python_neva-2.4.0 → python_neva-3.0.0}/tests/arch/__init__.py +0 -0
  97. {python_neva-2.4.0 → python_neva-3.0.0}/tests/config/__init__.py +0 -0
  98. {python_neva-2.4.0 → python_neva-3.0.0}/tests/config/test_loader.py +0 -0
  99. {python_neva-2.4.0 → python_neva-3.0.0}/tests/conftest.py +0 -0
  100. {python_neva-2.4.0 → python_neva-3.0.0}/tests/database/__init__.py +0 -0
  101. {python_neva-2.4.0 → python_neva-3.0.0}/tests/database/conftest.py +0 -0
  102. {python_neva-2.4.0 → python_neva-3.0.0}/tests/database/test_connection_manager.py +0 -0
  103. {python_neva-2.4.0 → python_neva-3.0.0}/tests/database/test_database_manager.py +0 -0
  104. {python_neva-2.4.0 → python_neva-3.0.0}/tests/database/test_edge_cases.py +0 -0
  105. {python_neva-2.4.0 → python_neva-3.0.0}/tests/database/test_multi_connection.py +0 -0
  106. {python_neva-2.4.0 → python_neva-3.0.0}/tests/database/test_sqlalchemy_integration.py +0 -0
  107. {python_neva-2.4.0 → python_neva-3.0.0}/tests/database/test_transaction.py +0 -0
  108. {python_neva-2.4.0 → python_neva-3.0.0}/tests/database/test_transaction_context.py +0 -0
  109. {python_neva-2.4.0 → python_neva-3.0.0}/tests/database/test_transaction_registry.py +0 -0
  110. {python_neva-2.4.0 → python_neva-3.0.0}/tests/events/__init__.py +0 -0
  111. {python_neva-2.4.0 → python_neva-3.0.0}/tests/events/conftest.py +0 -0
  112. {python_neva-2.4.0 → python_neva-3.0.0}/tests/events/test_deferred.py +0 -0
  113. {python_neva-2.4.0 → python_neva-3.0.0}/tests/events/test_event.py +0 -0
  114. {python_neva-2.4.0 → python_neva-3.0.0}/tests/events/test_event_registry.py +0 -0
  115. {python_neva-2.4.0 → python_neva-3.0.0}/tests/events/test_function_listener.py +0 -0
  116. {python_neva-2.4.0 → python_neva-3.0.0}/tests/events/test_immediate.py +0 -0
  117. {python_neva-2.4.0 → python_neva-3.0.0}/tests/obs/__init__.py +0 -0
  118. {python_neva-2.4.0 → python_neva-3.0.0}/tests/obs/test_correlation.py +0 -0
  119. {python_neva-2.4.0 → python_neva-3.0.0}/tests/obs/test_profiler.py +0 -0
  120. {python_neva-2.4.0 → python_neva-3.0.0}/tests/security/__init__.py +0 -0
  121. {python_neva-2.4.0 → python_neva-3.0.0}/tests/testing/__init__.py +0 -0
  122. {python_neva-2.4.0 → python_neva-3.0.0}/tests/testing/test_event_fake.py +0 -0
  123. {python_neva-2.4.0 → python_neva-3.0.0}/tests/testing/test_facade_restore.py +0 -0
@@ -26,3 +26,20 @@ repos:
26
26
  rev: v1.19.1
27
27
  hooks:
28
28
  - id: mypy
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
+ ]
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-neva
3
- Version: 2.4.0
3
+ Version: 3.0.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
@@ -4,9 +4,9 @@ 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
9
+ from neva.arch.integrations.fastapi import App
10
10
  from neva.arch.service_provider import (
11
11
  Bootable,
12
12
  ServiceProvider,
@@ -1,13 +1,14 @@
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.abc import AsyncIterator
5
+ from contextlib import AsyncExitStack, asynccontextmanager
6
6
  from pathlib import Path
7
7
  from typing import Any, Callable, Self
8
8
 
9
9
  import dishka
10
- from dishka.integrations.fastapi import FastapiProvider
10
+ from dishka.provider import BaseProvider
11
+ from typing_extensions import deprecated
11
12
 
12
13
  from neva import Err, Ok, Result
13
14
  from neva.arch.facade import Facade
@@ -33,7 +34,7 @@ class Application:
33
34
 
34
35
  self.config: ConfigRepository = ConfigRepository()
35
36
  self.providers: dict[type, ServiceProvider] = {}
36
- self.di_provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
37
+ self.root_provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
37
38
 
38
39
  configuration_path = (
39
40
  config_path
@@ -50,7 +51,9 @@ class Application:
50
51
  ):
51
52
  raise RuntimeError("Failed to register config")
52
53
 
53
- self.bind(lambda: self.config, interface=ConfigRepository)
54
+ _ = self.root_provider.provide(
55
+ lambda: self.config, provides=ConfigRepository
56
+ )
54
57
 
55
58
  providers_from_file: list[type[ServiceProvider]] = self.config.get(
56
59
  "providers.providers", type_=list[type[ServiceProvider]]
@@ -61,14 +64,18 @@ class Application:
61
64
  providers: set[type[ServiceProvider]] = set(providers_from_file).union(
62
65
  set(providers_from_app)
63
66
  )
67
+ _ = self.root_provider.provide(source=lambda: self, provides=Application)
64
68
  self.register_providers(base_providers().union(providers))
65
- self.bind(source=lambda: self, interface=Application)
66
69
  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())
70
+ self.build_container(self.root_provider)
71
+
72
+ def build_container(self, *providers: BaseProvider) -> None:
73
+ """Build the application container."""
74
+ self.container: dishka.AsyncContainer = dishka.make_async_container(
75
+ self.root_provider,
76
+ *[p.provider for p in self.providers.values()],
77
+ *providers,
78
+ )
72
79
 
73
80
  def register(self, provider: type[ServiceProvider]) -> Result[ServiceProvider, str]:
74
81
  """Registers a service provider with the application.
@@ -90,6 +97,7 @@ class Application:
90
97
  for provider in providers:
91
98
  _ = self.register(provider)
92
99
 
100
+ @deprecated("Use the new bind() method directly with a Service Provider.")
93
101
  def bind(
94
102
  self,
95
103
  source: type | Callable[..., Any],
@@ -98,32 +106,43 @@ class Application:
98
106
  scope: dishka.BaseScope | None = None,
99
107
  ) -> None:
100
108
  """Binds a source to the container."""
101
- _ = self.di_provider.provide(
109
+ _ = self.root_provider.provide(
102
110
  source=source,
103
111
  scope=scope,
104
112
  provides=interface,
105
113
  )
106
114
 
107
- def make[T](self, interface: type[T]) -> Result[T, str]:
115
+ async def make[T](self, interface: type[T]) -> Result[T, str]:
108
116
  """Resolve and instanciate a type from the container.
109
117
 
110
118
  Returns:
111
119
  Result containing the resolved type instance or an error message.
112
120
  """
113
121
  try:
114
- return Ok(self.container.get(interface))
122
+ return Ok(await self.container.get(interface))
115
123
  except Exception as e:
116
124
  return Err(f"Failed to resolve service '{interface.__name__}': {e}")
117
125
 
118
- @contextmanager
119
- def scope(self, scope: dishka.BaseScope | None = None) -> Iterator[Self]:
126
+ def make_sync[T](self, interface: type[T]) -> Result[T, str]:
127
+ """Synchronous version of make.
128
+
129
+ Returns:
130
+ Result containing the resolved type instance or an error message.
131
+ """
132
+ try:
133
+ return Ok(self.container.get_sync(interface))
134
+ except Exception as e:
135
+ return Err(f"Failed to resolve service '{interface.__name__}': {e}")
136
+
137
+ @asynccontextmanager
138
+ async def scope(self, scope: dishka.BaseScope | None = None) -> AsyncIterator[Self]:
120
139
  """Enter a new scope.
121
140
 
122
141
  Yields:
123
142
  The application instance with the new scope.
124
143
  """
125
144
  parent = self.container
126
- with self.container(scope=scope) as container:
145
+ async with self.container(scope=scope) as container:
127
146
  self.container = container
128
147
  try:
129
148
  yield self
@@ -140,9 +159,10 @@ class Application:
140
159
  if isinstance(provider, Bootable):
141
160
  await stack.enter_async_context(provider.lifespan())
142
161
 
143
- self._wire_event_listeners()
162
+ await self._wire_event_listeners()
144
163
  yield
145
164
 
165
+ await self.container.close()
146
166
  Facade.reset_facade_application()
147
167
 
148
168
  def _bind_event_listeners(self) -> None:
@@ -152,11 +172,11 @@ class Application:
152
172
  for listener_cls in listeners:
153
173
  self.bind(listener_cls)
154
174
 
155
- def _wire_event_listeners(self) -> None:
175
+ async def _wire_event_listeners(self) -> None:
156
176
  """Wire event-listener mappings from all providers onto the dispatcher."""
157
177
  from neva.events.dispatcher import EventDispatcher
158
178
 
159
- result = self.make(EventDispatcher)
179
+ result = await self.make(EventDispatcher)
160
180
  if result.is_err:
161
181
  return
162
182
 
@@ -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(cls.get_facade_accessor()))
156
+ .and_then(lambda a: a.make_sync(cls.get_facade_accessor()))
157
157
  .unwrap()
158
158
  )
159
159
  spy_obj = MagicMock(spec=type(real), wraps=real)
@@ -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]:
@@ -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(interface)
201
+ return app.make_sync(interface)
202
202
 
203
203
 
204
204
  class Facade(ABC, metaclass=FacadeMeta):
@@ -0,0 +1 @@
1
+ """Integrations module providing integrations with external frameworks."""
@@ -8,11 +8,10 @@ the Application class.
8
8
  from collections.abc import AsyncGenerator, AsyncIterator, Sequence
9
9
  from contextlib import asynccontextmanager
10
10
  from pathlib import Path
11
- from typing import Any, Callable
12
11
 
13
- import dishka
14
12
  import fastapi
15
- from dishka.integrations.fastapi import setup_dishka
13
+ from dishka import FromDishka
14
+ from dishka.integrations.fastapi import FastapiProvider, setup_dishka
16
15
  from starlette.middleware import Middleware
17
16
  from starlette.routing import BaseRoute
18
17
  from starlette.types import StatefulLifespan, StatelessLifespan
@@ -44,6 +43,7 @@ class App(fastapi.FastAPI):
44
43
 
45
44
  """
46
45
  self.application: Application = Application(config_path=config_path)
46
+ self.application.build_container(FastapiProvider())
47
47
 
48
48
  self._lifespan: StatelessLifespan["App"] | StatefulLifespan["App"] | None = (
49
49
  lifespan
@@ -51,13 +51,15 @@ class App(fastapi.FastAPI):
51
51
  config = self.application.config
52
52
 
53
53
  super().__init__(
54
- debug=config.get("app.debug", default=False).unwrap(),
54
+ debug=config.get("app.debug", type_=bool).unwrap_or(False), # noqa: FBT003
55
55
  routes=routes,
56
- title=config.get("app.title", default="Neva Application").unwrap(),
57
- version=config.get("app.version", default="0.1.0").unwrap(),
58
- openapi_url=config.get("app.openapi_url", default="/openapi.json").unwrap(),
59
- docs_url=config.get("app.docs_url", default="/docs").unwrap(),
60
- redoc_url=config.get("app.redoc_url", default="/redoc").unwrap(),
56
+ title=config.get("app.title", type_=str).unwrap_or("Neva Application"),
57
+ version=config.get("app.version", type_=str).unwrap_or("0.1.0"),
58
+ openapi_url=config.get("app.openapi_url", type_=str).unwrap_or(
59
+ "/openapi.json"
60
+ ),
61
+ docs_url=config.get("app.docs_url", type_=str).unwrap_or("/docs"),
62
+ redoc_url=config.get("app.redoc_url", type_=str).unwrap_or("/redoc"),
61
63
  lifespan=self._create_lifespan(),
62
64
  middleware=middlewares,
63
65
  )
@@ -75,28 +77,6 @@ class App(fastapi.FastAPI):
75
77
  """
76
78
  return self.application.register(provider=provider)
77
79
 
78
- def bind(
79
- self,
80
- source: type | Callable[..., Any],
81
- *,
82
- interface: type | None = None,
83
- scope: dishka.BaseScope | None = None,
84
- ) -> None:
85
- """Binds a source to the container."""
86
- self.application.bind(
87
- source=source,
88
- interface=interface,
89
- scope=scope,
90
- )
91
-
92
- def make[T](self, interface: type[T]) -> Result[T, str]:
93
- """Resolve and instanciate a type from the container.
94
-
95
- Returns:
96
- Result containing the resolved type instance or an error message.
97
- """
98
- return self.application.make(interface=interface)
99
-
100
80
  @asynccontextmanager
101
81
  async def lifespan(self) -> AsyncGenerator[None, None]:
102
82
  """Manage the lifecycle of the application."""
@@ -127,3 +107,6 @@ class App(fastapi.FastAPI):
127
107
  yield
128
108
 
129
109
  return composed_lifespan
110
+
111
+
112
+ Inject = FromDishka
@@ -2,9 +2,8 @@
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
@@ -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,7 +9,17 @@ 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
+ )
21
+
22
+ import dishka
13
23
 
14
24
  from neva import Result
15
25
 
@@ -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.
@@ -5,7 +5,9 @@ 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
+
10
+ from typing_extensions import TypeForm
9
11
 
10
12
  from neva import Err, Ok, Result
11
13
 
@@ -63,14 +65,12 @@ class ConfigRepository:
63
65
  def get[T](
64
66
  self,
65
67
  key: str,
66
- default: T | None = None,
67
- type_: type[T] | None = None,
68
+ type_: TypeForm[T] | None = None,
68
69
  ) -> Result[T, str]:
69
70
  """Get a configuration value using dot notation.
70
71
 
71
72
  Args:
72
73
  key: Dot-notated key path (e.g., "database.host").
73
- default: Default value to return if key is not found.
74
74
  type_: Type to cast the value to.
75
75
 
76
76
  Returns:
@@ -80,18 +80,14 @@ class ConfigRepository:
80
80
  keys = key.split(".")
81
81
  current = self._items
82
82
 
83
- try:
84
- for k in keys:
85
- if not isinstance(current, dict):
86
- if default is not None:
87
- return Ok(default)
88
- 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:
89
87
  current = current[k]
90
- return Ok(current) # type: ignore [arg-type]
91
- except KeyError:
92
- if default is not None:
93
- return Ok(default)
94
- 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))
95
91
 
96
92
  def has(self, key: str) -> bool:
97
93
  """Check if a configuration key exists.
@@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import create_async_engine
8
8
 
9
9
  from neva import Err, Ok, Result
10
10
  from neva.arch import ServiceProvider
11
+ from neva.database.config import ConnectionConfig, DatabaseConfig
11
12
  from neva.database.connection import TransactionContext
12
13
  from neva.database.manager import DatabaseManager
13
14
  from neva.obs import LogManager
@@ -18,22 +19,21 @@ class DatabaseServiceProvider(ServiceProvider):
18
19
 
19
20
  @override
20
21
  def register(self) -> Result[Self, str]:
21
- self.app.bind(TransactionContext)
22
- self.app.bind(DatabaseManager)
22
+ self.bind(TransactionContext)
23
+ self.bind(DatabaseManager)
23
24
  return Ok(self)
24
25
 
25
26
  @asynccontextmanager
26
27
  async def lifespan(self) -> AsyncIterator[None]:
27
28
  """Initialize and cleanup database connections."""
28
- logger: LogManager = self.app.make(LogManager).unwrap()
29
- db: DatabaseManager = self.app.make(DatabaseManager).unwrap()
29
+ logger: LogManager = (await self.app.make(LogManager)).unwrap()
30
+ db: DatabaseManager = (await self.app.make(DatabaseManager)).unwrap()
30
31
  logger.info("Beginning SQLAlchemy initialization...")
31
- match self.app.config.get("database"):
32
+ match self.app.config.get("database", type_=DatabaseConfig):
32
33
  case Ok(config):
33
- connections: dict = config.get("connections", {})
34
+ connections: dict[str, ConnectionConfig] = config["connections"]
34
35
  for name, conn_config in connections.items():
35
- url = conn_config.pop("url")
36
- engine = create_async_engine(url, **conn_config)
36
+ engine = create_async_engine(**conn_config)
37
37
  db.register_engine(name, engine)
38
38
  logger.info(f"Registered engine for connection '{name}'.")
39
39
  logger.info("SQLAlchemy initialization complete.")
@@ -72,7 +72,7 @@ class EventDispatcher:
72
72
  Returns:
73
73
  EventListener[Any]: The resolved listener.
74
74
  """
75
- result = self._app.make(listener_cls)
75
+ result = self._app.make_sync(listener_cls)
76
76
  if result.is_ok:
77
77
  return result.unwrap()
78
78
  return listener_cls()
@@ -23,5 +23,5 @@ class EventServiceProvider(ServiceProvider):
23
23
  """
24
24
  from neva.events.dispatcher import EventDispatcher
25
25
 
26
- self.app.bind(EventDispatcher)
26
+ self.bind(EventDispatcher)
27
27
  return Ok(self)
@@ -21,6 +21,6 @@ class LogServiceProvider(ServiceProvider):
21
21
 
22
22
  @override
23
23
  def register(self) -> Result[Self, str]:
24
- self.app.bind(LogManager)
24
+ self.bind(LogManager)
25
25
 
26
26
  return Ok(self)
@@ -127,7 +127,7 @@ class AesEncrypter:
127
127
  ValueError: If no encryption key is configured.
128
128
  """
129
129
  previous_keys = self._app.config.get(
130
- "app.previous_keys", default=[], type_=list[str]
130
+ "app.previous_keys", type_=list[str]
131
131
  ).unwrap_or([])
132
132
 
133
133
  match self._app.config.get("app.key", type_=str):
@@ -1,6 +1,6 @@
1
1
  """Hash manager for managing password hashing strategies."""
2
2
 
3
- from typing import Literal, cast, override
3
+ from typing import override
4
4
 
5
5
  from neva import Option
6
6
  from neva.arch import Application
@@ -108,15 +108,15 @@ class HashManager(StrategyResolver[Hasher]):
108
108
  Configured Argon2Hasher instance.
109
109
  """
110
110
  argon_config = manager.app.config.get(
111
- "hashing.argon", default={}, type_=Argon2Config
111
+ "hashing.argon", type_=Argon2Config
112
112
  ).unwrap()
113
113
 
114
114
  return Argon2Hasher(
115
- time_cost=cast(int, argon_config.get("time_cost", 2)),
116
- memory_cost=cast(int, argon_config.get("memory_cost", 102400)),
117
- parallelism=cast(int, argon_config.get("parallelism", 8)),
118
- hash_len=cast(int, argon_config.get("hash_len", 16)),
119
- salt_len=cast(int, argon_config.get("salt_len", 16)),
115
+ time_cost=argon_config.get("time_cost", 2),
116
+ memory_cost=argon_config.get("memory_cost", 102400),
117
+ parallelism=argon_config.get("parallelism", 8),
118
+ hash_len=argon_config.get("hash_len", 16),
119
+ salt_len=argon_config.get("salt_len", 16),
120
120
  )
121
121
 
122
122
  def _create_bcrypt_hasher(self, manager: StrategyResolver[Hasher]) -> BcryptHasher:
@@ -129,10 +129,10 @@ class HashManager(StrategyResolver[Hasher]):
129
129
  Configured BcryptHasher instance.
130
130
  """
131
131
  bcrypt_config = manager.app.config.get(
132
- "hashing.bcrypt", default={}, type_=BcryptConfig
132
+ "hashing.bcrypt", type_=BcryptConfig
133
133
  ).unwrap()
134
134
 
135
135
  return BcryptHasher(
136
- rounds=cast(int, bcrypt_config.get("rounds", 12)),
137
- prefix=cast(Literal["2a", "2b"], bcrypt_config.get("prefix", "2b")),
136
+ rounds=bcrypt_config.get("rounds", 12),
137
+ prefix=bcrypt_config.get("prefix", "2b"),
138
138
  )
@@ -12,6 +12,6 @@ class SecurityProvider(arch.ServiceProvider):
12
12
 
13
13
  @override
14
14
  def register(self) -> Result[Self, str]:
15
- self.app.bind(HashManager, interface=HashManager)
16
- self.app.bind(AesEncrypter, interface=Encrypter)
15
+ self.bind(HashManager, interface=HashManager)
16
+ self.bind(AesEncrypter, interface=Encrypter)
17
17
  return Ok(self)
@@ -95,7 +95,7 @@ class RefreshDatabase:
95
95
  _setup_database_engine: None, # ordering dependency: runs after engine setup
96
96
  ) -> AsyncIterator[None]:
97
97
  """Wrap the test in a DB transaction and roll it back on completion."""
98
- db = _test_case_application.make(DatabaseManager).unwrap()
98
+ db = (await _test_case_application.make(DatabaseManager)).unwrap()
99
99
  async with db.begin() as tx:
100
100
  tx.share()
101
101
  yield
@@ -7,13 +7,13 @@ packages = ["neva"]
7
7
 
8
8
  [project]
9
9
  name = "python-neva"
10
- version = "2.4.0"
10
+ version = "3.0.0"
11
11
  description = "Add your description here"
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.12"
14
14
  dependencies = [
15
15
  "cryptography>=46.0.3",
16
- "dishka>=1.7.2",
16
+ "dishka>=1.10.0",
17
17
  "fastapi[all]>=0.129.0",
18
18
  "faststream>=0.6.6",
19
19
  "pwdlib[argon2,bcrypt]>=0.3.0",
@@ -43,6 +43,13 @@ dev = [
43
43
  "ruff>=0.15.6",
44
44
  ]
45
45
 
46
+ [tool.basedpyright]
47
+ enableExperimentalFeatures = true
48
+
49
+ [tool.mypy]
50
+ enable_incomplete_feature = ["TypeForm"]
51
+ plugins = ["pydantic.mypy"]
52
+
46
53
  [tool.pytest.ini_options]
47
54
  asyncio_mode = "auto"
48
55
  asyncio_default_fixture_loop_scope = "function"