python-neva 2.2.1__py3-none-any.whl → 2.4.0__py3-none-any.whl

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.
neva/arch/application.py CHANGED
@@ -52,8 +52,12 @@ class Application:
52
52
 
53
53
  self.bind(lambda: self.config, interface=ConfigRepository)
54
54
 
55
- providers_from_file = self.config.get("providers.providers").unwrap_or([])
56
- providers_from_app = self.config.get("app.providers").unwrap_or([])
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([])
57
61
  providers: set[type[ServiceProvider]] = set(providers_from_file).union(
58
62
  set(providers_from_app)
59
63
  )
neva/config/repository.py CHANGED
@@ -60,12 +60,18 @@ class ConfigRepository:
60
60
  current[keys[-1]] = value
61
61
  return Ok(None)
62
62
 
63
- def get(self, key: str, default: object = None) -> Result[Any, str]:
63
+ def get[T](
64
+ self,
65
+ key: str,
66
+ default: T | None = None,
67
+ type_: type[T] | None = None,
68
+ ) -> Result[T, str]:
64
69
  """Get a configuration value using dot notation.
65
70
 
66
71
  Args:
67
72
  key: Dot-notated key path (e.g., "database.host").
68
73
  default: Default value to return if key is not found.
74
+ type_: Type to cast the value to.
69
75
 
70
76
  Returns:
71
77
  Result containing the configuration value or the default.
@@ -81,7 +87,7 @@ class ConfigRepository:
81
87
  return Ok(default)
82
88
  return Err(f"Config key '{key}' not found")
83
89
  current = current[k]
84
- return Ok(current)
90
+ return Ok(current) # type: ignore [arg-type]
85
91
  except KeyError:
86
92
  if default is not None:
87
93
  return Ok(default)
neva/database/manager.py CHANGED
@@ -10,6 +10,7 @@ from neva import Nothing, Option, Some
10
10
  from neva.database.connection import ConnectionManager, TransactionContext
11
11
  from neva.database.transaction import BoundTransaction, Transaction
12
12
  from neva.obs import LogManager
13
+ from neva.obs.instrumentation.sqlalchemy import instrument
13
14
 
14
15
 
15
16
  @final
@@ -30,6 +31,7 @@ class DatabaseManager:
30
31
  name: The connection name.
31
32
  engine: The async engine.
32
33
  """
34
+ instrument(engine)
33
35
  self._engines[name] = engine
