python-neva 5.0.0__py3-none-any.whl → 5.1.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.
@@ -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 | None,
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. It must never be lost: fall
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 a
13
- separate plugin, so installing the core commits an application to no observability backend.
14
- There is no `Trace` facade, no tracer, and no OpenTelemetry dependency here — do not add one.
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 go to the default channel. Everything after the message is structured context,
61
- not format arguments — never build the message with an f-string when the value belongs in a
62
- field.
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** on a name no channel is configured for, carrying the reason.
65
- A typo is a configuration mistake, and serving the default instead would hide it behind
66
- working output. Where the failure has to be a value, `Log.channels` gives the resolver and
67
- `use(name)` returns `Result`.
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 — the OpenTelemetry plugin injects the current
84
- span's ids this way.
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
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 Argon2Hasher, BcryptHasher, Hasher, HashManager
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
  ]
@@ -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: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-neva
3
- Version: 5.0.0
3
+ Version: 5.1.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. | planned |
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=PkpC--RyLh0K2AR26a1ZlpHN0p4uFFlcbwSpPdSj8Y8,7274
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,9 +39,12 @@ 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/observability.md,sha256=E3ekECOAJ7K5NlPrgjw2dO10r0r0J85DmW-aeEuD_2M,4858
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/service-providers.md,sha256=IM-UOhtrfy0uFe2z7RsMuDMcWQblpCDSkr1rfoorDZQ,6412
46
+ neva/guidelines/fragments/security.md,sha256=Pz9OHB7o7qVE-Pc3JpSNfMwPqov0ZxLlaIxMmsYfxbQ,5994
47
+ neva/guidelines/fragments/service-providers.md,sha256=Jdzz4J28PkziEdkVoF3sx_qVUJVBrocVQm8E5mL2tyA,8051
45
48
  neva/guidelines/fragments/testing.md,sha256=0HZEdv1RmKbdVnzVVEe5ug4AD73gJoD6r74WC9WT5kI,5659
46
49
  neva/obs/__init__.py,sha256=SShyp-wlNt6USx7Eus8_Csy0miEHXOS2YnVbP2EARjs,490
47
50
  neva/obs/config.py,sha256=cFDverbec3QSpLn0txcXnBnMvnUV7dGRTEdVERhl9L4,1026
@@ -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=Dhh3ZsseP6biWgRQY4hYKkoYFCR7ehhxz4VBqN8e02k,440
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=k3SviEvtJLjBXTkR5I6w9YMPw6Q-LIoE1wGNz3GYvOE,374
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=yC1yhn1EiDGsxryG7K5lQ6esHnaxg3qC_B4U2hei6e8,2548
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
105
  neva/testing/test_case.py,sha256=quVjECLWw5ZOnf7kwvtRsyD-_mkyxv5yRuvNSOAJYUI,8145
103
- python_neva-5.0.0.dist-info/METADATA,sha256=DkBt1UZCLD8-RTloyFVKSpSJhGLmYdvUNk1vm4v2JEw,7523
104
- python_neva-5.0.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
105
- python_neva-5.0.0.dist-info/entry_points.txt,sha256=DmWZ-qNgvl4eUAXxwX4iLgb38e6X-zU8JDx_brlmkyk,45
106
- python_neva-5.0.0.dist-info/RECORD,,
106
+ python_neva-5.1.0.dist-info/METADATA,sha256=M7wOZqbMQilm0N-giNwTqL6JupSTdDzmZIDmkov4IC0,7352
107
+ python_neva-5.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
108
+ python_neva-5.1.0.dist-info/entry_points.txt,sha256=DmWZ-qNgvl4eUAXxwX4iLgb38e6X-zU8JDx_brlmkyk,45
109
+ python_neva-5.1.0.dist-info/RECORD,,