python-neva 5.4.0__py3-none-any.whl → 5.4.1__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
@@ -68,6 +68,7 @@ class Application:
68
68
  self._extra_providers: list[BaseProvider] = []
69
69
  self._container: dishka.AsyncContainer | None = None
70
70
  self._booted: bool = False
71
+ self._declarations_applied: bool = False
71
72
 
72
73
  configuration_path = _resolve_config_path(config_path)
73
74
  match ConfigLoader(configuration_path).load_all():
@@ -164,6 +165,15 @@ class Application:
164
165
  would orphan. Registering after boot is an error rather than a
165
166
  silently ignored call.
166
167
 
168
+ A provider arriving after construction has its declared config
169
+ namespaces validated here, and its secrets marked once it registers, so
170
+ that it is held to the same shape as one named in the `providers`
171
+ config namespace. Validation runs before the provider is instantiated:
172
+ a malformed namespace leaves nothing registered and no secret marked.
173
+ Providers registered *during* construction are skipped here and
174
+ validated together by `_apply_provider_config_declarations`, which
175
+ reports every bad namespace in one message.
176
+
167
177
  Returns:
168
178
  Result containing the registered provider instance or an error message.
169
179
  """
@@ -177,12 +187,18 @@ class Application:
177
187
  "or declare them in the 'providers' config namespace."
178
188
  )
179
189
 
190
+ late = self._declarations_applied
191
+ if late and (failures := self._declaration_failures(provider)):
192
+ return Err("Invalid configuration: " + "; ".join(failures))
193
+
180
194
  registered = (
181
195
  provider(self)
182
196
  .register()
183
197
  .map(lambda p: self.providers.setdefault(provider, p))
184
198
  )
185
199
  if registered.is_ok:
200
+ if late:
201
+ self.config.mark_sensitive(*provider.sensitive)
186
202
  self._container = None
187
203
  return registered
188
204
 
@@ -194,22 +210,39 @@ class Application:
194
210
  configuration error. Every failure is reported together, so a boot
195
211
  naming three bad namespaces takes one run rather than three.
196
212
 
213
+ Providers registered after this point go through `register`, which
214
+ applies the same declarations one provider at a time.
215
+
197
216
  Raises:
198
217
  RuntimeError: If any declared namespace fails its shape.
199
218
  """
200
- checks: list[Result[None, str]] = []
219
+ failures: list[str] = []
201
220
  for provider in self.providers.values():
202
221
  declaring = type(provider)
203
222
  self.config.mark_sensitive(*declaring.sensitive)
204
- checks.extend(
205
- self._validate_namespace(namespace, schema)
206
- for namespace, schema in declaring.config_schema.items()
207
- )
223
+ failures.extend(self._declaration_failures(declaring))
208
224
 
209
- _, failures = partition(checks)
210
225
  if failures:
211
226
  raise RuntimeError("Invalid configuration: " + "; ".join(failures))
212
227
 
228
+ self._declarations_applied = True
229
+
230
+ def _declaration_failures(self, declaring: type[ServiceProvider]) -> list[str]:
231
+ """Check one provider's declared namespaces, applying nothing.
232
+
233
+ Args:
234
+ declaring: The provider type whose `config_schema` to check.
235
+
236
+ Returns:
237
+ One message per malformed namespace, empty when every declared
238
+ namespace is absent or valid.
239
+ """
240
+ _, failures = partition(
241
+ self._validate_namespace(namespace, schema)
242
+ for namespace, schema in declaring.config_schema.items()
243
+ )
244
+ return failures
245
+
213
246
  def _validate_namespace(
214
247
  self, namespace: str, schema: TypeForm[Any]
215
248
  ) -> Result[None, str]:
@@ -104,9 +104,9 @@ reaches the repository, frozen or not.
104
104
 
105
105
  ## Validating a namespace at boot
106
106
 
107
- A provider declares the shape its namespace must have, and it is checked once during
108
- construction (since 5.4). The declaration reuses the `TypedDict` shapes above — nothing needs
109
- to become a pydantic model.
107
+ A provider declares the shape its namespace must have, and it is checked at boot (since 5.4).
108
+ The declaration reuses the `TypedDict` shapes above — nothing needs to become a pydantic
109
+ model.
110
110
 
111
111
  ```python
112
112
  class DatabaseServiceProvider(ServiceProvider):
@@ -120,10 +120,21 @@ class DatabaseServiceProvider(ServiceProvider):
120
120
  - A provider that declares nothing is validated not at all — the behaviour before 5.4.
121
121
  - The constructor raises, as it does for any config failure; `Application.create()` returns it
122
122
  as an `Err` instead.
123
+ - **A provider registered after construction is checked too, and reports as an `Err` from
124
+ `Application.register` rather than raising** (since 5.4.1). That is the path
125
+ `App.register` on `neva-fastapi` and `neva-faststream` delegates to; before 5.4.1 it skipped
126
+ validation and marked no secrets. A rejected provider is not registered at all.
127
+
128
+ **`type_` is a cast, not validation, so the value a malformed key holds is still whatever the
129
+ file put there.** Validation only *rejects* it — nothing is coerced and written back, and
130
+ pydantic's default lax mode accepts `"false"` for a `bool`. A namespace whose values must be
131
+ exactly typed declares `__pydantic_config__ = ConfigDict(strict=True)` on its `TypedDict`.
123
132
 
124
133
  ## Secrets
125
134
 
126
135
  A provider marks the keys that hold secrets, and they are replaced in the display form only.
136
+ Marking happens wherever the provider is registered — at construction, or through
137
+ `Application.register` (since 5.4.1).
127
138
 
128
139
  ```python
