python-neva 0.4.0__tar.gz → 0.6.0.dev1__tar.gz
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.
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/PKG-INFO +2 -1
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/application.py +21 -4
- python_neva-0.6.0.dev1/neva/console/__init__.py +1 -0
- python_neva-0.6.0.dev1/neva/console/kernel.py +47 -0
- python_neva-0.6.0.dev1/neva/console/runner.py +26 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/config.py +8 -8
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/hash_manager.py +13 -3
- python_neva-0.6.0.dev1/neva/security/hashing/hashers/sha256.py +42 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/pyproject.toml +5 -1
- python_neva-0.6.0.dev1/tests/test_scope.py +144 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/uv.lock +3 -1
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/.envrc +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/.gitignore +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/.pre-commit-config.yaml +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/.python-version +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/README.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/app.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/config.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/facade.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/service_provider.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/config/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/config/base_providers.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/config/loader.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/config/provider.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/config/repository.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/database/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/database/config.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/database/manager.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/database/provider.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/database/repository.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/dispatcher.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/event.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/event_registry.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/interface.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/listener.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/logging/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/logging/manager.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/logging/provider.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/middleware/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/middleware/correlation.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/middleware/profiler.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/py.typed +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/encryption/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/encryption/encrypter.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/encryption/protocol.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/hashers/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/hashers/argon2.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/hashers/bcrypt.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/hashers/protocol.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/provider.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/accessors.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/app.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/app.pyi +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/config.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/config.pyi +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/crypt.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/crypt.pyi +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/hash.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/hash.pyi +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/log.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/log.pyi +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/results.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/strategy.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/strconv.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/time.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/testing/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/testing/fixtures.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/testing/http.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/testing/test_case.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/ruff.toml +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/specifications/future_ideas.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/specifications/security.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/__init__.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/conftest.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/test_encrypter.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/test_example_usage.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/test_fixtures.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/test_hash_manager.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/test_test_case.py +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/01-overview.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/02-dependency-injection.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/03-service-providers.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/04-facades.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/05-application-lifecycle.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/06-result-option.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/01-overview.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/02-configuration-files.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/03-accessing-configuration.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/04-config-repository.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/05-loading-process.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/06-configuration-in-providers.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/testing/01-introduction.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/testing/02-test-case.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/testing/03-fixtures.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/testing/04-http-testing.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/testing/05-custom-configuration.md +0 -0
- {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/testing/06-test-isolation.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-neva
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0.dev1
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: cryptography>=46.0.3
|
|
@@ -11,6 +11,7 @@ Requires-Dist: pwdlib[argon2,bcrypt]>=0.3.0
|
|
|
11
11
|
Requires-Dist: pyinstrument>=5.1.1
|
|
12
12
|
Requires-Dist: structlog>=25.5.0
|
|
13
13
|
Requires-Dist: tortoise-orm[accel]>=0.25.3
|
|
14
|
+
Requires-Dist: typer>=0.21.1
|
|
14
15
|
Provides-Extra: testing
|
|
15
16
|
Requires-Dist: pytest-asyncio>=0.25.3; extra == 'testing'
|
|
16
17
|
Requires-Dist: pytest>=9.0.2; extra == 'testing'
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Base application for DI and facade injection."""
|
|
2
2
|
|
|
3
|
-
from collections.abc import AsyncIterator
|
|
4
|
-
from contextlib import AsyncExitStack, asynccontextmanager
|
|
3
|
+
from collections.abc import AsyncIterator, Iterator
|
|
4
|
+
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
|
|
5
|
+
import os
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import Any, Callable
|
|
7
|
+
from typing import Any, Callable, Self
|
|
7
8
|
|
|
8
9
|
import dishka
|
|
9
10
|
from dishka.integrations.fastapi import FastapiProvider
|
|
@@ -33,9 +34,10 @@ class Application:
|
|
|
33
34
|
self.providers: dict[type, ServiceProvider] = {}
|
|
34
35
|
self.di_provider: dishka.Provider = dishka.Provider(scope=dishka.Scope.APP)
|
|
35
36
|
|
|
37
|
+
configuration_path = config_path or os.getenv("NEVA_CONFIG_PATH", default=None)
|
|
36
38
|
config_provider = ConfigServiceProvider(
|
|
37
39
|
app=self,
|
|
38
|
-
config_path=
|
|
40
|
+
config_path=configuration_path,
|
|
39
41
|
).register()
|
|
40
42
|
if config_provider.is_err:
|
|
41
43
|
raise RuntimeError(
|
|
@@ -114,6 +116,21 @@ class Application:
|
|
|
114
116
|
except Exception as e:
|
|
115
117
|
return Err(f"Failed to resolve service '{interface.__name__}': {e}")
|
|
116
118
|
|
|
119
|
+
@contextmanager
|
|
120
|
+
def scope(self, scope: dishka.BaseScope | None = None) -> Iterator[Self]:
|
|
121
|
+
"""Enter a new scope.
|
|
122
|
+
|
|
123
|
+
Yields:
|
|
124
|
+
The application instance with the new scope.
|
|
125
|
+
"""
|
|
126
|
+
parent = self.container
|
|
127
|
+
with self.container(scope=scope) as container:
|
|
128
|
+
self.container = container
|
|
129
|
+
try:
|
|
130
|
+
yield self
|
|
131
|
+
finally:
|
|
132
|
+
self.container = parent
|
|
133
|
+
|
|
117
134
|
@asynccontextmanager
|
|
118
135
|
async def lifespan(self) -> AsyncIterator[None]:
|
|
119
136
|
"""Wire the facades and providers."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI utilities."""
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Defines the CLI kernel."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, Callable, override
|
|
6
|
+
from typer import Typer
|
|
7
|
+
from typer.core import click
|
|
8
|
+
|
|
9
|
+
from typer.models import CommandFunctionType
|
|
10
|
+
|
|
11
|
+
from neva.arch import Application
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Kernel(Typer):
|
|
15
|
+
"""CLI kernel."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
callback: Callable[..., Any] | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Initialize the CLI kernel."""
|
|
22
|
+
super().__init__(callback=callback)
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
def command(
|
|
26
|
+
self,
|
|
27
|
+
*args: Any,
|
|
28
|
+
**kwargs: Any,
|
|
29
|
+
) -> Callable[[CommandFunctionType], CommandFunctionType]:
|
|
30
|
+
base_decorator = super().command(*args, **kwargs)
|
|
31
|
+
|
|
32
|
+
def decorator(f: CommandFunctionType) -> CommandFunctionType:
|
|
33
|
+
@wraps(f)
|
|
34
|
+
def wrapper(*f_args: Any, **f_kwargs: Any) -> Any: # noqa: ANN401
|
|
35
|
+
ctx = click.get_current_context()
|
|
36
|
+
config_path = ctx.obj.get("config_path", None)
|
|
37
|
+
app = Application(config_path)
|
|
38
|
+
|
|
39
|
+
async def runner() -> Any: # noqa: ANN401
|
|
40
|
+
async with app.lifespan():
|
|
41
|
+
return f(*f_args, **f_kwargs)
|
|
42
|
+
|
|
43
|
+
return asyncio.run(runner())
|
|
44
|
+
|
|
45
|
+
return base_decorator(wrapper)
|
|
46
|
+
|
|
47
|
+
return decorator
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""CLI runner."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from neva.console.kernel import Kernel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def set_config(
|
|
11
|
+
ctx: typer.Context,
|
|
12
|
+
config_path: Annotated[
|
|
13
|
+
str,
|
|
14
|
+
typer.Option(help="Sets the config path"),
|
|
15
|
+
] = "",
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Set the configuration path."""
|
|
18
|
+
ctx.obj = {"config_path": config_path}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
app = Kernel(set_config)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
"""Run the CLI."""
|
|
26
|
+
app()
|
|
@@ -6,23 +6,23 @@ from typing import Literal, NotRequired, TypedDict
|
|
|
6
6
|
class BcryptConfig(TypedDict):
|
|
7
7
|
"""Bcrypt hasher config."""
|
|
8
8
|
|
|
9
|
-
rounds: int
|
|
10
|
-
prefix:
|
|
9
|
+
rounds: NotRequired[int]
|
|
10
|
+
prefix: NotRequired[Literal["2a", "2b"]]
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class Argon2Config(TypedDict):
|
|
14
14
|
"""Argon2 hasher config."""
|
|
15
15
|
|
|
16
|
-
time_cost: int
|
|
17
|
-
memory_cost: int
|
|
18
|
-
parallelism: int
|
|
19
|
-
hash_len: int
|
|
20
|
-
salt_len: int
|
|
16
|
+
time_cost: NotRequired[int]
|
|
17
|
+
memory_cost: NotRequired[int]
|
|
18
|
+
parallelism: NotRequired[int]
|
|
19
|
+
hash_len: NotRequired[int]
|
|
20
|
+
salt_len: NotRequired[int]
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class HashingConfig(TypedDict):
|
|
24
24
|
"""Hasher config."""
|
|
25
25
|
|
|
26
|
-
driver: Literal["argon2", "bcrypt"]
|
|
26
|
+
driver: Literal["argon2", "bcrypt", "sha256"]
|
|
27
27
|
argon: NotRequired[Argon2Config]
|
|
28
28
|
bcrypt: NotRequired[BcryptConfig]
|
|
@@ -8,6 +8,7 @@ from neva.config import ConfigRepository
|
|
|
8
8
|
from neva.security.hashing.hashers.argon2 import Argon2Hasher
|
|
9
9
|
from neva.security.hashing.hashers.bcrypt import BcryptHasher
|
|
10
10
|
from neva.security.hashing.hashers.protocol import Hasher
|
|
11
|
+
from neva.security.hashing.hashers.sha256 import Sha256Hasher
|
|
11
12
|
from neva.support.strategy import StrategyResolver
|
|
12
13
|
|
|
13
14
|
|
|
@@ -22,15 +23,16 @@ class HashManager(StrategyResolver[Hasher]):
|
|
|
22
23
|
"""
|
|
23
24
|
super().__init__(app)
|
|
24
25
|
|
|
25
|
-
self.register("argon2", self._create_argon2_hasher)
|
|
26
|
-
self.register("bcrypt", self._create_bcrypt_hasher)
|
|
26
|
+
_ = self.register("argon2", self._create_argon2_hasher)
|
|
27
|
+
_ = self.register("bcrypt", self._create_bcrypt_hasher)
|
|
28
|
+
_ = self.register("sha256", self._create_sha256_hasher)
|
|
27
29
|
|
|
28
30
|
@override
|
|
29
31
|
def default(self) -> Option[str]:
|
|
30
32
|
"""Get the default hasher from configuration.
|
|
31
33
|
|
|
32
34
|
Returns:
|
|
33
|
-
Option containing the default hasher name
|
|
35
|
+
Option containing the default hasher name.
|
|
34
36
|
"""
|
|
35
37
|
config_result = self.app.make(ConfigRepository)
|
|
36
38
|
if config_result.is_err:
|
|
@@ -144,3 +146,11 @@ class HashManager(StrategyResolver[Hasher]):
|
|
|
144
146
|
prefix=bcrypt_config.get("prefix", "2b"),
|
|
145
147
|
)
|
|
146
148
|
return BcryptHasher()
|
|
149
|
+
|
|
150
|
+
def _create_sha256_hasher(self, _: StrategyResolver[Hasher]) -> Sha256Hasher:
|
|
151
|
+
"""Create an instance of the SHA256 hash strategy.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Configured Sha256Hasher instance.
|
|
155
|
+
"""
|
|
156
|
+
return Sha256Hasher()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""SHA256 hasher."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from typing import override
|
|
5
|
+
from neva.security.hashing.hashers.protocol import Hasher
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ensure_bytes(value: str | bytes) -> bytes:
|
|
9
|
+
"""Ensure the value is in bytes.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
the value in bytes
|
|
13
|
+
"""
|
|
14
|
+
return value.encode() if isinstance(value, str) else value
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Sha256Hasher(Hasher):
|
|
18
|
+
"""SHA256 hasher."""
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
def make(
|
|
22
|
+
self,
|
|
23
|
+
plaintext: str | bytes,
|
|
24
|
+
*,
|
|
25
|
+
salt: bytes | None = None,
|
|
26
|
+
) -> str:
|
|
27
|
+
return hashlib.sha256(ensure_bytes(plaintext)).hexdigest()
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
def check(
|
|
31
|
+
self,
|
|
32
|
+
plaintext: str | bytes,
|
|
33
|
+
hashed: str | bytes,
|
|
34
|
+
) -> bool:
|
|
35
|
+
return hashlib.sha256(ensure_bytes(plaintext)).digest() == ensure_bytes(hashed)
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
def needs_rehash(
|
|
39
|
+
self,
|
|
40
|
+
hashed: str | bytes,
|
|
41
|
+
) -> bool:
|
|
42
|
+
return False
|
|
@@ -7,7 +7,7 @@ packages = ["neva"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "python-neva"
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.6.0.dev1"
|
|
11
11
|
description = "Add your description here"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.12"
|
|
@@ -20,11 +20,15 @@ dependencies = [
|
|
|
20
20
|
"pyinstrument>=5.1.1",
|
|
21
21
|
"structlog>=25.5.0",
|
|
22
22
|
"tortoise-orm[accel]>=0.25.3",
|
|
23
|
+
"typer>=0.21.1",
|
|
23
24
|
]
|
|
24
25
|
|
|
25
26
|
[project.optional-dependencies]
|
|
26
27
|
testing = ["pytest>=9.0.2", "pytest-asyncio>=0.25.3"]
|
|
27
28
|
|
|
29
|
+
[project.scripts]
|
|
30
|
+
neva = "neva.console.runner:main"
|
|
31
|
+
|
|
28
32
|
[dependency-groups]
|
|
29
33
|
dev = [
|
|
30
34
|
"bandit>=1.9.2",
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Self, override
|
|
3
|
+
|
|
4
|
+
import dishka
|
|
5
|
+
|
|
6
|
+
from neva import Ok, Result, arch
|
|
7
|
+
from neva.config import ConfigRepository
|
|
8
|
+
from neva.testing import TestCase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RequestService:
|
|
12
|
+
"""A service scoped to REQUEST."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self.value = id(self)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RequestScopedProvider(arch.ServiceProvider):
|
|
19
|
+
"""Registers a REQUEST-scoped service."""
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
def register(self) -> Result[Self, str]:
|
|
23
|
+
self.app.bind(
|
|
24
|
+
RequestService,
|
|
25
|
+
interface=RequestService,
|
|
26
|
+
scope=dishka.Scope.REQUEST,
|
|
27
|
+
)
|
|
28
|
+
return Ok(self)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _make_config_dir(tmp_path: Path) -> Path:
|
|
32
|
+
config_dir = tmp_path / "config"
|
|
33
|
+
config_dir.mkdir()
|
|
34
|
+
|
|
35
|
+
_ = (config_dir / "app.py").write_text(
|
|
36
|
+
'config = {"name": "TestApp", "debug": True}'
|
|
37
|
+
)
|
|
38
|
+
_ = (config_dir / "providers.py").write_text(
|
|
39
|
+
"""
|
|
40
|
+
from tests.test_scope import RequestScopedProvider
|
|
41
|
+
|
|
42
|
+
config = {"providers": [RequestScopedProvider]}
|
|
43
|
+
"""
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return config_dir
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestScopeEntersChildScope(TestCase):
|
|
50
|
+
@override
|
|
51
|
+
def create_config(self, tmp_path: Path) -> Path:
|
|
52
|
+
return _make_config_dir(tmp_path)
|
|
53
|
+
|
|
54
|
+
async def test_request_scoped_service_resolved_in_scope(self) -> None:
|
|
55
|
+
with self.app.scope(dishka.Scope.REQUEST) as scoped:
|
|
56
|
+
result = scoped.make(RequestService)
|
|
57
|
+
|
|
58
|
+
assert result.is_ok
|
|
59
|
+
|
|
60
|
+
async def test_request_scoped_service_not_resolved_at_app_scope(self) -> None:
|
|
61
|
+
result = self.app.make(RequestService)
|
|
62
|
+
|
|
63
|
+
assert result.is_err
|
|
64
|
+
|
|
65
|
+
async def test_app_scoped_service_available_in_child_scope(self) -> None:
|
|
66
|
+
with self.app.scope(dishka.Scope.REQUEST) as scoped:
|
|
67
|
+
result = scoped.make(ConfigRepository)
|
|
68
|
+
|
|
69
|
+
assert result.is_ok
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestScopeRestoresContainer(TestCase):
|
|
73
|
+
@override
|
|
74
|
+
def create_config(self, tmp_path: Path) -> Path:
|
|
75
|
+
return _make_config_dir(tmp_path)
|
|
76
|
+
|
|
77
|
+
async def test_container_restored_after_scope_exit(self) -> None:
|
|
78
|
+
original_container = self.app.container
|
|
79
|
+
|
|
80
|
+
with self.app.scope(dishka.Scope.REQUEST):
|
|
81
|
+
assert self.app.container is not original_container
|
|
82
|
+
|
|
83
|
+
assert self.app.container is original_container
|
|
84
|
+
|
|
85
|
+
async def test_container_restored_after_exception(self) -> None:
|
|
86
|
+
original_container = self.app.container
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
with self.app.scope(dishka.Scope.REQUEST):
|
|
90
|
+
msg = "intentional"
|
|
91
|
+
raise RuntimeError(msg)
|
|
92
|
+
except RuntimeError:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
assert self.app.container is original_container
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestScopeInstanceLifetime(TestCase):
|
|
99
|
+
@override
|
|
100
|
+
def create_config(self, tmp_path: Path) -> Path:
|
|
101
|
+
return _make_config_dir(tmp_path)
|
|
102
|
+
|
|
103
|
+
async def test_same_instance_within_scope(self) -> None:
|
|
104
|
+
with self.app.scope(dishka.Scope.REQUEST) as scoped:
|
|
105
|
+
first = scoped.make(RequestService).unwrap()
|
|
106
|
+
second = scoped.make(RequestService).unwrap()
|
|
107
|
+
|
|
108
|
+
assert first is second
|
|
109
|
+
|
|
110
|
+
async def test_different_instances_across_scopes(self) -> None:
|
|
111
|
+
with self.app.scope(dishka.Scope.REQUEST) as scoped:
|
|
112
|
+
first = scoped.make(RequestService).unwrap()
|
|
113
|
+
|
|
114
|
+
with self.app.scope(dishka.Scope.REQUEST) as scoped:
|
|
115
|
+
second = scoped.make(RequestService).unwrap()
|
|
116
|
+
|
|
117
|
+
assert first is not second
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestScopeDefaultTarget(TestCase):
|
|
121
|
+
@override
|
|
122
|
+
def create_config(self, tmp_path: Path) -> Path:
|
|
123
|
+
return _make_config_dir(tmp_path)
|
|
124
|
+
|
|
125
|
+
async def test_scope_without_argument_enters_next_non_skipped(self) -> None:
|
|
126
|
+
with self.app.scope() as scoped:
|
|
127
|
+
result = scoped.make(RequestService)
|
|
128
|
+
|
|
129
|
+
assert result.is_ok
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestNestedScopes(TestCase):
|
|
133
|
+
@override
|
|
134
|
+
def create_config(self, tmp_path: Path) -> Path:
|
|
135
|
+
return _make_config_dir(tmp_path)
|
|
136
|
+
|
|
137
|
+
async def test_nested_scope_restores_to_parent_scope(self) -> None:
|
|
138
|
+
with self.app.scope(dishka.Scope.REQUEST) as request_scoped:
|
|
139
|
+
request_container = request_scoped.container
|
|
140
|
+
|
|
141
|
+
with request_scoped.scope(dishka.Scope.ACTION) as action_scoped:
|
|
142
|
+
assert action_scoped.container is not request_container
|
|
143
|
+
|
|
144
|
+
assert request_scoped.container is request_container
|
|
@@ -1267,7 +1267,7 @@ wheels = [
|
|
|
1267
1267
|
|
|
1268
1268
|
[[package]]
|
|
1269
1269
|
name = "python-neva"
|
|
1270
|
-
version = "0.
|
|
1270
|
+
version = "0.6.0.dev1"
|
|
1271
1271
|
source = { editable = "." }
|
|
1272
1272
|
dependencies = [
|
|
1273
1273
|
{ name = "cryptography" },
|
|
@@ -1278,6 +1278,7 @@ dependencies = [
|
|
|
1278
1278
|
{ name = "pyinstrument" },
|
|
1279
1279
|
{ name = "structlog" },
|
|
1280
1280
|
{ name = "tortoise-orm", extra = ["accel"] },
|
|
1281
|
+
{ name = "typer" },
|
|
1281
1282
|
]
|
|
1282
1283
|
|
|
1283
1284
|
[package.optional-dependencies]
|
|
@@ -1312,6 +1313,7 @@ requires-dist = [
|
|
|
1312
1313
|
{ name = "pytest-asyncio", marker = "extra == 'testing'", specifier = ">=0.25.3" },
|
|
1313
1314
|
{ name = "structlog", specifier = ">=25.5.0" },
|
|
1314
1315
|
{ name = "tortoise-orm", extras = ["accel"], specifier = ">=0.25.3" },
|
|
1316
|
+
{ name = "typer", specifier = ">=0.21.1" },
|
|
1315
1317
|
]
|
|
1316
1318
|
provides-extras = ["testing"]
|
|
1317
1319
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/03-accessing-configuration.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/06-configuration-in-providers.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|