python-neva 5.0.0__py3-none-any.whl → 5.3.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/database/connection.py +3 -6
- neva/guidelines/fragments/facades.md +95 -0
- neva/guidelines/fragments/factories.md +79 -0
- neva/guidelines/fragments/observability.md +28 -14
- neva/guidelines/fragments/security.md +142 -0
- neva/guidelines/fragments/service-providers.md +37 -5
- neva/guidelines/fragments/testing.md +30 -3
- neva/security/__init__.py +18 -2
- neva/security/hashing/__init__.py +4 -0
- neva/support/facade/log.pyi +6 -1
- neva/testing/test_case.py +40 -4
- {python_neva-5.0.0.dist-info → python_neva-5.3.0.dist-info}/METADATA +7 -17
- {python_neva-5.0.0.dist-info → python_neva-5.3.0.dist-info}/RECORD +15 -12
- {python_neva-5.0.0.dist-info → python_neva-5.3.0.dist-info}/WHEEL +0 -0
- {python_neva-5.0.0.dist-info → python_neva-5.3.0.dist-info}/entry_points.txt +0 -0
neva/database/connection.py
CHANGED
|
@@ -7,7 +7,6 @@ from dataclasses import dataclass, field
|
|
|
7
7
|
from typing import final
|
|
8
8
|
|
|
9
9
|
from sqlalchemy.ext import asyncio
|
|
10
|
-
from structlog.stdlib import get_logger
|
|
11
10
|
|
|
12
11
|
from neva.database.transaction import BoundTransaction, TransactionState
|
|
13
12
|
from neva.obs import LogManager
|
|
@@ -87,7 +86,7 @@ class ConnectionManager:
|
|
|
87
86
|
self,
|
|
88
87
|
name: str,
|
|
89
88
|
tx_context: TransactionContext,
|
|
90
|
-
logger: LogManager
|
|
89
|
+
logger: LogManager,
|
|
91
90
|
engine: asyncio.AsyncEngine,
|
|
92
91
|
) -> None:
|
|
93
92
|
self.name = name
|
|
@@ -149,12 +148,10 @@ class ConnectionManager:
|
|
|
149
148
|
)
|
|
150
149
|
label = "commit" if committed else "rollback"
|
|
151
150
|
# A post-commit callback cannot un-commit the transaction, so its
|
|
152
|
-
# failure is reported rather than raised.
|
|
153
|
-
# back to a module logger when the manager has none.
|
|
154
|
-
logger = self.logger if self.logger is not None else get_logger(__name__)
|
|
151
|
+
# failure is reported rather than raised.
|
|
155
152
|
for result in results:
|
|
156
153
|
if result.is_err:
|
|
157
|
-
logger.error(
|
|
154
|
+
self.logger.error(
|
|
158
155
|
f"{label} callback failed",
|
|
159
156
|
error=result.err().unwrap(),
|
|
160
157
|
connection=self.name,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: facades
|
|
3
|
+
title: Facades
|
|
4
|
+
requires: python-neva>=5.0
|
|
5
|
+
triggers: [Log facade, DB facade, static access to a service, adding a facade, facade root, why does my facade raise AttributeError, pyi stub]
|
|
6
|
+
priority: 25
|
|
7
|
+
verified_by: [tests/arch/test_facade_resolution.py, tests/arch/test_facade_root_nesting.py, tests/testing/test_facade_restore.py]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Facades
|
|
11
|
+
|
|
12
|
+
A static front for a container-bound service. `Log.info(...)` resolves `LogManager` from the
|
|
13
|
+
application and calls `info` on it.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from neva.support.facade import App, Config, Crypt, DB, Event, Hash, Log
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Each names one service through `get_facade_accessor()`. Nothing else is special: a facade holds
|
|
20
|
+
no state and caches no instance.
|
|
21
|
+
|
|
22
|
+
## When to use one
|
|
23
|
+
|
|
24
|
+
Use a facade for genuinely cross-cutting access — logging, config, the ambient transaction.
|
|
25
|
+
**Inject the service anywhere it is part of a unit's contract.** A class taking
|
|
26
|
+
`DatabaseManager` in its constructor says what it needs and can be unit-tested by passing a
|
|
27
|
+
double; the same class reaching for `DB` hides the dependency and can only be tested by
|
|
28
|
+
installing a global. Actions, repositories and services take constructor parameters.
|
|
29
|
+
|
|
30
|
+
## Resolution is per access
|
|
31
|
+
|
|
32
|
+
`FacadeMeta.__getattr__` resolves the root on **every** attribute access. A swap installed
|
|
33
|
+
mid-test therefore takes effect immediately, and a facade never pins a stale instance across a
|
|
34
|
+
rebuild.
|
|
35
|
+
|
|
36
|
+
The cost is that failures arrive as `AttributeError`, not as a `Result`:
|
|
37
|
+
|
|
38
|
+
| Situation | What you get |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| No application set | `AttributeError: A facade root (App instance) has not been set for <Facade>. Call Facade.set_facade_application(app) first.` |
|
|
41
|
+
| Service not bound | `AttributeError` carrying the container's resolution error. |
|
|
42
|
+
| Attribute the service lacks | `AttributeError: 'LogManager' object has no attribute '...'` |
|
|
43
|
+
|
|
44
|
+
An `AttributeError` from a facade almost always means a missing provider, not a typo. Check the
|
|
45
|
+
`providers` config namespace first.
|
|
46
|
+
|
|
47
|
+
**A swap bypasses the container but not the root check.** `DB.swap(manager)` outside a booted
|
|
48
|
+
application still raises "a facade root has not been set" — the double is only consulted once
|
|
49
|
+
an application is present. A test installing a double therefore needs an application anyway,
|
|
50
|
+
which is what `TestCase` and the `application` fixture provide.
|
|
51
|
+
|
|
52
|
+
## The facade root
|
|
53
|
+
|
|
54
|
+
`Application.lifespan()` sets the root on entry and restores it on exit — you never call
|
|
55
|
+
`set_facade_application` yourself outside a bespoke harness.
|
|
56
|
+
|
|
57
|
+
The root is a single process-global slot, and teardown **hands it back rather than clearing
|
|
58
|
+
it**: an application booted inside a longer-lived one restores the outer application on exit,
|
|
59
|
+
and only the outermost teardown leaves it unset. Without that, a short-lived inner application
|
|
60
|
+
would disarm every facade for whatever the outer one still had to do — exactly what a test
|
|
61
|
+
harness does when its application outlives a single test.
|
|
62
|
+
|
|
63
|
+
## Type checking needs a stub
|
|
64
|
+
|
|
65
|
+
Forwarding through `__getattr__` is invisible to a type checker, so every facade ships a
|
|
66
|
+
sibling `.pyi` declaring its methods as classmethods (`neva/support/facade/log.pyi`). Two
|
|
67
|
+
consequences:
|
|
68
|
+
|
|
69
|
+
- **Adding a method to a service does not add it to the facade's public API.** Update the stub
|
|
70
|
+
in the same change, or callers get an error on a call that works at runtime.
|
|
71
|
+
- A stub can drift the other way too — the events fragment documents one such gap, where the
|
|
72
|
+
dispatcher accepts a list its contract does not declare.
|
|
73
|
+
|
|
74
|
+
## Adding a facade
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
class Cache(Facade):
|
|
78
|
+
@classmethod
|
|
79
|
+
@override
|
|
80
|
+
def get_facade_accessor(cls) -> type:
|
|
81
|
+
return CacheManager
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Import the service inside the method when the module would otherwise import at package-import
|
|
85
|
+
time — the shipped facades do this to keep `neva.support.facade` cheap. Bind the service in a
|
|
86
|
+
provider, write the `.pyi`, and export it from your facade package.
|
|
87
|
+
|
|
88
|
+
## Testing
|
|
89
|
+
|
|
90
|
+
Doubles are installed on the concrete facade, never on `Facade` itself, and live in one global
|
|
91
|
+
registry keyed by facade class. See the testing fragment for the full table — `fake()`,
|
|
92
|
+
`swap()`, `spy()`, `faking()`, `restore()`, `restore_all()`.
|
|
93
|
+
|
|
94
|
+
`TestCase` calls `Facade.restore_all()` after every test. Outside it, prefer `faking()` so a
|
|
95
|
+
failing assertion cannot leak a double into the next test.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: factories
|
|
3
|
+
title: Model factories
|
|
4
|
+
requires: python-neva>=5.0
|
|
5
|
+
triggers: [building test data, ModelFactory, polyfactory, seeding a model, create_async, factory for a SQLAlchemy model]
|
|
6
|
+
priority: 65
|
|
7
|
+
verified_by: [tests/polyfactory/test_model_factory.py]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Model factories
|
|
11
|
+
|
|
12
|
+
`ModelFactory` builds SQLAlchemy models for tests and seeds, persisting them through the
|
|
13
|
+
**caller's** transaction. It is a thin base over Polyfactory's `SQLAlchemyFactory`.
|
|
14
|
+
|
|
15
|
+
Ships behind an extra — `python-neva[polyfactory]`. Importing `neva.polyfactory` without it
|
|
16
|
+
fails, so it belongs in a dev dependency group, not in application code paths.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from neva.polyfactory import ModelFactory
|
|
20
|
+
|
|
21
|
+
class ActorFactory(ModelFactory[Actor]):
|
|
22
|
+
__model__ = Actor
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Persisting
|
|
26
|
+
|
|
27
|
+
`create_async()` and `create_batch_async(n)` write through the ambient session — they read it
|
|
28
|
+
from the `DB` facade rather than opening a transaction of their own, and they **flush without
|
|
29
|
+
committing**. The enclosing block still owns settling, so a factory call inside a rolled-back
|
|
30
|
+
test leaves nothing behind.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
async with DB.begin() as tx:
|
|
34
|
+
actor = await ActorFactory.create_async()
|
|
35
|
+
actors = await ActorFactory.create_batch_async(3)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**There must be an open transaction.** Outside one, `create_async` raises `UnwrapError` from
|
|
39
|
+
the unwrapped `DB.session()` rather than quietly opening its own — the same rule the database
|
|
40
|
+
fragment states for actions. Under `RefreshDatabase` the wrapper transaction already satisfies
|
|
41
|
+
this.
|
|
42
|
+
|
|
43
|
+
`build()` and `batch()` are inherited unchanged and touch no database. Use them when the model
|
|
44
|
+
never needs to be persisted.
|
|
45
|
+
|
|
46
|
+
## Relationships are not populated — but foreign keys still are
|
|
47
|
+
|
|
48
|
+
`__set_relationships__` is `False` on the base, so a factory never sets a relationship
|
|
49
|
+
attribute and never invents the related row.
|
|
50
|
+
|
|
51
|
+
**It does still fill the foreign-key column**, because that is an ordinary column: a required
|
|
52
|
+
`author_id` comes back a random integer pointing at no row. SQLite does not check the
|
|
53
|
+
constraint by default and lets it through; Postgres rejects it. This is the failure to expect
|
|
54
|
+
when a factory that passed locally breaks against a real database.
|
|
55
|
+
|
|
56
|
+
Always supply the parent — the relationship or the id — rather than letting the factory invent
|
|
57
|
+
one:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
author = await AuthorFactory.create_async()
|
|
61
|
+
book = await BookFactory.create_async(author=author) # or author_id=author.id
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For a nullable key with no parent in the scenario, say so explicitly:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
book = await BookFactory.create_async(author_id=None)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Set `__set_relationships__ = True` on one factory where a fixture genuinely wants the whole
|
|
71
|
+
graph built for it.
|
|
72
|
+
|
|
73
|
+
## Rules
|
|
74
|
+
|
|
75
|
+
- One factory per model, named `<Model>Factory`, beside the component's other test support.
|
|
76
|
+
- Never call `session.commit()` from a factory or a seeder — the caller's block settles.
|
|
77
|
+
- Do not give a factory its own `DB.begin()`; it inherits the caller's unit of work.
|
|
78
|
+
- Override a field with a keyword argument rather than editing the instance afterwards, so the
|
|
79
|
+
value is present when the row is flushed.
|
|
@@ -4,14 +4,16 @@ title: Observability
|
|
|
4
4
|
requires: python-neva>=5.0
|
|
5
5
|
triggers: [logging, Log facade, log channel, structured logging, log level, JSON logs, audit log, obs config, tracing, OpenTelemetry, metrics, correlation id]
|
|
6
6
|
priority: 55
|
|
7
|
-
verified_by: [tests/obs/test_channels.py, tests/obs/test_resolver.py, tests/obs/test_manager.py, tests/obs/test_facade.py, tests/obs/test_provider_hook.py]
|
|
7
|
+
verified_by: [tests/obs/test_channels.py, tests/obs/test_resolver.py, tests/obs/test_manager.py, tests/obs/test_facade.py, tests/obs/test_provider_hook.py, tests/support/test_strategy.py]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Observability
|
|
11
11
|
|
|
12
|
-
The core ships **logging only**. Tracing, metrics, exporters and instrumentors live in
|
|
13
|
-
|
|
14
|
-
There is no `Trace` facade, no tracer, and no OpenTelemetry dependency
|
|
12
|
+
The core ships **logging only**. Tracing, metrics, exporters and instrumentors live in the
|
|
13
|
+
`neva-otel` plugin (`python-neva[otel]`), so installing the core commits an application to no
|
|
14
|
+
observability backend. There is no `Trace` facade, no tracer, and no OpenTelemetry dependency
|
|
15
|
+
here — do not add one. Asked for tracing, install the plugin; never reach for
|
|
16
|
+
`opentelemetry-*` inside this package.
|
|
15
17
|
|
|
16
18
|
## Channels
|
|
17
19
|
|
|
@@ -57,14 +59,25 @@ Log.info("order placed", order_id=42) # default channel
|
|
|
57
59
|
Log.channel("audit").warning("role granted", actor="root")
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
Level methods
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
Level methods are `debug`, `info`, `warning`, `error`, `critical` and `exception`, and go to the
|
|
63
|
+
default channel. **Use `Log.exception(...)` inside an `except` block** — it attaches the
|
|
64
|
+
traceback, which `Log.error(str(e))` throws away. Everything after the message is structured
|
|
65
|
+
context, not format arguments — never build the message with an f-string when the value belongs
|
|
66
|
+
in a field.
|
|
63
67
|
|
|
64
|
-
`Log.channel(name)` **raises
|
|
65
|
-
A typo is a configuration mistake, and serving the default instead would hide it
|
|
66
|
-
working output. Where the failure has to be a value,
|
|
67
|
-
`
|
|
68
|
+
`Log.channel(name)` **raises `UnwrapError`** on a name no channel is configured for, carrying
|
|
69
|
+
the reason. A typo is a configuration mistake, and serving the default instead would hide it
|
|
70
|
+
behind working output. Where the failure has to be a value, go through the resolver:
|
|
71
|
+
`Log.channels` returns `Option[ChannelResolver]` — `Nothing` for a manager built with no
|
|
72
|
+
application — and the resolver's `use(name)` returns `Result[Channel, str]`.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
channel = Log.channels.and_then(lambda r: r.use("audit").ok())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Three failures are told apart by their message: no channel of that name, an unknown `driver`
|
|
79
|
+
(the error lists the drivers that are registered), and a `stack` channel that reaches itself,
|
|
80
|
+
which reports being "defined in terms of itself" rather than recursing.
|
|
68
81
|
|
|
69
82
|
## Ambient context
|
|
70
83
|
|
|
@@ -80,8 +93,8 @@ around — `neva-asgi`'s middleware binds it the same way.
|
|
|
80
93
|
## Enriching records from a plugin
|
|
81
94
|
|
|
82
95
|
`LogManager.processor(fn)` appends a structlog processor to **every** channel's chain. This is
|
|
83
|
-
the seam a plugin adds derived fields through —
|
|
84
|
-
|
|
96
|
+
the seam a plugin adds derived fields through — `neva-otel` injects the current span's ids
|
|
97
|
+
this way.
|
|
85
98
|
|
|
86
99
|
```python
|
|
87
100
|
class MyProvider(ServiceProvider):
|
|
@@ -100,7 +113,8 @@ Records emitted by base providers' own startup are not enriched — their lifesp
|
|
|
100
113
|
a plugin's. Everything after is. Already-built channels are discarded on registration, so the
|
|
101
114
|
next resolution picks the processor up.
|
|
102
115
|
|
|
103
|
-
Custom drivers register the same way, through `resolver.driver(name, builder)
|
|
116
|
+
Custom drivers register the same way, through `resolver.driver(name, builder)` — the
|
|
117
|
+
resolver is a `StrategyResolver`, described in the service-providers fragment.
|
|
104
118
|
|
|
105
119
|
## Rules
|
|
106
120
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: security
|
|
3
|
+
title: Hashing, tokens and encryption
|
|
4
|
+
requires: python-neva>=5.0
|
|
5
|
+
triggers: [hashing a password, checking a password, generating an API key, storing a token, encrypting a value, key rotation, app.key, Hash facade, Crypt facade]
|
|
6
|
+
priority: 45
|
|
7
|
+
verified_by: [tests/security/test_hash_manager.py, tests/security/test_encrypter.py, tests/security/test_tokens.py, tests/security/test_config_shapes.py, tests/support/test_strategy.py]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Hashing, tokens and encryption
|
|
11
|
+
|
|
12
|
+
Three separate concerns with three separate tools. Reaching for the wrong one is the mistake
|
|
13
|
+
this fragment exists to prevent — they are not interchangeable, and two of the three pairings
|
|
14
|
+
are outright vulnerabilities.
|
|
15
|
+
|
|
16
|
+
| Secret | Tool | Why |
|
|
17
|
+
| ------ | ---- | --- |
|
|
18
|
+
| A user's password | `Hash` (argon2/bcrypt) | Human-chosen, low entropy. Needs a deliberately slow, salted KDF. |
|
|
19
|
+
| A token you generated | `generate_token` + `hash_token` | Already high-entropy. A fast digest is correct and a KDF is pure cost. |
|
|
20
|
+
| Data you must read back | `Crypt` (AES-256-GCM) | Reversible. Never a hash. |
|
|
21
|
+
|
|
22
|
+
Never hash a password with `hash_token` — SHA-256 is unsalted and fast, so a stolen table is
|
|
23
|
+
brute-forceable. Never encrypt a password with `Crypt` — a password must not be recoverable.
|
|
24
|
+
|
|
25
|
+
Everything here comes from `SecurityProvider`, which binds `HashManager` and `Encrypter`. It is
|
|
26
|
+
**not** a base provider: list it in the `providers` config namespace or `Hash` and `Crypt`
|
|
27
|
+
resolve to nothing.
|
|
28
|
+
|
|
29
|
+
## Hashing passwords
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from neva.support.facade import Hash
|
|
33
|
+
|
|
34
|
+
hashed = await Hash.make_async(password)
|
|
35
|
+
if await Hash.check_async(password, user.password):
|
|
36
|
+
...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**In async code use `make_async` / `check_async`.** Hashing is deliberately CPU- and
|
|
40
|
+
memory-expensive — argon2 defaults to 100 MiB and tens of milliseconds — so the sync `make` and
|
|
41
|
+
`check` stall the whole event loop, blocking every other request for the duration. The async
|
|
42
|
+
pair hands the work to a thread (`asyncio.to_thread`); they are otherwise identical.
|
|
43
|
+
|
|
44
|
+
One `hashing` namespace, drivers `argon2` and `bcrypt`. `driver` is the only required key; both
|
|
45
|
+
tuning namespaces are optional and every field has a default.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from neva.security import HashingConfig
|
|
49
|
+
|
|
50
|
+
config: HashingConfig = {"driver": "argon2", "argon": {"time_cost": 4}}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Key | Default |
|
|
54
|
+
| --- | ------- |
|
|
55
|
+
| `argon.time_cost` | 2 |
|
|
56
|
+
| `argon.memory_cost` | 102400 (KiB) |
|
|
57
|
+
| `argon.parallelism` | 8 |
|
|
58
|
+
| `argon.hash_len` | 16 |
|
|
59
|
+
| `argon.salt_len` | 16 |
|
|
60
|
+
| `bcrypt.rounds` | 12 |
|
|
61
|
+
| `bcrypt.prefix` | `"2b"` |
|
|
62
|
+
|
|
63
|
+
**A missing `hashing` namespace does not fail boot.** `HashManager` resolves fine and the
|
|
64
|
+
failure surfaces at the first call, as `UnwrapError: Failed to resolve hasher 'default'` — the
|
|
65
|
+
same shape as an unknown driver name. If you see it, check the namespace before anything else.
|
|
66
|
+
Passing `hasher="bcrypt"` explicitly bypasses the default and works regardless.
|
|
67
|
+
|
|
68
|
+
`Hash.needs_rehash(hashed)` reports whether a stored hash predates the current tuning. Check it
|
|
69
|
+
on successful login and re-hash there — it is the only moment the plaintext is available.
|
|
70
|
+
|
|
71
|
+
## Tokens
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from neva.security import generate_token, hash_token, verify_token
|
|
75
|
+
|
|
76
|
+
token = generate_token() # give this to the caller, once
|
|
77
|
+
user.api_key = hash_token(token) # store only this
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`generate_token(nbytes=32)` draws from `secrets.token_urlsafe`. **`nbytes` is entropy, not
|
|
81
|
+
output length** — base64url makes the string roughly `4 * nbytes / 3` characters, so the default
|
|
82
|
+
yields 43. Do not shrink it to control the column width.
|
|
83
|
+
|
|
84
|
+
`hash_token` is a bare hex SHA-256, and `verify_token` compares with `hmac.compare_digest` so
|
|
85
|
+
the check is constant-time. Both accept `str` or `bytes` and agree on either. Store the hash,
|
|
86
|
+
never the token; a stolen database then yields nothing usable.
|
|
87
|
+
|
|
88
|
+
## Encryption
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from neva.support.facade import Crypt
|
|
92
|
+
|
|
93
|
+
match Crypt.encrypt({"iban": iban}):
|
|
94
|
+
case Ok(payload): ...
|
|
95
|
+
case Err(reason): ...
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
AES-256-GCM. `encrypt(value) -> Result[str, str]` takes any JSON-serialisable value — strings
|
|
99
|
+
round-trip as strings, everything else through JSON — and returns a base64 payload carrying its
|
|
100
|
+
own random IV, so the same input never produces the same ciphertext. `decrypt(payload) ->
|
|
101
|
+
Result[JsonValue, str]`.
|
|
102
|
+
|
|
103
|
+
**Both return `Result`; neither raises.** `DecryptionError` is exported but never raised
|
|
104
|
+
anywhere in the core — an `except DecryptionError` block is dead code. Handle the `Err`.
|
|
105
|
+
|
|
106
|
+
### Keys and rotation
|
|
107
|
+
|
|
108
|
+
`app.key` is a base64-encoded 32 bytes; `AesEncrypter.generate_key()` mints one. `app.previous_keys`
|
|
109
|
+
is a list of superseded keys.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
config: AppConfig = {"key": CURRENT, "previous_keys": [SUPERSEDED]}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Encryption always uses `app.key`. Decryption tries `app.key` first, then each previous key in
|
|
116
|
+
order, so rotating a key leaves existing ciphertext readable without a migration. Re-encrypt at
|
|
117
|
+
your own pace, then drop the old key from the list.
|
|
118
|
+
|
|
119
|
+
**Keys load lazily, on the first encrypt or decrypt.** A missing or malformed `app.key` raises
|
|
120
|
+
`ValueError` there, not at boot — an application with no key configured starts perfectly and
|
|
121
|
+
fails at the first secret it touches.
|
|
122
|
+
|
|
123
|
+
## Registering another hasher
|
|
124
|
+
|
|
125
|
+
`HashManager` is a `StrategyResolver` — see the service-providers fragment for the pattern and
|
|
126
|
+
its `use()` contract. Register a factory from a provider's `lifespan()`, never `register()`.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
manager = (await self.app.make_async(HashManager)).unwrap()
|
|
130
|
+
_ = manager.register("scrypt", lambda resolver: ScryptHasher())
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A hasher satisfies the `Hasher` protocol: `make`, `check`, `needs_rehash`.
|
|
134
|
+
|
|
135
|
+
## Pitfalls
|
|
136
|
+
|
|
137
|
+
- Sync `Hash.make` / `Hash.check` in async code. Use the `_async` pair.
|
|
138
|
+
- `SecurityProvider` left out of the `providers` namespace.
|
|
139
|
+
- Storing a token instead of `hash_token(token)`.
|
|
140
|
+
- Treating `nbytes` as the token's length.
|
|
141
|
+
- Expecting `Crypt` to raise. It returns `Err`.
|
|
142
|
+
- Assuming a missing `hashing` namespace or `app.key` fails at boot. Both fail at first use.
|
|
@@ -4,7 +4,7 @@ title: Service providers and bindings
|
|
|
4
4
|
requires: python-neva>=4.0
|
|
5
5
|
triggers: [registering a service, binding a dependency, provider.py, wiring a component, DI container, resolving a type]
|
|
6
6
|
priority: 20
|
|
7
|
-
verified_by: [tests/arch/test_lifetimes.py, tests/arch/test_registration.py, tests/arch/test_cache.py, tests/arch/test_scope.py, tests/arch/test_extends.py, tests/arch/test_register_resolution.py]
|
|
7
|
+
verified_by: [tests/arch/test_lifetimes.py, tests/arch/test_registration.py, tests/arch/test_cache.py, tests/arch/test_scope.py, tests/arch/test_extends.py, tests/arch/test_register_resolution.py, tests/support/test_strategy.py]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Service providers and bindings
|
|
@@ -95,6 +95,33 @@ the binding behind a marker.
|
|
|
95
95
|
- `when: ClassVar[Marker | None]` on the provider gates all of its bindings at once.
|
|
96
96
|
- `listen: ClassVar[dict[type[Event], list[type[EventListener]]]]` — see the events fragment.
|
|
97
97
|
|
|
98
|
+
## Strategy resolvers
|
|
99
|
+
|
|
100
|
+
Where a service picks one of several interchangeable implementations by **name** from config,
|
|
101
|
+
the core uses `StrategyResolver` (`neva.support.strategy`) rather than a second container.
|
|
102
|
+
`HashManager` and the logging `ChannelResolver` are both instances of it, so the contract is
|
|
103
|
+
worth knowing once:
|
|
104
|
+
|
|
105
|
+
| Call | Meaning |
|
|
106
|
+
| ---- | ------- |
|
|
107
|
+
| `use(name=None)` | `Result[T, str]`. Resolves, **caches**, and returns. `None` means the configured default. |
|
|
108
|
+
| `default()` | Abstract — each resolver reads its own config key (`hashing.driver`, `obs.logging.default`). |
|
|
109
|
+
| `register(name, factory)` | Add an implementation. The factory takes the resolver, so it can read config. |
|
|
110
|
+
| `resolve(name)` | Build without caching. Override to change what a name means. |
|
|
111
|
+
| `clear()` | Drop cached instances, so the next `use` rebuilds. |
|
|
112
|
+
|
|
113
|
+
Two failures come back as `Err`, never as an exception: `"No default strategy configured."`
|
|
114
|
+
when `default()` is `Nothing`, and `"No strategy registered with name '<name>'"`. A factory that
|
|
115
|
+
raises is caught and returned as `Err` naming the strategy.
|
|
116
|
+
|
|
117
|
+
**Register from `lifespan()`, not `register()`** — the resolver is a container-bound service,
|
|
118
|
+
so the rule above applies: one resolved during registration is discarded, and the
|
|
119
|
+
implementation registered on it silently never serves. `clear()` matters when adding a
|
|
120
|
+
processor or driver after something has already been resolved.
|
|
121
|
+
|
|
122
|
+
Use it for named, config-selected implementations. Use the container for everything else —
|
|
123
|
+
resolution there is by type, and a strategy resolver is not a general service locator.
|
|
124
|
+
|
|
98
125
|
## Startup and shutdown
|
|
99
126
|
|
|
100
127
|
Implement `lifespan()` and the provider is `Bootable` — a runtime-checkable Protocol, so no
|
|
@@ -125,10 +152,15 @@ request-scoped container to another framework wants that, not `container`.
|
|
|
125
152
|
|
|
126
153
|
## Registration is closed after boot
|
|
127
154
|
|
|
128
|
-
`register()`
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
`register()` **returns `Err` once the application has booted**: dishka validates the graph at
|
|
156
|
+
build time, and the booted container holds APP-scoped instances a rebuild would orphan. Declare
|
|
157
|
+
providers in config, or register before entering `lifespan()`.
|
|
158
|
+
|
|
159
|
+
One case slips past that, and the order is what makes it: the already-registered check is read
|
|
160
|
+
**first**, so re-registering a class the application already holds returns `Ok(existing)` even
|
|
161
|
+
booted. Nothing is rebuilt, so nothing is at risk. It does mean a test asserting the refusal
|
|
162
|
+
must pass a provider the application never registered: hand it one the config or the setup
|
|
163
|
+
already installed and the assertion comes back `Ok`, proving nothing.
|
|
132
164
|
|
|
133
165
|
## Typed config shapes
|
|
134
166
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
id: testing
|
|
3
3
|
title: Testing
|
|
4
|
-
requires: python-neva>=
|
|
5
|
-
triggers: [writing a test, TestCase, RefreshDatabase, faking a facade, database isolation in tests, create_config]
|
|
4
|
+
requires: python-neva>=5.2
|
|
5
|
+
triggers: [writing a test, TestCase, RefreshDatabase, faking a facade, database isolation in tests, create_config, boot_application]
|
|
6
6
|
priority: 60
|
|
7
|
-
verified_by: [tests/testing/test_test_case.py, tests/testing/test_application_reuse.py, tests/testing/test_create_config_migration.py, tests/testing/test_refresh_database.py, tests/testing/test_facade_restore.py, tests/testing/test_event_fake.py, tests/testing/test_fixtures.py]
|
|
7
|
+
verified_by: [tests/testing/test_test_case.py, tests/testing/test_boot_application.py, tests/testing/test_application_reuse.py, tests/testing/test_create_config_migration.py, tests/testing/test_refresh_database.py, tests/testing/test_facade_restore.py, tests/testing/test_event_fake.py, tests/testing/test_fixtures.py]
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Testing
|
|
@@ -66,6 +66,33 @@ naming the fix. There is no instance-method fallback and no deprecation path —
|
|
|
66
66
|
Omit the override entirely and the default writes a minimal `app.py` / `providers.py` into
|
|
67
67
|
`tmp_path`.
|
|
68
68
|
|
|
69
|
+
## boot_application picks the application, not just its config
|
|
70
|
+
|
|
71
|
+
`create_config` says what configuration the class runs on; `boot_application` says what
|
|
72
|
+
application runs over it, and which lifespan holds it open. Override it when the thing under
|
|
73
|
+
test **owns** an `Application` instead of being one — an HTTP app, whose own lifespan is what
|
|
74
|
+
seeds its per-request state — so entering `Application.lifespan()` would boot the container and
|
|
75
|
+
skip everything wrapped around it.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
class HttpTestCase(TestCase):
|
|
79
|
+
@override
|
|
80
|
+
@classmethod
|
|
81
|
+
@asynccontextmanager
|
|
82
|
+
async def boot_application(cls, config_path: Path) -> AsyncIterator[Application]:
|
|
83
|
+
webapp = App(config_path=config_path)
|
|
84
|
+
async with webapp.router.lifespan_context(webapp):
|
|
85
|
+
yield webapp.application
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Yield the application `self.app` and the facade root must point at. It is a **context manager**,
|
|
89
|
+
so `@classmethod` and `@asynccontextmanager` both belong on the override — a factory returning
|
|
90
|
+
an application would hand the lifespan back to `TestCase`. `FreshApplication` routes through the
|
|
91
|
+
same hook, once per test.
|
|
92
|
+
|
|
93
|
+
Registration order still holds: register providers and include routers *before* entering the
|
|
94
|
+
lifespan. `Application.register` returns `Err` once booted.
|
|
95
|
+
|
|
69
96
|
## RefreshDatabase
|
|
70
97
|
|
|
71
98
|
Mix it in **alongside** `TestCase`; it cannot boot an application alone.
|
neva/security/__init__.py
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
"""Security module for authentication and hashing."""
|
|
2
2
|
|
|
3
|
-
from neva.security.encryption import AesEncrypter, DecryptionError, Encrypter
|
|
4
|
-
from neva.security.hashing import
|
|
3
|
+
from neva.security.encryption import AesEncrypter, DecryptionError, Encrypter, JsonValue
|
|
4
|
+
from neva.security.hashing import (
|
|
5
|
+
Argon2Config,
|
|
6
|
+
Argon2Hasher,
|
|
7
|
+
BcryptConfig,
|
|
8
|
+
BcryptHasher,
|
|
9
|
+
Hasher,
|
|
10
|
+
HashingConfig,
|
|
11
|
+
HashManager,
|
|
12
|
+
)
|
|
5
13
|
from neva.security.provider import SecurityProvider
|
|
14
|
+
from neva.security.tokens import generate_token, hash_token, verify_token
|
|
6
15
|
|
|
7
16
|
|
|
8
17
|
__all__ = [
|
|
9
18
|
"AesEncrypter",
|
|
19
|
+
"Argon2Config",
|
|
10
20
|
"Argon2Hasher",
|
|
21
|
+
"BcryptConfig",
|
|
11
22
|
"BcryptHasher",
|
|
12
23
|
"DecryptionError",
|
|
13
24
|
"Encrypter",
|
|
14
25
|
"HashManager",
|
|
15
26
|
"Hasher",
|
|
27
|
+
"HashingConfig",
|
|
28
|
+
"JsonValue",
|
|
16
29
|
"SecurityProvider",
|
|
30
|
+
"generate_token",
|
|
31
|
+
"hash_token",
|
|
32
|
+
"verify_token",
|
|
17
33
|
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Hashing module for password hashing."""
|
|
2
2
|
|
|
3
|
+
from neva.security.hashing.config import Argon2Config, BcryptConfig, HashingConfig
|
|
3
4
|
from neva.security.hashing.hash_manager import HashManager
|
|
4
5
|
from neva.security.hashing.hashers.argon2 import Argon2Hasher
|
|
5
6
|
from neva.security.hashing.hashers.bcrypt import BcryptHasher
|
|
@@ -7,8 +8,11 @@ from neva.security.hashing.hashers.protocol import Hasher
|
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
11
|
+
"Argon2Config",
|
|
10
12
|
"Argon2Hasher",
|
|
13
|
+
"BcryptConfig",
|
|
11
14
|
"BcryptHasher",
|
|
12
15
|
"HashManager",
|
|
13
16
|
"Hasher",
|
|
17
|
+
"HashingConfig",
|
|
14
18
|
]
|
neva/support/facade/log.pyi
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"""Type stub for Log facade."""
|
|
2
2
|
|
|
3
|
-
from typing import override
|
|
3
|
+
from typing import ClassVar, override
|
|
4
4
|
|
|
5
5
|
from neva.arch import Facade
|
|
6
6
|
from neva.obs.logging.contracts import Channel, Processor
|
|
7
|
+
from neva.obs.logging.resolver import ChannelResolver
|
|
8
|
+
from neva.support import Option
|
|
7
9
|
|
|
8
10
|
class Log(Facade):
|
|
11
|
+
channels: ClassVar[Option[ChannelResolver]]
|
|
12
|
+
"""The resolver, for callers wanting a resolution failure as a value."""
|
|
13
|
+
|
|
9
14
|
@classmethod
|
|
10
15
|
@override
|
|
11
16
|
def get_facade_accessor(cls) -> type: ...
|
neva/testing/test_case.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
from collections.abc import AsyncIterator, Iterator
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import override
|
|
7
8
|
|
|
@@ -39,6 +40,10 @@ class TestCase:
|
|
|
39
40
|
|
|
40
41
|
A class that genuinely needs a fresh application per test -- one varying the
|
|
41
42
|
*shape* of its config rather than reading it -- inherits `FreshApplication`.
|
|
43
|
+
|
|
44
|
+
Two public override points: `create_config` for the configuration, and
|
|
45
|
+
`boot_application` for the application built over it and the lifespan it is
|
|
46
|
+
held open by.
|
|
42
47
|
"""
|
|
43
48
|
|
|
44
49
|
pytestmark = pytest.mark.asyncio(loop_scope="class")
|
|
@@ -103,6 +108,39 @@ class TestCase:
|
|
|
103
108
|
)
|
|
104
109
|
raise TypeError(msg)
|
|
105
110
|
|
|
111
|
+
@classmethod
|
|
112
|
+
@asynccontextmanager
|
|
113
|
+
async def boot_application(cls, config_path: Path) -> AsyncIterator[Application]:
|
|
114
|
+
"""Build the application under test and hold it booted.
|
|
115
|
+
|
|
116
|
+
The public override point for *what* a class tests and *how* it starts.
|
|
117
|
+
`create_config` decides the configuration; this decides the application.
|
|
118
|
+
Overriding it is how a downstream package tests something that owns an
|
|
119
|
+
`Application` rather than being one -- an HTTP app whose own lifespan has
|
|
120
|
+
to be the one entered, because that is what seeds its per-request state.
|
|
121
|
+
|
|
122
|
+
An override yields the `Application` the facades and `self.app` must point
|
|
123
|
+
at, from inside whatever lifespan it needs::
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
@asynccontextmanager
|
|
127
|
+
async def boot_application(cls, config_path):
|
|
128
|
+
webapp = App(config_path=config_path)
|
|
129
|
+
async with webapp.router.lifespan_context(webapp):
|
|
130
|
+
yield webapp.application
|
|
131
|
+
|
|
132
|
+
It is a context manager rather than a factory so that the lifespan stays
|
|
133
|
+
the subclass's to choose. A factory would leave `TestCase` entering
|
|
134
|
+
`Application.lifespan()` itself, which boots the container but skips any
|
|
135
|
+
lifespan wrapped around it.
|
|
136
|
+
|
|
137
|
+
Yields:
|
|
138
|
+
The booted application under test.
|
|
139
|
+
"""
|
|
140
|
+
app = Application(config_path=config_path)
|
|
141
|
+
async with app.lifespan():
|
|
142
|
+
yield app
|
|
143
|
+
|
|
106
144
|
@pytest_asyncio.fixture(scope="class", loop_scope="class")
|
|
107
145
|
@classmethod
|
|
108
146
|
async def _test_case_application(
|
|
@@ -113,8 +151,7 @@ class TestCase:
|
|
|
113
151
|
Yields:
|
|
114
152
|
Application instance with lifespan managed.
|
|
115
153
|
"""
|
|
116
|
-
|
|
117
|
-
async with app.lifespan():
|
|
154
|
+
async with cls.boot_application(_test_case_config) as app:
|
|
118
155
|
yield app
|
|
119
156
|
|
|
120
157
|
@pytest.fixture(autouse=True)
|
|
@@ -174,8 +211,7 @@ class FreshApplication(TestCase):
|
|
|
174
211
|
Yields:
|
|
175
212
|
A freshly built Application, torn down at the end of the test.
|
|
176
213
|
"""
|
|
177
|
-
|
|
178
|
-
async with app.lifespan():
|
|
214
|
+
async with self.boot_application(self._build_config(tmp_path)) as app:
|
|
179
215
|
yield app
|
|
180
216
|
|
|
181
217
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-neva
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.3.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: aiosqlite>=0.20.0
|
|
@@ -20,6 +20,8 @@ Provides-Extra: fastapi
|
|
|
20
20
|
Requires-Dist: neva-fastapi>=1.1.1; extra == 'fastapi'
|
|
21
21
|
Provides-Extra: faststream
|
|
22
22
|
Requires-Dist: faststream>=0.6.6; extra == 'faststream'
|
|
23
|
+
Provides-Extra: otel
|
|
24
|
+
Requires-Dist: neva-otel>=0.2.0; extra == 'otel'
|
|
23
25
|
Provides-Extra: polyfactory
|
|
24
26
|
Requires-Dist: polyfactory>=3.1.0; extra == 'polyfactory'
|
|
25
27
|
Provides-Extra: testing
|
|
@@ -52,7 +54,7 @@ independent repo, independently versioned and published.
|
|
|
52
54
|
| `neva-asgi` | `neva-asgi/` | **ASGI middleware** — correlation IDs and per-request profiling. Pure ASGI, so both the HTTP and messaging integrations can consume it. Pulled in via the `python-neva[asgi]` extra. | published ([PyPI](https://pypi.org/project/neva-asgi/)) |
|
|
53
55
|
| `neva-faststream` | `neva-faststream/` | FastStream (messaging) integration. | scaffolded, early placeholder |
|
|
54
56
|
| `neva-auth` | `neva-auth/` | Authentication & identity toolkit — token-issuance engine, OAuth2 grants, guards, pluggable formats/backends. | core complete, OAuth-server slice in progress |
|
|
55
|
-
| `neva-otel` | `neva-otel/` | **OpenTelemetry** — SDK wiring, tracer and meter providers, exporters, samplers and instrumentors. Kept out of the core so nothing is committed to an observability backend. |
|
|
57
|
+
| `neva-otel` | `neva-otel/` | **OpenTelemetry** — SDK wiring, tracer and meter providers, exporters, samplers and instrumentors. Kept out of the core so nothing is committed to an observability backend. Pulled in via the `python-neva[otel]` extra. | published ([PyPI](https://pypi.org/project/neva-otel/)) |
|
|
56
58
|
| `neva-boost` | `neva-boost/` | **Agent guidelines** — composes each installed package's versioned guideline fragments into a project's agent configuration (Claude Code skills or `AGENTS.md`). Dev-time tooling; pulled in via the `python-neva[boost]` extra. | published ([PyPI](https://pypi.org/project/neva-boost/)) |
|
|
57
59
|
| `neva-example` | `neva-example/` | Reference application consuming `python-neva[fastapi]`. Not published; demonstration + integration test bed. | unpublished |
|
|
58
60
|
|
|
@@ -65,20 +67,6 @@ independent repo, independently versioned and published.
|
|
|
65
67
|
Logging is a set of named channels, each with its own driver, level and output —
|
|
66
68
|
Laravel's `Log`, configured from one `config/obs.py` namespace.
|
|
67
69
|
|
|
68
|
-
```python
|
|
69
|
-
config = {
|
|
70
|
-
"logging": {
|
|
71
|
-
"default": "stdout",
|
|
72
|
-
"channels": {
|
|
73
|
-
"stdout": {"driver": "console", "level": "DEBUG"},
|
|
74
|
-
"json": {"driver": "json", "stream": "stderr", "level": "INFO"},
|
|
75
|
-
"audit": {"driver": "file", "path": "var/audit.log"},
|
|
76
|
-
"both": {"driver": "stack", "channels": ["json", "audit"]},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
70
|
```python
|
|
83
71
|
from neva.support.facade import Log
|
|
84
72
|
|
|
@@ -100,6 +88,9 @@ current span's ids on every record.
|
|
|
100
88
|
the core carries no OpenTelemetry dependency and commits no application to an
|
|
101
89
|
observability backend.
|
|
102
90
|
|
|
91
|
+
Channel configuration, the driver keys and the failure modes are documented in
|
|
92
|
+
`neva/guidelines/fragments/observability.md`, which CI checks against the tests.
|
|
93
|
+
|
|
103
94
|
## Develop
|
|
104
95
|
|
|
105
96
|
```bash
|
|
@@ -147,7 +138,6 @@ rewrite the new tag with the rendered changelog as its annotation.
|
|
|
147
138
|
- Feature-parity FastStream integration (now scaffolded as `neva-faststream`)
|
|
148
139
|
- Improved router registration (auto-discovery OR provider-based? both?)
|
|
149
140
|
- Improved security tooling (performance improvements, better defaults, etc.)
|
|
150
|
-
- OpenTelemetry as a plugin (`neva-otel`) — the core keeps logging and takes no OTel dependency
|
|
151
141
|
- Improved factory module (based on Polyfactory)
|
|
152
142
|
- Queue/Jobs system
|
|
153
143
|
- CLI integration
|
|
@@ -15,7 +15,7 @@ neva/config/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
15
15
|
neva/config/repository.py,sha256=p8PjTfjV23ioy_iyb5b6oYPnYq1bVe6q6gVRSegJr7Q,5505
|
|
16
16
|
neva/database/__init__.py,sha256=3yYnEe8HQM86tURyAhTbMtE7AXubIEMN3GuwxkkT-bk,508
|
|
17
17
|
neva/database/config.py,sha256=bVKUqlrYDFrURqg_FbntPXyVkbOUPSVXQJTKjyucjL0,451
|
|
18
|
-
neva/database/connection.py,sha256=
|
|
18
|
+
neva/database/connection.py,sha256=XPVrzJiGYtm1Za7RYaC11XZqVG_ezsH1CBsRyzF-pLw,7061
|
|
19
19
|
neva/database/manager.py,sha256=UNQh1xorvoBWHoNS5N8By7lxt5wtGtRixIcMhjZjzYI,4731
|
|
20
20
|
neva/database/provider.py,sha256=XCi8-mfJ5KDVpVO10YlNX4jt0a9ESSeTyn8o3ohOe9c,1701
|
|
21
21
|
neva/database/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -39,10 +39,13 @@ neva/guidelines/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
39
39
|
neva/guidelines/fragments/configuration.md,sha256=rrjxaZW6JPwAnJ1ow9r5sx0itLUzBIQjvVRUZ7zn8SA,4607
|
|
40
40
|
neva/guidelines/fragments/database-transactions.md,sha256=i0IBbz_DZ19w7Sz_e8t-sAwYQiVU55GJRxM11azjGcM,5941
|
|
41
41
|
neva/guidelines/fragments/events.md,sha256=0eG0zGTfCQpToClydlGhpj5BjXhnKfi2GGve3jz7lv8,6071
|
|
42
|
-
neva/guidelines/fragments/
|
|
42
|
+
neva/guidelines/fragments/facades.md,sha256=4GCoeUGCiuduWY0msgZO0yJ8MOfDwqqQfm_WsWvjHq8,4308
|
|
43
|
+
neva/guidelines/fragments/factories.md,sha256=4ajYgtN1z4Fwb55FnXw7ROGQUrhqE4N9sgGHbg7UUmA,3101
|
|
44
|
+
neva/guidelines/fragments/observability.md,sha256=RjX73YXulr30v-SCw1J3NHN_f6CGLCKUHQPS-rFeOLE,5760
|
|
43
45
|
neva/guidelines/fragments/result-option.md,sha256=fGyuGwJmSIqb7gWjecbQXiKBdstJ7p5_k82j8KLOzcc,2809
|
|
44
|
-
neva/guidelines/fragments/
|
|
45
|
-
neva/guidelines/fragments/
|
|
46
|
+
neva/guidelines/fragments/security.md,sha256=Pz9OHB7o7qVE-Pc3JpSNfMwPqov0ZxLlaIxMmsYfxbQ,5994
|
|
47
|
+
neva/guidelines/fragments/service-providers.md,sha256=NAJfbChBUq9JC5GyWH7eZbUg55lvVi0tOGnxbvkbQtI,8445
|
|
48
|
+
neva/guidelines/fragments/testing.md,sha256=cFOwNHjjyorpooUyEwsSk3O8XynAlkyk3_AzlQUO_K0,6993
|
|
46
49
|
neva/obs/__init__.py,sha256=SShyp-wlNt6USx7Eus8_Csy0miEHXOS2YnVbP2EARjs,490
|
|
47
50
|
neva/obs/config.py,sha256=cFDverbec3QSpLn0txcXnBnMvnUV7dGRTEdVERhl9L4,1026
|
|
48
51
|
neva/obs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -56,13 +59,13 @@ neva/polyfactory/__init__.py,sha256=t7uAXaleAyshSyBdGXNzvgY0nNpb5AqFU4zjE8lyKyk,
|
|
|
56
59
|
neva/polyfactory/factories.py,sha256=8VoZJlY0-_ZOM8rmfUcvEbt-KOZsaDj8f6R5EQF62u8,600
|
|
57
60
|
neva/polyfactory/persistence.py,sha256=1qBg8L_xkiRFq6J2XoZFez43oxNWDaUM8SrlNAeOkQA,605
|
|
58
61
|
neva/polyfactory/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
|
-
neva/security/__init__.py,sha256=
|
|
62
|
+
neva/security/__init__.py,sha256=VycgRsm4qfkfdj6PWPCf95tGzuwi-j6nbGP92qTTLLw,739
|
|
60
63
|
neva/security/provider.py,sha256=6GqSzAsgHtOEnkQFmd0Dusgbv65QhJRxOEBfmObspmc,533
|
|
61
64
|
neva/security/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
62
65
|
neva/security/encryption/__init__.py,sha256=eVWW-qk03vy7Bn-sNQ2WJHWZI6tPJOkknJz8llc1m0M,288
|
|
63
66
|
neva/security/encryption/encrypter.py,sha256=YiJrLmv2C_CkqU3eZDvBHnkwA5zzjb6T3-X2kThywZs,5152
|
|
64
67
|
neva/security/encryption/protocol.py,sha256=pZtkTCz0k6eUX0fivJrvb7yDXy2eTAg--ZbCXhfUXWc,847
|
|
65
|
-
neva/security/hashing/__init__.py,sha256=
|
|
68
|
+
neva/security/hashing/__init__.py,sha256=LoLeOTXifNDFR5_SAsJ_dk135X0TKYFTODCiALCGQ-E,518
|
|
66
69
|
neva/security/hashing/config.py,sha256=ltj7_GW44gSg2N0i8CEYaw6Y6xUfzwsr4zXk5ic_sJI,624
|
|
67
70
|
neva/security/hashing/hash_manager.py,sha256=F55T1xcQpIEV3A52L4KeAbkUeS7ogCgm8lI79RUJP_M,6123
|
|
68
71
|
neva/security/hashing/hashers/__init__.py,sha256=Zn_Q2tj_8lhhwJRZ7RXeJLYujd-3j7Rr8JQO2sSGgJs,44
|
|
@@ -94,13 +97,13 @@ neva/support/facade/event.pyi,sha256=7R6osyTZbHt_zkCfQ-uZnTLfyZTeN1bi3exsgl3-36k
|
|
|
94
97
|
neva/support/facade/hash.py,sha256=tGhsvfGovt-mcRXSjDKf4jt1n7ib0eDPXFvRUfVxLUc,292
|
|
95
98
|
neva/support/facade/hash.pyi,sha256=Uy0CkV67MImPjHc6BmmLQPd7qF1QNDtO1oDFyyRr-1A,2950
|
|
96
99
|
neva/support/facade/log.py,sha256=_uLoHB9tkpHkiqEJXazbaqfiKUOtv3NS71ZCZAprzOI,347
|
|
97
|
-
neva/support/facade/log.pyi,sha256=
|
|
100
|
+
neva/support/facade/log.pyi,sha256=6ZI72G3mgn2cUAnPvKMO9cLYhiTrjzwNGDV2WNxtWoA,2770
|
|
98
101
|
neva/testing/__init__.py,sha256=QW9inJP1lC_HeoJbKjD_kyHCy4cFV6qt9Rp-EjsKmOY,245
|
|
99
102
|
neva/testing/fakes.py,sha256=w2dnFaO6x14l0_si7iBNELMCxaFi8VsVOSQ6PmTlclE,2936
|
|
100
103
|
neva/testing/fixtures.py,sha256=oiPa5ntUkW-SbySDvwEHXtti8NqSwI-R0CT1_ezTx_Y,1445
|
|
101
104
|
neva/testing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
102
|
-
neva/testing/test_case.py,sha256=
|
|
103
|
-
python_neva-5.
|
|
104
|
-
python_neva-5.
|
|
105
|
-
python_neva-5.
|
|
106
|
-
python_neva-5.
|
|
105
|
+
neva/testing/test_case.py,sha256=GdIKrHz9ZuirJybUuaXbW8o-PhOzkvg-pJwe8PAwFD4,9789
|
|
106
|
+
python_neva-5.3.0.dist-info/METADATA,sha256=oGUx6-CaM5vaFk2bx2Jq8anhB-ZETQCktJrh3XwTJl4,7352
|
|
107
|
+
python_neva-5.3.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
108
|
+
python_neva-5.3.0.dist-info/entry_points.txt,sha256=DmWZ-qNgvl4eUAXxwX4iLgb38e6X-zU8JDx_brlmkyk,45
|
|
109
|
+
python_neva-5.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|