ashka-lifecycle 0.1.0__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.
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.3
2
+ Name: ashka-lifecycle
3
+ Version: 0.1.0
4
+ Summary: dishka lifecycle support
5
+ Author: handsome-Druid
6
+ Author-email: handsome-Druid <102826168@qq.com>
7
+ Requires-Dist: dishka>=1.9.0,<=1.10.1
8
+ Requires-Python: >=3.10, <3.15
9
+ Description-Content-Type: text/markdown
10
+
11
+ # ashka-lifecycle
12
+
13
+ `ashka-lifecycle` adds an application bootstrap phase to dishka containers. Dependencies
14
+ registered with `AshkaScope.BOOTSTRAP` are created eagerly when the root
15
+ container is initialized, while retaining the dependency, caching, and
16
+ shutdown behavior of dishka's `Scope.APP`. Both synchronous and asynchronous
17
+ containers support explicit initialization and initialization through their
18
+ context managers.
19
+
20
+ The implementation is designed to keep the integration with dishka minimally
21
+ intrusive. ashka-lifecycle extends the public provider and container construction APIs,
22
+ while leaving dishka's scope and factory implementations unchanged. The
23
+ `AshkaScope.BOOTSTRAP` value is only an ashka-side marker; the provider maps it
24
+ to dishka's native `Scope.APP` instead of introducing a new dishka scope.
25
+
26
+ ashka-lifecycle maintains its own registry of Bootstrap provider sources and the
27
+ corresponding dependency keys for each container. When a container is entered,
28
+ ashka-lifecycle looks up the registered keys and eagerly resolves them using the existing
29
+ container API. Synchronous containers resolve them one by one, while
30
+ asynchronous containers resolve them concurrently. This keeps the changes
31
+ localized to the public extension points and leaves dishka's existing scope,
32
+ caching, shutdown, and factory behavior in control.
33
+
34
+ `ashka-lifecycle` can also be installed and used independently from `ashka` as
35
+ an escape hatch. In this mode, manually import `ashka_lifecycle` before
36
+ importing `dishka` to ensure that its patches are applied.
37
+
38
+ See more details and about how to use:
39
+
40
+ [English](https://github.com/handsome-Druid/ashka/blob/master/docs/en/features/bootstrap-lifecycle.md)
41
+ [中文](https://github.com/handsome-Druid/ashka/blob/master/docs/zh/features/bootstrap-lifecycle.md)
@@ -0,0 +1,31 @@
1
+ # ashka-lifecycle
2
+
3
+ `ashka-lifecycle` adds an application bootstrap phase to dishka containers. Dependencies
4
+ registered with `AshkaScope.BOOTSTRAP` are created eagerly when the root
5
+ container is initialized, while retaining the dependency, caching, and
6
+ shutdown behavior of dishka's `Scope.APP`. Both synchronous and asynchronous
7
+ containers support explicit initialization and initialization through their
8
+ context managers.
9
+
10
+ The implementation is designed to keep the integration with dishka minimally
11
+ intrusive. ashka-lifecycle extends the public provider and container construction APIs,
12
+ while leaving dishka's scope and factory implementations unchanged. The
13
+ `AshkaScope.BOOTSTRAP` value is only an ashka-side marker; the provider maps it
14
+ to dishka's native `Scope.APP` instead of introducing a new dishka scope.
15
+
16
+ ashka-lifecycle maintains its own registry of Bootstrap provider sources and the
17
+ corresponding dependency keys for each container. When a container is entered,
18
+ ashka-lifecycle looks up the registered keys and eagerly resolves them using the existing
19
+ container API. Synchronous containers resolve them one by one, while
20
+ asynchronous containers resolve them concurrently. This keeps the changes
21
+ localized to the public extension points and leaves dishka's existing scope,
22
+ caching, shutdown, and factory behavior in control.
23
+
24
+ `ashka-lifecycle` can also be installed and used independently from `ashka` as
25
+ an escape hatch. In this mode, manually import `ashka_lifecycle` before
26
+ importing `dishka` to ensure that its patches are applied.
27
+
28
+ See more details and about how to use:
29
+
30
+ [English](https://github.com/handsome-Druid/ashka/blob/master/docs/en/features/bootstrap-lifecycle.md)
31
+ [中文](https://github.com/handsome-Druid/ashka/blob/master/docs/zh/features/bootstrap-lifecycle.md)
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "ashka-lifecycle"
3
+ version = "0.1.0"
4
+ description = "dishka lifecycle support"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10,<3.15"
7
+ dependencies = ["dishka>=1.9.0,<=1.10.1"]
8
+
9
+ [[project.authors]]
10
+ name = "handsome-Druid"
11
+ email = "102826168@qq.com"
12
+
13
+ [build-system]
14
+ requires = ["uv_build>=0.12.5,<0.13.0"]
15
+ build-backend = "uv_build"
16
+
17
+ [dependency-groups]
18
+ dev = [
19
+ "ashka>=0.8.0",
20
+ "fastapi>=0.141.1",
21
+ "httpx2>=2.12.0",
22
+ "pyright>=1.1.411",
23
+ "pytest>=9.1.1",
24
+ "pytest-asyncio>=1.4.0",
25
+ "pytest-cov>=7.1.0",
26
+ "pytest-xdist>=3.8.0",
27
+ "ruff>=0.16.4",
28
+ ]
29
+
30
+ [[tool.uv.index]]
31
+ name = "testpypi"
32
+ url = "https://test.pypi.org/simple/"
33
+ publish-url = "https://test.pypi.org/legacy/"
34
+ explicit = true
35
+
36
+ [tool.coverage.run]
37
+ branch = true
38
+ patch = ["subprocess"]
39
+
40
+ [tool.coverage.report]
41
+ fail_under = 100
42
+
43
+ [tool.pytest.ini_options]
44
+ addopts = "-n auto"
45
+
46
+ [tool.ruff]
47
+ exclude = ["typings"]
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "ashka-lifecycle"
3
+ version = "0.1.0"
4
+ description = "dishka lifecycle support"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "handsome-Druid", email = "102826168@qq.com" }
8
+ ]
9
+ requires-python = ">=3.10,<3.15"
10
+ dependencies = [
11
+ "dishka>=1.9.0,<=1.10.1",
12
+ ]
13
+
14
+ [build-system]
15
+ requires = ["uv_build>=0.12.5,<0.13.0"]
16
+ build-backend = "uv_build"
17
+
18
+ [dependency-groups]
19
+ dev = [
20
+ "ashka>=0.8.0",
21
+ "fastapi>=0.141.1",
22
+ "httpx2>=2.12.0",
23
+ "pyright>=1.1.411",
24
+ "pytest>=9.1.1",
25
+ "pytest-asyncio>=1.4.0",
26
+ "pytest-cov>=7.1.0",
27
+ "pytest-xdist>=3.8.0",
28
+ "ruff>=0.16.4",
29
+ ]
30
+
31
+ [[tool.uv.index]]
32
+ name = "testpypi"
33
+ url = "https://test.pypi.org/simple/"
34
+ publish-url = "https://test.pypi.org/legacy/"
35
+ explicit = true
36
+
37
+ [tool.coverage.run]
38
+ branch = true
39
+ patch = ["subprocess"]
40
+
41
+ [tool.coverage.report]
42
+ fail_under = 100
43
+
44
+ [tool.pytest.ini_options]
45
+ addopts = "-n auto"
46
+
47
+ [tool.ruff]
48
+ exclude = ["typings"]
@@ -0,0 +1,11 @@
1
+ __all__ = [
2
+ "AshkaScope",
3
+ "make_async_container",
4
+ "make_container",
5
+ "provide",
6
+ ]
7
+
8
+ from .async_container import make_async_container
9
+ from .container import make_container
10
+ from .entities.scope import AshkaScope
11
+ from .provider import provide # pyright: ignore[reportUnknownVariableType]
@@ -0,0 +1,10 @@
1
+ """
2
+ This type stub file was generated by pyright.
3
+ """
4
+
5
+ from .async_container import make_async_container
6
+ from .container import make_container
7
+ from .entities.scope import AshkaScope
8
+ from .provider import provide
9
+
10
+ __all__ = ["AshkaScope", "make_async_container", "make_container", "provide"]
@@ -0,0 +1,62 @@
1
+ from asyncio import gather
2
+ from typing import Any, cast
3
+
4
+ import dishka
5
+ from dishka import AsyncContainer
6
+ from dishka.provider import BaseProvider
7
+
8
+ from ashka_lifecycle.entities.bootstrap import (
9
+ bootstrap_keys_by_container,
10
+ bootstrap_sources, # pyright: ignore[reportUnknownVariableType]
11
+ )
12
+
13
+ __all__ = ["make_async_container"]
14
+
15
+ _aenter = AsyncContainer.__aenter__
16
+
17
+
18
+ async def __aenter__(self: AsyncContainer):
19
+ aenter = await _aenter(self)
20
+
21
+ if self in bootstrap_keys_by_container:
22
+ await gather(
23
+ *(
24
+ self.get(key.type_hint, key.component)
25
+ for key in bootstrap_keys_by_container[self]
26
+ )
27
+ )
28
+
29
+ return aenter
30
+
31
+
32
+ class AsyncContainerType(AsyncContainer):
33
+ __slots__ = ()
34
+
35
+ __aenter__ = __aenter__
36
+
37
+ async def init(self) -> None:
38
+ await self.__aenter__()
39
+
40
+
41
+ AsyncContainer.__aenter__ = __aenter__
42
+ AsyncContainer.init = AsyncContainerType.init # pyright: ignore[reportAttributeAccessIssue]
43
+
44
+ _make_async_container = dishka.make_async_container
45
+
46
+
47
+ def make_async_container(*providers: BaseProvider, **kwargs: Any) -> AsyncContainerType:
48
+ bootstrap_keys_by_container[
49
+ async_container := _make_async_container(*providers, **kwargs)
50
+ ] = [
51
+ factory.provides.with_component(provider.component)
52
+ for provider in providers
53
+ for factory in provider.factories
54
+ if getattr(factory.source, "__func__", factory.source) in bootstrap_sources
55
+ ]
56
+
57
+ return cast(AsyncContainerType, async_container)
58
+
59
+
60
+ dishka.make_async_container = dishka.async_container.make_async_container = (
61
+ make_async_container
62
+ )
@@ -0,0 +1,25 @@
1
+ """
2
+ This type stub file was generated by pyright.
3
+ """
4
+
5
+ from typing import Any
6
+ from dishka import AsyncContainer
7
+ from dishka.provider import BaseProvider
8
+
9
+ __all__ = ["make_async_container"]
10
+ _aenter = ...
11
+ async def __aenter__(self: AsyncContainer): # -> AsyncContainer:
12
+ ...
13
+
14
+ class AsyncContainerType(AsyncContainer):
15
+ __slots__ = ...
16
+ __aenter__ = ...
17
+ async def init(self) -> None:
18
+ ...
19
+
20
+
21
+
22
+ _make_async_container = ...
23
+ def make_async_container(*providers: BaseProvider, **kwargs: Any) -> AsyncContainerType:
24
+ ...
25
+
@@ -0,0 +1,53 @@
1
+ from typing import Any, cast
2
+
3
+ import dishka
4
+ from dishka import Container
5
+ from dishka.provider import BaseProvider
6
+
7
+ from ashka_lifecycle.entities.bootstrap import (
8
+ bootstrap_keys_by_container,
9
+ bootstrap_sources, # pyright: ignore[reportUnknownVariableType]
10
+ )
11
+
12
+ __all__ = ["make_container"]
13
+
14
+ _enter = Container.__enter__
15
+
16
+
17
+ def __enter__(self: Container):
18
+ enter = _enter(self)
19
+
20
+ if self in bootstrap_keys_by_container:
21
+ for key in bootstrap_keys_by_container[self]:
22
+ self.get(key.type_hint, key.component)
23
+
24
+ return enter
25
+
26
+
27
+ class ContainerType(Container):
28
+ __slots__ = ()
29
+
30
+ __enter__ = __enter__
31
+
32
+ def init(self) -> None:
33
+ self.__enter__()
34
+
35
+
36
+ Container.__enter__ = __enter__
37
+ Container.init = ContainerType.init # pyright: ignore[reportAttributeAccessIssue]
38
+
39
+ _make_container = dishka.make_container
40
+
41
+
42
+ def make_container(*providers: BaseProvider, **kwargs: Any) -> ContainerType:
43
+ bootstrap_keys_by_container[container := _make_container(*providers, **kwargs)] = [
44
+ factory.provides.with_component(provider.component)
45
+ for provider in providers
46
+ for factory in provider.factories
47
+ if getattr(factory.source, "__func__", factory.source) in bootstrap_sources
48
+ ]
49
+
50
+ return cast(ContainerType, container)
51
+
52
+
53
+ dishka.make_container = dishka.container.make_container = make_container
@@ -0,0 +1,25 @@
1
+ """
2
+ This type stub file was generated by pyright.
3
+ """
4
+
5
+ from typing import Any
6
+ from dishka import Container
7
+ from dishka.provider import BaseProvider
8
+
9
+ __all__ = ["make_container"]
10
+ _enter = ...
11
+ def __enter__(self: Container): # -> Container:
12
+ ...
13
+
14
+ class ContainerType(Container):
15
+ __slots__ = ...
16
+ __enter__ = ...
17
+ def init(self) -> None:
18
+ ...
19
+
20
+
21
+
22
+ _make_container = ...
23
+ def make_container(*providers: BaseProvider, **kwargs: Any) -> ContainerType:
24
+ ...
25
+
@@ -0,0 +1,23 @@
1
+ from weakref import WeakKeyDictionary, WeakSet
2
+
3
+ from dishka import AsyncContainer, Container, DependencyKey
4
+ from dishka.provider.make_factory import ProvideSource
5
+
6
+ bootstrap_sources: WeakSet[ProvideSource] = WeakSet() # pyright: ignore[reportUnknownVariableType]
7
+
8
+ if all(
9
+ hasattr(Container_, "__weakref__") for Container_ in (Container, AsyncContainer)
10
+ ):
11
+ bootstrap_keys_by_container: ( # pragma: nocover
12
+ WeakKeyDictionary[
13
+ Container | AsyncContainer,
14
+ list[DependencyKey],
15
+ ]
16
+ | dict[
17
+ Container | AsyncContainer,
18
+ list[DependencyKey],
19
+ ]
20
+ ) = WeakKeyDictionary()
21
+
22
+ else:
23
+ bootstrap_keys_by_container = {} # pragma: nocover
@@ -0,0 +1,13 @@
1
+ """
2
+ This type stub file was generated by pyright.
3
+ """
4
+
5
+ from weakref import WeakKeyDictionary, WeakSet
6
+ from dishka import AsyncContainer, Container, DependencyKey
7
+ from dishka.provider.make_factory import ProvideSource
8
+
9
+ bootstrap_sources: WeakSet[ProvideSource] = ...
10
+ if all(hasattr(Container_, "__weakref__") for Container_ in (Container, AsyncContainer)):
11
+ bootstrap_keys_by_container: (WeakKeyDictionary[Container | AsyncContainer, list[DependencyKey],] | dict[Container | AsyncContainer, list[DependencyKey],]) = ...
12
+ else:
13
+ bootstrap_keys_by_container = ...
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+ __all__ = ["AshkaScope"]
4
+
5
+
6
+ class AshkaScope(Enum):
7
+ BOOTSTRAP = "BOOTSTRAP"
@@ -0,0 +1,11 @@
1
+ """
2
+ This type stub file was generated by pyright.
3
+ """
4
+
5
+ from enum import Enum
6
+
7
+ __all__ = ["AshkaScope"]
8
+ class AshkaScope(Enum):
9
+ BOOTSTRAP = ...
10
+
11
+
@@ -0,0 +1,3 @@
1
+ from .make_factory import provide # pyright: ignore[reportUnknownVariableType]
2
+
3
+ __all__ = ["provide"]
@@ -0,0 +1,7 @@
1
+ """
2
+ This type stub file was generated by pyright.
3
+ """
4
+
5
+ from .make_factory import provide
6
+
7
+ __all__ = ["provide"]
@@ -0,0 +1,54 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, overload
3
+
4
+ from dishka import BaseScope, Scope
5
+ from dishka import provide as _provide # pyright: ignore[reportUnknownVariableType]
6
+ from dishka.dependency_source.composite import CompositeDependencySource
7
+ from dishka.provider.make_factory import ProvideSource
8
+
9
+ from ashka_lifecycle.entities.bootstrap import (
10
+ bootstrap_sources, # pyright: ignore[reportUnknownVariableType]
11
+ )
12
+ from ashka_lifecycle.entities.scope import AshkaScope
13
+
14
+ __all__ = ["provide"]
15
+
16
+
17
+ @overload
18
+ def provide(
19
+ *, scope: BaseScope | AshkaScope | None = None, **kwargs: Any
20
+ ) -> Callable[[Callable[..., Any]], CompositeDependencySource]: ...
21
+
22
+
23
+ @overload
24
+ def provide(
25
+ source: ProvideSource, # pyright: ignore[reportUnknownParameterType]
26
+ *,
27
+ scope: BaseScope | AshkaScope | None = None,
28
+ **kwargs: Any,
29
+ ) -> CompositeDependencySource: ...
30
+
31
+
32
+ def provide(
33
+ source: ProvideSource | None = None, # pyright: ignore[reportUnknownParameterType]
34
+ *,
35
+ scope: BaseScope | AshkaScope | None = None,
36
+ **kwargs: Any,
37
+ ) -> (
38
+ CompositeDependencySource
39
+ | Callable[
40
+ [Callable[..., Any]],
41
+ CompositeDependencySource,
42
+ ]
43
+ ):
44
+ if scope is not AshkaScope.BOOTSTRAP:
45
+ return _provide(source, scope=scope, **kwargs)
46
+
47
+ def scoped(source: ProvideSource): # pyright: ignore[reportUnknownParameterType]
48
+ bootstrap_sources.add(getattr(source, "__func__", source)) # pyright: ignore[reportUnknownArgumentType, reportUnknownMemberType]
49
+ return _provide(source, scope=Scope.APP, **kwargs)
50
+
51
+ if source is not None:
52
+ return scoped(source)
53
+
54
+ return scoped # pyright: ignore[reportUnknownVariableType]
@@ -0,0 +1,23 @@
1
+ """
2
+ This type stub file was generated by pyright.
3
+ """
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any, overload
7
+ from dishka import BaseScope
8
+ from dishka.dependency_source.composite import CompositeDependencySource
9
+ from dishka.provider.make_factory import ProvideSource
10
+ from ashka_lifecycle.entities.scope import AshkaScope
11
+
12
+ __all__ = ["provide"]
13
+ @overload
14
+ def provide(*, scope: BaseScope | AshkaScope | None = ..., **kwargs: Any) -> Callable[[Callable[..., Any]], CompositeDependencySource]:
15
+ ...
16
+
17
+ @overload
18
+ def provide(source: ProvideSource, *, scope: BaseScope | AshkaScope | None = ..., **kwargs: Any) -> CompositeDependencySource:
19
+ ...
20
+
21
+ def provide(source: ProvideSource | None = ..., *, scope: BaseScope | AshkaScope | None = ..., **kwargs: Any) -> (CompositeDependencySource | Callable[[Callable[..., Any]], CompositeDependencySource,]):
22
+ ...
23
+
File without changes