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 +39 -6
- neva/guidelines/fragments/configuration.md +14 -3
- neva/guidelines/fragments/drivers.md +1 -1
- {python_neva-5.4.0.dist-info → python_neva-5.4.1.dist-info}/METADATA +1 -1
- {python_neva-5.4.0.dist-info → python_neva-5.4.1.dist-info}/RECORD +7 -7
- {python_neva-5.4.0.dist-info → python_neva-5.4.1.dist-info}/WHEEL +0 -0
- {python_neva-5.4.0.dist-info → python_neva-5.4.1.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
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
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
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.
|
|
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,5 +1,5 @@
|
|
|
1
1
|
neva/arch/__init__.py,sha256=S9I9HUL9G-ylfzMdIfhLS8ux1heyFSti1auXpKvZrEY,523
|
|
2
|
-
neva/arch/application.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
114
|
-
python_neva-5.4.
|
|
115
|
-
python_neva-5.4.
|
|
116
|
-
python_neva-5.4.
|
|
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,,
|
|
File without changes
|
|
File without changes
|