129
140
  class SecurityProvider(ServiceProvider):
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  id: drivers
3
3
  title: Swappable drivers
4
- requires: python-neva>=5.3
4
+ requires: python-neva>=5.0
5
5
  triggers: [swappable backend, driver manager, multiple implementations, choosing an implementation from config, registering a driver, plugin extension point]
6
6
  priority: 70
7
7
  verified_by: [tests/support/test_strategy.py, tests/support/test_strategy_imports.py]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-neva
3
- Version: 5.4.0
3
+ Version: 5.4.1
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: aiosqlite>=0.20.0
@@ -1,5 +1,5 @@
1
1
  neva/arch/__init__.py,sha256=S9I9HUL9G-ylfzMdIfhLS8ux1heyFSti1auXpKvZrEY,523
2
- neva/arch/application.py,sha256=g34eAOVNI87rW8RF0U89NnTgmOr5_KNqTuk96XfMduI,14220
2
+ neva/arch/application.py,sha256=9vZoN05D7qCP24WOYjZVvw1jd0ET5579Ng1IKxAJahQ,15728
3
3
  neva/arch/config.py,sha256=F_4TNIYJpqnAbQrfeYsKKHK9Py6L0ew1UkW0P9YkFro,1478
4
4
  neva/arch/facade.py,sha256=6flXZFgWb_n46NEO2DPXg7s6f89wMAHmogRJ81bN7c8,7985
5
5
  neva/arch/markers.py,sha256=STdhmW88qV8uogIa9kSD9gs-uMRaglZI8HOw3PrhX1Q,103
@@ -36,9 +36,9 @@ neva/events/contracts/listener.py,sha256=0xwPrjprkMzXu9vY1Z-EjSLFdvOUpomiE65hH16
36
36
  neva/guidelines/__init__.py,sha256=YY5RUpzw8AOuQQmMgNEOUUYiOSEaJPXJtyOicnkTVWY,148
37
37
  neva/guidelines/manifest.py,sha256=FE1I5-UEUcMpYwmIjvel6aEf25AMlap-oT9vw-8MUfg,662
38
38
  neva/guidelines/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- neva/guidelines/fragments/configuration.md,sha256=DnumMDixu0a5PtHClzPHG5QNL8pY89OlIareBeTwmZ0,7188
39
+ neva/guidelines/fragments/configuration.md,sha256=IaJ31s0AU4tze4FvqwyukrAljsH3wA6ZliRlFkOl2vU,8010
40
40
  neva/guidelines/fragments/database-transactions.md,sha256=i0IBbz_DZ19w7Sz_e8t-sAwYQiVU55GJRxM11azjGcM,5941
41
- neva/guidelines/fragments/drivers.md,sha256=YbvJcYQ5Vj1JGCx52q2YEPVDmv2Kt8dxvLVKcQcqTQg,3591
41
+ neva/guidelines/fragments/drivers.md,sha256=yyirWyCMsQUToR9BzWMfccov9eKTkKDsL0JtDKrr400,3591
42
42
  neva/guidelines/fragments/events.md,sha256=h4WAo8I5_6FC2kz_IWuqS34Q6XU5zK0PIHQukhsRkHs,7089
43
43
  neva/guidelines/fragments/facades.md,sha256=4GCoeUGCiuduWY0msgZO0yJ8MOfDwqqQfm_WsWvjHq8,4308
44
44
  neva/guidelines/fragments/factories.md,sha256=4ajYgtN1z4Fwb55FnXw7ROGQUrhqE4N9sgGHbg7UUmA,3101
@@ -110,7 +110,7 @@ neva/testing/fakes.py,sha256=w2dnFaO6x14l0_si7iBNELMCxaFi8VsVOSQ6PmTlclE,2936
110
110
  neva/testing/fixtures.py,sha256=oiPa5ntUkW-SbySDvwEHXtti8NqSwI-R0CT1_ezTx_Y,1445
111
111
  neva/testing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
112
112
  neva/testing/test_case.py,sha256=GdIKrHz9ZuirJybUuaXbW8o-PhOzkvg-pJwe8PAwFD4,9789
113
- python_neva-5.4.0.dist-info/METADATA,sha256=vyRJ0DLLLt4vevzTTs07KpUHk3sspu36tx063ZlzjKE,7352
114
- python_neva-5.4.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
115
- python_neva-5.4.0.dist-info/entry_points.txt,sha256=DmWZ-qNgvl4eUAXxwX4iLgb38e6X-zU8JDx_brlmkyk,45
116
- python_neva-5.4.0.dist-info/RECORD,,
113
+ python_neva-5.4.1.dist-info/METADATA,sha256=EXjysaABKXk9Rw4ycPfWFZroMzWWBbKEdXic6IE5hig,7352
114
+ python_neva-5.4.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
115
+ python_neva-5.4.1.dist-info/entry_points.txt,sha256=DmWZ-qNgvl4eUAXxwX4iLgb38e6X-zU8JDx_brlmkyk,45
116
+ python_neva-5.4.1.dist-info/RECORD,,