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.
Files changed (105) hide show
  1. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/PKG-INFO +2 -1
  2. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/application.py +21 -4
  3. python_neva-0.6.0.dev1/neva/console/__init__.py +1 -0
  4. python_neva-0.6.0.dev1/neva/console/kernel.py +47 -0
  5. python_neva-0.6.0.dev1/neva/console/runner.py +26 -0
  6. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/config.py +8 -8
  7. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/hash_manager.py +13 -3
  8. python_neva-0.6.0.dev1/neva/security/hashing/hashers/sha256.py +42 -0
  9. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/pyproject.toml +5 -1
  10. python_neva-0.6.0.dev1/tests/test_scope.py +144 -0
  11. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/uv.lock +3 -1
  12. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/.envrc +0 -0
  13. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/.gitignore +0 -0
  14. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/.pre-commit-config.yaml +0 -0
  15. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/.python-version +0 -0
  16. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/README.md +0 -0
  17. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/__init__.py +0 -0
  18. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/__init__.py +0 -0
  19. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/app.py +0 -0
  20. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/config.py +0 -0
  21. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/facade.py +0 -0
  22. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/arch/service_provider.py +0 -0
  23. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/config/__init__.py +0 -0
  24. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/config/base_providers.py +0 -0
  25. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/config/loader.py +0 -0
  26. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/config/provider.py +0 -0
  27. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/config/repository.py +0 -0
  28. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/database/__init__.py +0 -0
  29. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/database/config.py +0 -0
  30. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/database/manager.py +0 -0
  31. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/database/provider.py +0 -0
  32. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/database/repository.py +0 -0
  33. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/__init__.py +0 -0
  34. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/dispatcher.py +0 -0
  35. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/event.py +0 -0
  36. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/event_registry.py +0 -0
  37. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/interface.py +0 -0
  38. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/events/listener.py +0 -0
  39. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/__init__.py +0 -0
  40. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/logging/__init__.py +0 -0
  41. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/logging/manager.py +0 -0
  42. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/logging/provider.py +0 -0
  43. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/middleware/__init__.py +0 -0
  44. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/middleware/correlation.py +0 -0
  45. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/obs/middleware/profiler.py +0 -0
  46. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/py.typed +0 -0
  47. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/__init__.py +0 -0
  48. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/encryption/__init__.py +0 -0
  49. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/encryption/encrypter.py +0 -0
  50. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/encryption/protocol.py +0 -0
  51. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/__init__.py +0 -0
  52. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/hashers/__init__.py +0 -0
  53. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/hashers/argon2.py +0 -0
  54. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/hashers/bcrypt.py +0 -0
  55. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/hashing/hashers/protocol.py +0 -0
  56. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/security/provider.py +0 -0
  57. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/__init__.py +0 -0
  58. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/accessors.py +0 -0
  59. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/__init__.py +0 -0
  60. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/app.py +0 -0
  61. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/app.pyi +0 -0
  62. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/config.py +0 -0
  63. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/config.pyi +0 -0
  64. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/crypt.py +0 -0
  65. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/crypt.pyi +0 -0
  66. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/hash.py +0 -0
  67. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/hash.pyi +0 -0
  68. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/log.py +0 -0
  69. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/facade/log.pyi +0 -0
  70. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/results.py +0 -0
  71. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/strategy.py +0 -0
  72. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/strconv.py +0 -0
  73. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/support/time.py +0 -0
  74. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/testing/__init__.py +0 -0
  75. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/testing/fixtures.py +0 -0
  76. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/testing/http.py +0 -0
  77. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/neva/testing/test_case.py +0 -0
  78. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/ruff.toml +0 -0
  79. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/specifications/future_ideas.md +0 -0
  80. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/specifications/security.md +0 -0
  81. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/__init__.py +0 -0
  82. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/conftest.py +0 -0
  83. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/test_encrypter.py +0 -0
  84. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/test_example_usage.py +0 -0
  85. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/test_fixtures.py +0 -0
  86. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/test_hash_manager.py +0 -0
  87. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/tests/test_test_case.py +0 -0
  88. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/01-overview.md +0 -0
  89. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/02-dependency-injection.md +0 -0
  90. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/03-service-providers.md +0 -0
  91. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/04-facades.md +0 -0
  92. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/05-application-lifecycle.md +0 -0
  93. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/architecture/06-result-option.md +0 -0
  94. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/01-overview.md +0 -0
  95. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/02-configuration-files.md +0 -0
  96. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/03-accessing-configuration.md +0 -0
  97. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/04-config-repository.md +0 -0
  98. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/05-loading-process.md +0 -0
  99. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/configuration/06-configuration-in-providers.md +0 -0
  100. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/testing/01-introduction.md +0 -0
  101. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/testing/02-test-case.md +0 -0
  102. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/testing/03-fixtures.md +0 -0
  103. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/testing/04-http-testing.md +0 -0
  104. {python_neva-0.4.0 → python_neva-0.6.0.dev1}/wiki/testing/05-custom-configuration.md +0 -0
  105. {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.4.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=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: str
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 ("argon2" or "bcrypt").
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.4.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.4.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