34
36
  self._session_factories[name] = async_sessionmaker(
35
37
  bind=engine,
neva/database/provider.py CHANGED
@@ -8,7 +8,6 @@ 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.config.repository import ConfigRepository
12
11
  from neva.database.connection import TransactionContext
13
12
  from neva.database.manager import DatabaseManager
14
13
  from neva.obs import LogManager
@@ -29,7 +28,7 @@ class DatabaseServiceProvider(ServiceProvider):
29
28
  logger: LogManager = self.app.make(LogManager).unwrap()
30
29
  db: DatabaseManager = self.app.make(DatabaseManager).unwrap()
31
30
  logger.info("Beginning SQLAlchemy initialization...")
32
- match self.app.make(ConfigRepository).unwrap().get("database"):
31
+ match self.app.config.get("database"):
33
32
  case Ok(config):
34
33
  connections: dict = config.get("connections", {})
35
34
  for name, conn_config in connections.items():
@@ -0,0 +1 @@
1
+ """Various OTel instrumentation."""
@@ -0,0 +1,15 @@
1
+ """SQLAlchemy instrumentation."""
2
+
3
+ from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
4
+ from sqlalchemy import Engine
5
+ from sqlalchemy.ext.asyncio import AsyncEngine
6
+
7
+
8
+ def instrument(engine: AsyncEngine | Engine) -> None:
9
+ """Instrument SQLAlchemy engine."""
10
+ match engine:
11
+ case AsyncEngine():
12
+ to_instrument = engine.sync_engine
13
+ case Engine():
14
+ to_instrument = engine
15
+ SQLAlchemyInstrumentor().instrument(engine=to_instrument)
@@ -10,7 +10,6 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
10
10
 
11
11
  from neva import Err, Ok, Result
12
12
  from neva.arch import Application
13
- from neva.config import ConfigRepository
14
13
  from neva.security.encryption.protocol import JsonValue
15
14
 
16
15
 
@@ -127,18 +126,16 @@ class AesEncrypter:
127
126
  Raises:
128
127
  ValueError: If no encryption key is configured.
129
128
  """
130
- config = self._app.make(ConfigRepository).expect(
131
- "ConfigRepository not found in container"
132
- )
133
-
134
- key = config.get("app.key", default=None).unwrap_or(None)
135
- previous_keys = config.get("app.previous_keys", default=[]).unwrap_or([])
136
-
137
- if key is None:
138
- msg = "No encryption key configured. Set 'app.key' in configuration."
139
- raise ValueError(msg)
140
-
141
- ciphers = [AESGCM(self._parse_key(key))]
129
+ previous_keys = self._app.config.get(
130
+ "app.previous_keys", default=[], type_=list[str]
131
+ ).unwrap_or([])
132
+
133
+ match self._app.config.get("app.key", type_=str):
134
+ case Err():
135
+ msg = "No encryption key configured. Set 'app.key' in configuration."
136
+ raise ValueError(msg)
137
+ case Ok(key):
138
+ ciphers = [AESGCM(self._parse_key(key))]
142
139
 
143
140
  for prev_key in previous_keys:
144
141
  ciphers.append(AESGCM(self._parse_key(prev_key)))
@@ -1,10 +1,10 @@
1
1
  """Hash manager for managing password hashing strategies."""
2
2
 
3
- from typing import override
3
+ from typing import Literal, cast, override
4
4
 
5
- from neva import Option, from_optional
5
+ from neva import Option
6
6
  from neva.arch import Application
7
- from neva.config import ConfigRepository
7
+ from neva.security.hashing.config import Argon2Config, BcryptConfig
8
8
  from neva.security.hashing.hashers.argon2 import Argon2Hasher
9
9
  from neva.security.hashing.hashers.bcrypt import BcryptHasher
10
10
  from neva.security.hashing.hashers.protocol import Hasher
@@ -32,13 +32,7 @@ class HashManager(StrategyResolver[Hasher]):
32
32
  Returns:
33
33
  Option containing the default hasher name.
34
34
  """
35
- config_result = self.app.make(ConfigRepository)
36
- if config_result.is_err:
37
- return from_optional(None)
38
-
39
- config = config_result.unwrap()
40
- driver = config.get("hashing.driver", default=None).unwrap_or(None)
41
- return from_optional(driver)
35
+ return self.app.config.get("hashing.driver", type_=str).ok()
42
36
 
43
37
  def make(
44
38
  self,
@@ -113,18 +107,17 @@ class HashManager(StrategyResolver[Hasher]):
113
107
  Returns:
114
108
  Configured Argon2Hasher instance.
115
109
  """
116
- config = manager.app.make(ConfigRepository).unwrap()
117
- argon_config = config.get("hashing.argon", default={}).unwrap()
118
-
119
- if isinstance(argon_config, dict):
120
- return Argon2Hasher(
121
- time_cost=argon_config.get("time_cost", 2),
122
- memory_cost=argon_config.get("memory_cost", 102400),
123
- parallelism=argon_config.get("parallelism", 8),
124
- hash_len=argon_config.get("hash_len", 16),
125
- salt_len=argon_config.get("salt_len", 16),
126
- )
127
- return Argon2Hasher()
110
+ argon_config = manager.app.config.get(
111
+ "hashing.argon", default={}, type_=Argon2Config
112
+ ).unwrap()
113
+
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)),
120
+ )
128
121
 
129
122
  def _create_bcrypt_hasher(self, manager: StrategyResolver[Hasher]) -> BcryptHasher:
130
123
  """Create an instance of the Bcrypt hash strategy.
@@ -135,12 +128,11 @@ class HashManager(StrategyResolver[Hasher]):
135
128
  Returns:
136
129
  Configured BcryptHasher instance.
137
130
  """
138
- config = manager.app.make(ConfigRepository).unwrap()
139
- bcrypt_config = config.get("hashing.bcrypt", default={}).unwrap()
140
-
141
- if isinstance(bcrypt_config, dict):
142
- return BcryptHasher(
143
- rounds=bcrypt_config.get("rounds", 12),
144
- prefix=bcrypt_config.get("prefix", "2b"),
145
- )
146
- return BcryptHasher()
131
+ bcrypt_config = manager.app.config.get(
132
+ "hashing.bcrypt", default={}, type_=BcryptConfig
133
+ ).unwrap()
134
+
135
+ return BcryptHasher(
136
+ rounds=cast(int, bcrypt_config.get("rounds", 12)),
137
+ prefix=cast(Literal["2a", "2b"], bcrypt_config.get("prefix", "2b")),
138
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-neva
3
- Version: 2.2.1
3
+ Version: 2.4.0
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: aiosqlite>=0.20.0
@@ -9,6 +9,7 @@ Requires-Dist: cryptography>=46.0.3
9
9
  Requires-Dist: dishka>=1.7.2
10
10
  Requires-Dist: fastapi[all]>=0.129.0
11
11
  Requires-Dist: faststream>=0.6.6
12
+ Requires-Dist: opentelemetry-instrumentation-sqlalchemy>=0.62b0
12
13
  Requires-Dist: pwdlib[argon2,bcrypt]>=0.3.0
13
14
  Requires-Dist: pyinstrument>=5.1.1
14
15
  Requires-Dist: sqlalchemy[asyncio]>=2.0.0
@@ -2,7 +2,7 @@ neva/__init__.py,sha256=Pj77_FvqTS-Xn0C1MZE34zTzl5QQcIyF1Zne64L37So,333
2
2
  neva/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  neva/arch/__init__.py,sha256=ZKMZ3u-pxVRRQcbqIF2L4F5IUclRcRVNe83hnN5YYeo,470
4
4
  neva/arch/app.py,sha256=devhJqAmssWCdhvT7d-mE6vyUuEXX5rIY06oIoR21QE,4451
5
- neva/arch/application.py,sha256=9bJCGKXX82xSN5LxmvGLYNSzbc8CgmWEk3DS9JqTMT8,5953
5
+ neva/arch/application.py,sha256=pBoN8ExT1_MXjeJhrRNdp_S3KdB7a9qf_Hc_sHNRei4,6096
6
6
  neva/arch/config.py,sha256=R8_wMKTwX8vTFqfQhgglDQ9PfB6Yo6mx6OnKg8bnIWw,1042
7
7
  neva/arch/facade.py,sha256=4aTOFuBwM58gn1FR7X1mOoKdi37J10OjPOpLVnCTCw8,7915
8
8
  neva/arch/faststream.py,sha256=1eUQRh3ahmQ6T991iHpxMeItDI53mEFq_30krIagNmM,2361
@@ -10,12 +10,12 @@ neva/arch/service_provider.py,sha256=g9tvlBNpGxdJ6yL9w6L-oPLzC6AV_Z6vLDR-TW3Kqz0
10
10
  neva/config/__init__.py,sha256=Ygfnh-MRRKYDdf54_LeMSHwmck5VpNHTdnnMtGEFLtU,121
11
11
  neva/config/base_providers.py,sha256=fK4feZWLzchpQ_xaeumtR9P2JK5nuk8Vq5O7JWkGyO8,908
12
12
  neva/config/loader.py,sha256=ZIcx8oYIi2r4wv1G_Q7_a6nG7wARyzjiZ2wF43t3B2I,3942
13
- neva/config/repository.py,sha256=B7iWgJLtOIwdmp7I0y9ISgl3g_Xc60tUe-ofDUKM6fg,4827
13
+ neva/config/repository.py,sha256=RlErYZfNBQQkwId3zXs3wjgcKR-uhUxNtNjh_3r9cD4,4972
14
14
  neva/database/__init__.py,sha256=3yYnEe8HQM86tURyAhTbMtE7AXubIEMN3GuwxkkT-bk,508
15
15
  neva/database/config.py,sha256=bVKUqlrYDFrURqg_FbntPXyVkbOUPSVXQJTKjyucjL0,451
16
16
  neva/database/connection.py,sha256=et1fvGxa2iHTJ8yzAv9VIYqfD4GtGaV0KQUW97SdLHQ,7107
17
- neva/database/manager.py,sha256=lgW0rd6CHQKNUSfIR25mTMIalu13HZD7Vklyfq6Tl10,4425
18
- neva/database/provider.py,sha256=R-K4aFCgF8Ewu6I_-BXo874IYgQgI3b8qYPsGe7ArFA,1785
17
+ neva/database/manager.py,sha256=tQFHj2HZzo3oxdWyWNQlv5277I7vh7vDlEEiUyAKjDs,4511
18
+ neva/database/provider.py,sha256=GvWZjT3f4BEzQFY9yB2QHciw2NXGoy6L-d1CbP6vPa4,1708
19
19
  neva/database/transaction.py,sha256=yQuFDhdXipUWac3zx4rVy6R6XUB_xPT_CLoQ-26223Y,5933
20
20
  neva/events/__init__.py,sha256=xwIAXNcOASpuCY4DJpshZc1gtxQHimzwy-s7c-4QuiQ,746
21
21
  neva/events/dispatcher.py,sha256=3xMhxE5uQBY7648jp75NkRwu09lqYo7ymmfSvnl6kT0,2850
@@ -24,6 +24,8 @@ neva/events/event_registry.py,sha256=jucYOUjCERz-ICdPiDrxYw_ySvmPJOcRzYKCgka29qY
24
24
  neva/events/listener.py,sha256=AMfi24R_WA_-mVg0fxfOrwY6EV8kO1wU5YpAFPvGrus,1825
25
25
  neva/events/provider.py,sha256=ilPW_-zaLMrh-cgWphLe_aqvs00LNp93OOMFFpIppyE,767
26
26
  neva/obs/__init__.py,sha256=dVzgljk9Hvo44LI34RcwbDyT42_z4nSnFVmL4GLbKD0,331
27
+ neva/obs/instrumentation/__init__.py,sha256=SfIAvuCs-L0kEzOxUJ4aOIkhbydCe7qBwWtGeDELVA0,36
28
+ neva/obs/instrumentation/sqlalchemy.py,sha256=kwk43NthFi4mPMhDJI55rxt9YPOY_mwOkr2E-cD64ZY,497
27
29
  neva/obs/logging/__init__.py,sha256=gIYBAzMK7-wPGEpA_kYKvJPT3hYN99OcCc9UuHFeiHk,251
28
30
  neva/obs/logging/manager.py,sha256=ktd3muBGU3e1KuBK6tSU5faRJRliPwmknuqJ2EYeFBk,2422
29
31
  neva/obs/logging/provider.py,sha256=lt5oma5eV6mjN4C5iFhyYf0BmI9BMbFVBJJNSy2XOkg,638
@@ -33,11 +35,11 @@ neva/obs/middleware/profiler.py,sha256=ovx3s-39WpNtu2KhzVMWyIJa6OLrSVDuL56xyDA-v
33
35
  neva/security/__init__.py,sha256=Dhh3ZsseP6biWgRQY4hYKkoYFCR7ehhxz4VBqN8e02k,440
34
36
  neva/security/provider.py,sha256=20ZPKp4SxYV7pN1TN1yx-RIHken2TEHJBpaVjIFRxYI,499
35
37
  neva/security/encryption/__init__.py,sha256=eVWW-qk03vy7Bn-sNQ2WJHWZI6tPJOkknJz8llc1m0M,288
36
- neva/security/encryption/encrypter.py,sha256=_yo_jTU7yg3YloxvzjyUJBuXb-WAbnyYVTzuCF9F8JA,5238
38
+ neva/security/encryption/encrypter.py,sha256=E2DvTsG_756L75K1IUeYlEzazKn4Eqk1psryCk7HEHE,5156
37
39
  neva/security/encryption/protocol.py,sha256=H_JSt8IBH_AVkgccltY_4dGtcmESt4o76W0vxszBikM,839
38
40
  neva/security/hashing/__init__.py,sha256=k3SviEvtJLjBXTkR5I6w9YMPw6Q-LIoE1wGNz3GYvOE,374
39
41
  neva/security/hashing/config.py,sha256=ltj7_GW44gSg2N0i8CEYaw6Y6xUfzwsr4zXk5ic_sJI,624
40
- neva/security/hashing/hash_manager.py,sha256=ViFtbaKrbFLWJ6OpajFByqpmZ8YAnZfy_3Jatm0aWZ0,4866
42
+ neva/security/hashing/hash_manager.py,sha256=7Oy2gOUZvzRbMsD3unvE1HFDjcHvB-U3Gl84kd3Lbz4,4568
41
43
  neva/security/hashing/hashers/__init__.py,sha256=Zn_Q2tj_8lhhwJRZ7RXeJLYujd-3j7Rr8JQO2sSGgJs,44
42
44
  neva/security/hashing/hashers/argon2.py,sha256=pJqTl3_oCJCjJarnQwNSFRznlaijjbjdDVpC0DXDoSg,1402
43
45
  neva/security/hashing/hashers/bcrypt.py,sha256=H_15r-z9DYkd4HEbTz0QA4NHVBiFDXEqHkngP3HMSBY,1083
@@ -72,6 +74,6 @@ neva/testing/fakes.py,sha256=ffAAZC7WymuutVYk3vefuPDUvpNHav2QV2lj-Gw0Kic,2666
72
74
  neva/testing/fixtures.py,sha256=-l_Drw6nXTD_cKpfg1Z1pmsXnaD3NM2kihdRHxSkfzE,1657
73
75
  neva/testing/http.py,sha256=9oKNzaz38nCKkL-ZUF5CRWFPqpNmXkqbYcoukC509_Q,494
74
76
  neva/testing/test_case.py,sha256=FzXBHctkVkFNu46S5lnt7iRX91TLPLfL1Lzgn-iU-_E,3325
75
- python_neva-2.2.1.dist-info/METADATA,sha256=XhKt4y0UPn7sq-dj0xaKMi_gHIe3RB3TzLwrEtQfKcQ,627
76
- python_neva-2.2.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
77
- python_neva-2.2.1.dist-info/RECORD,,
77
+ python_neva-2.4.0.dist-info/METADATA,sha256=XyVNgtWT8cYqCGaFuRTgviKMZTOF7aKJaGTZz8nkUWc,691
78
+ python_neva-2.4.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
79
+ python_neva-2.4.0.dist-info/RECORD,,