fastapi-injected 0.1.2__tar.gz → 0.1.3__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 (29) hide show
  1. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/PKG-INFO +1 -1
  2. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/fastapi_injected/__init__.py +4 -2
  3. fastapi_injected-0.1.3/fastapi_injected/_fastapi_lifecycle.py +62 -0
  4. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/fastapi_injected/deps.py +4 -42
  5. fastapi_injected-0.1.2/fastapi_injected/solve.py → fastapi_injected-0.1.3/fastapi_injected/resolve.py +2 -2
  6. fastapi_injected-0.1.3/fastapi_injected/scope.py +108 -0
  7. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/pyproject.toml +2 -1
  8. fastapi_injected-0.1.3/tests/test_fastapi.py +35 -0
  9. fastapi_injected-0.1.2/tests/test_solve.py → fastapi_injected-0.1.3/tests/test_resolve.py +6 -6
  10. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/tests/test_typing.py +3 -3
  11. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/uv.lock +3 -1
  12. fastapi_injected-0.1.2/fastapi_injected/scope.py +0 -75
  13. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/.github/dependabot.yml +0 -0
  14. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/.github/workflows/automerge.yml +0 -0
  15. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/.github/workflows/lint.yml +0 -0
  16. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/.github/workflows/publish.yml +0 -0
  17. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/.github/workflows/test.yml +0 -0
  18. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/.gitignore +0 -0
  19. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/.pre-commit-config.yaml +0 -0
  20. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/LICENSE +0 -0
  21. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/README.md +0 -0
  22. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/fastapi_injected/inject.py +0 -0
  23. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/fastapi_injected/sign.py +0 -0
  24. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/fastapi_injected/types.py +0 -0
  25. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/tests/__init__.py +0 -0
  26. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/tests/deps.py +0 -0
  27. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/tests/ext/__init__.py +0 -0
  28. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/tests/ext/test_pydantic_ai.py +0 -0
  29. {fastapi_injected-0.1.2 → fastapi_injected-0.1.3}/tests/test_inject.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-injected
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Yet another library to reuse fastapi dependency injection
5
5
  Project-URL: Repository, https://github.com/uriyyo/fastapi-injected
6
6
  Author-email: Yurii Karabas <1998uriyyo@gmail.com>
@@ -1,13 +1,15 @@
1
+ from ._fastapi_lifecycle import init_inject_scope
1
2
  from .inject import inject
3
+ from .resolve import resolve
2
4
  from .scope import push_inject_scope
3
- from .solve import solve
4
5
  from .types import Dep, DepFactory, Injected
5
6
 
6
7
  __all__ = [
7
8
  "Dep",
8
9
  "DepFactory",
9
10
  "Injected",
11
+ "init_inject_scope",
10
12
  "inject",
11
13
  "push_inject_scope",
12
- "solve",
14
+ "resolve",
13
15
  ]
@@ -0,0 +1,62 @@
1
+ from collections.abc import AsyncIterable
2
+ from functools import wraps
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from fastapi import Request, WebSocket, routing
6
+ from fastapi.dependencies import utils
7
+
8
+ from .scope import push_inject_scope
9
+ from .types import DependencyCache
10
+
11
+ _DEPENDENCY_CACHE_KEY = "__fastapi_injected_dependency_cache__"
12
+
13
+
14
+ class MissingDependencyCacheError(Exception):
15
+ pass
16
+
17
+
18
+ if not TYPE_CHECKING:
19
+ _base_solve_dependencies = utils.solve_dependencies
20
+
21
+ @wraps(_base_solve_dependencies)
22
+ async def _solve_dependencies(
23
+ *,
24
+ request: Request | WebSocket,
25
+ dependency_cache: DependencyCache | None = None,
26
+ **kwargs: Any,
27
+ ) -> utils.SolvedDependency:
28
+ if (current_cache := request.scope.get(_DEPENDENCY_CACHE_KEY)) is not None:
29
+ dependency_cache = current_cache
30
+ elif dependency_cache is None:
31
+ dependency_cache = {}
32
+
33
+ request.scope[_DEPENDENCY_CACHE_KEY] = dependency_cache
34
+
35
+ return await _base_solve_dependencies(
36
+ request=request,
37
+ dependency_cache=dependency_cache,
38
+ **kwargs,
39
+ )
40
+
41
+ utils.solve_dependencies = _solve_dependencies
42
+ routing.solve_dependencies = _solve_dependencies
43
+
44
+
45
+ def _get_dependency_cache(request: Request) -> DependencyCache:
46
+ try:
47
+ return request.scope[_DEPENDENCY_CACHE_KEY]
48
+ except KeyError:
49
+ raise MissingDependencyCacheError("Dependency cache not found") from None
50
+
51
+
52
+ async def init_inject_scope(request: Request) -> AsyncIterable[None]:
53
+ async with push_inject_scope(
54
+ dependency_cache=_get_dependency_cache(request),
55
+ request=request,
56
+ ):
57
+ yield
58
+
59
+
60
+ __all__ = [
61
+ "init_inject_scope",
62
+ ]
@@ -1,5 +1,6 @@
1
1
  import inspect
2
2
  from collections.abc import Callable
3
+ from contextlib import AsyncExitStack
3
4
  from contextvars import ContextVar
4
5
  from copy import copy
5
6
  from functools import lru_cache, wraps
@@ -9,47 +10,12 @@ from fastapi import Depends
9
10
  from fastapi.dependencies.models import Dependant
10
11
  from fastapi.dependencies.utils import get_dependant, get_typed_signature
11
12
  from fastapi.dependencies.utils import solve_dependencies as _solve_dependencies
12
- from starlette.requests import Request
13
- from starlette.types import Message, Scope
14
13
 
15
14
  from .scope import InjectScope
16
15
  from .sign import prepare_sign, update_func_sign
17
16
  from .types import Coro, HasSignature
18
17
 
19
18
 
20
- def _dummy_scope() -> Scope:
21
- return {
22
- "type": "http",
23
- "http_version": "1.1",
24
- "query_string": b"",
25
- "headers": [],
26
- }
27
-
28
-
29
- def _dummy_request(
30
- *,
31
- extra_scope: Scope | None = None,
32
- ) -> Request:
33
- async def _dummy_receive() -> Message:
34
- return {
35
- "type": "http.request",
36
- "body": b"",
37
- }
38
-
39
- async def _dummy_send(_: Message, /) -> None:
40
- pass
41
-
42
- scope = _dummy_scope()
43
- if extra_scope:
44
- scope.update(extra_scope)
45
-
46
- return Request(
47
- scope,
48
- receive=_dummy_receive,
49
- send=_dummy_send,
50
- )
51
-
52
-
53
19
  @lru_cache(maxsize=1024)
54
20
  def create_dependant[**P, R](func: Callable[P, Coro[R]], /) -> Dependant:
55
21
  @wraps(func)
@@ -114,16 +80,12 @@ async def solve_dependencies(
114
80
  single: bool = False,
115
81
  ) -> dict[str, Any]:
116
82
  solved = await _solve_dependencies(
117
- request=_dummy_request(
118
- extra_scope={
119
- "fastapi_inner_astack": scope.request_astack,
120
- "fastapi_function_astack": scope.func_astack,
121
- },
122
- ),
83
+ request=scope.request,
123
84
  dependant=dependant,
124
- async_exit_stack=scope.request_astack,
125
85
  dependency_cache=copy(scope.dependency_cache),
126
86
  dependency_overrides_provider=_dependency_override_provider.get(),
87
+ # this parameter is deprecated and not used
88
+ async_exit_stack=cast(AsyncExitStack, None),
127
89
  embed_body_fields=False,
128
90
  )
129
91
 
@@ -4,7 +4,7 @@ from .deps import create_single_dependant, solve_dependencies
4
4
  from .scope import inside_inject_scope
5
5
 
6
6
 
7
- async def solve[R](
7
+ async def resolve[R](
8
8
  tp: TypeForm[R],
9
9
  *,
10
10
  new_scope: bool = False,
@@ -22,5 +22,5 @@ async def solve[R](
22
22
 
23
23
 
24
24
  __all__ = [
25
- "solve",
25
+ "resolve",
26
26
  ]
@@ -0,0 +1,108 @@
1
+ from collections.abc import AsyncIterator
2
+ from contextlib import (
3
+ AsyncExitStack,
4
+ asynccontextmanager,
5
+ )
6
+ from contextvars import ContextVar
7
+ from dataclasses import dataclass
8
+
9
+ from fastapi import Request
10
+ from starlette.types import Message, Scope
11
+
12
+ from .types import DependencyCache
13
+
14
+
15
+ def _dummy_scope() -> Scope:
16
+ return {
17
+ "type": "http",
18
+ "http_version": "1.1",
19
+ "query_string": b"",
20
+ "headers": [],
21
+ }
22
+
23
+
24
+ def _dummy_request(
25
+ *,
26
+ extra_scope: Scope | None = None,
27
+ ) -> Request:
28
+ async def _dummy_receive() -> Message:
29
+ return {
30
+ "type": "http.request",
31
+ "body": b"",
32
+ }
33
+
34
+ async def _dummy_send(_: Message, /) -> None:
35
+ pass
36
+
37
+ scope = _dummy_scope()
38
+ if extra_scope:
39
+ scope.update(extra_scope)
40
+
41
+ return Request(
42
+ scope,
43
+ receive=_dummy_receive,
44
+ send=_dummy_send,
45
+ )
46
+
47
+
48
+ @dataclass
49
+ class InjectScope:
50
+ dependency_cache: DependencyCache
51
+ request: Request
52
+
53
+
54
+ _inject_scope: ContextVar[InjectScope | None] = ContextVar(
55
+ "_inject_scope",
56
+ default=None,
57
+ )
58
+
59
+
60
+ @asynccontextmanager
61
+ async def push_inject_scope(
62
+ *,
63
+ dependency_cache: DependencyCache | None = None,
64
+ request: Request | None = None,
65
+ ) -> AsyncIterator[InjectScope]:
66
+ if dependency_cache is None:
67
+ dependency_cache = {}
68
+
69
+ async with AsyncExitStack() as stack:
70
+ request = request or _dummy_request(
71
+ extra_scope={
72
+ "fastapi_inner_astack": stack,
73
+ "fastapi_function_astack": stack,
74
+ },
75
+ )
76
+
77
+ scope = InjectScope(dependency_cache, request)
78
+ token = _inject_scope.set(scope)
79
+
80
+ try:
81
+ yield scope
82
+ finally:
83
+ _inject_scope.reset(token)
84
+
85
+
86
+ def current_inject_scope() -> InjectScope | None:
87
+ return _inject_scope.get()
88
+
89
+
90
+ @asynccontextmanager
91
+ async def inside_inject_scope(
92
+ *,
93
+ new_scope: bool = False,
94
+ ) -> AsyncIterator[InjectScope]:
95
+ scope = current_inject_scope()
96
+
97
+ async with AsyncExitStack() as stack:
98
+ if scope is None or new_scope:
99
+ scope = await stack.enter_async_context(push_inject_scope())
100
+
101
+ yield scope
102
+
103
+
104
+ __all__ = [
105
+ "InjectScope",
106
+ "current_inject_scope",
107
+ "inside_inject_scope",
108
+ ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastapi-injected"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Yet another library to reuse fastapi dependency injection"
5
5
  authors = [
6
6
  { name = "Yurii Karabas", email = "1998uriyyo@gmail.com" },
@@ -30,6 +30,7 @@ dev = [
30
30
  "ty>=0.0.62",
31
31
  "pre-commit>=4.6.1",
32
32
  "pydantic-ai-slim>=2.15.0",
33
+ "httpx>=0.28.1",
33
34
  ]
34
35
 
35
36
  [project.urls]
@@ -0,0 +1,35 @@
1
+ from fastapi import Depends, FastAPI, status
2
+ from fastapi.testclient import TestClient
3
+
4
+ from fastapi_injected import Dep, Injected, init_inject_scope, inject, resolve
5
+
6
+ from .deps import Container
7
+
8
+ app = FastAPI(
9
+ dependencies=[
10
+ Depends(init_inject_scope),
11
+ ],
12
+ )
13
+
14
+ client = TestClient(app)
15
+
16
+
17
+ @inject
18
+ async def _func(
19
+ *,
20
+ container: Dep[Container] = Injected,
21
+ ) -> Container:
22
+ return container
23
+
24
+
25
+ @app.get("/")
26
+ async def route(container: Dep[Container]) -> str:
27
+ assert await resolve(Container) is container
28
+ assert await _func() is container
29
+
30
+ return ""
31
+
32
+
33
+ def test_cache_is_working() -> None:
34
+ result = client.get("/")
35
+ assert result.status_code == status.HTTP_200_OK
@@ -2,7 +2,7 @@ from dataclasses import dataclass
2
2
 
3
3
  import pytest
4
4
 
5
- from fastapi_injected import Dep, push_inject_scope, solve
5
+ from fastapi_injected import Dep, push_inject_scope, resolve
6
6
 
7
7
  pytestmark = pytest.mark.asyncio
8
8
 
@@ -17,16 +17,16 @@ class Bar:
17
17
  foo: Dep[Foo]
18
18
 
19
19
 
20
- async def test_solve():
21
- bar = await solve(Bar)
20
+ async def test_resolve():
21
+ bar = await resolve(Bar)
22
22
 
23
23
  assert isinstance(bar, Bar)
24
24
  assert isinstance(bar.foo, Foo)
25
25
 
26
26
 
27
- async def test_solve_reuse_cache():
27
+ async def test_resolve_reuse_cache():
28
28
  async with push_inject_scope():
29
- b1 = await solve(Bar)
30
- b2 = await solve(Bar)
29
+ b1 = await resolve(Bar)
30
+ b2 = await resolve(Bar)
31
31
 
32
32
  assert b1 is b2
@@ -1,6 +1,6 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
- from fastapi_injected import Dep, DepFactory, Injected, inject, solve
3
+ from fastapi_injected import Dep, DepFactory, Injected, inject, resolve
4
4
 
5
5
  from .deps import Child, Container, ContextState, ctx_dep
6
6
 
@@ -22,8 +22,8 @@ async def func(
22
22
  return a + 10
23
23
 
24
24
 
25
- async def solve_test() -> None:
26
- container = await solve(Container)
25
+ async def resolve_test() -> None:
26
+ container = await resolve(Container)
27
27
 
28
28
  static_assert(is_equivalent_to(TypeOf[container], Container))
29
29
 
@@ -160,7 +160,7 @@ wheels = [
160
160
 
161
161
  [[package]]
162
162
  name = "fastapi-injected"
163
- version = "0.1.2"
163
+ version = "0.1.3"
164
164
  source = { editable = "." }
165
165
  dependencies = [
166
166
  { name = "fastapi" },
@@ -168,6 +168,7 @@ dependencies = [
168
168
 
169
169
  [package.dev-dependencies]
170
170
  dev = [
171
+ { name = "httpx" },
171
172
  { name = "pre-commit" },
172
173
  { name = "pydantic-ai-slim" },
173
174
  { name = "pytest" },
@@ -182,6 +183,7 @@ requires-dist = [{ name = "fastapi", specifier = ">=0.139.2" }]
182
183
 
183
184
  [package.metadata.requires-dev]
184
185
  dev = [
186
+ { name = "httpx", specifier = ">=0.28.1" },
185
187
  { name = "pre-commit", specifier = ">=4.6.1" },
186
188
  { name = "pydantic-ai-slim", specifier = ">=2.15.0" },
187
189
  { name = "pytest", specifier = ">=9.1.1" },
@@ -1,75 +0,0 @@
1
- from collections.abc import AsyncIterator
2
- from contextlib import (
3
- AbstractAsyncContextManager,
4
- AsyncExitStack,
5
- asynccontextmanager,
6
- nullcontext,
7
- )
8
- from contextvars import ContextVar
9
- from dataclasses import dataclass
10
- from typing import Any
11
-
12
- from .types import DependencyCache
13
-
14
-
15
- @dataclass
16
- class InjectScope:
17
- dependency_cache: DependencyCache
18
- func_astack: AsyncExitStack
19
- request_astack: AsyncExitStack
20
-
21
-
22
- _inject_scope: ContextVar[InjectScope | None] = ContextVar(
23
- "_inject_scope",
24
- default=None,
25
- )
26
-
27
-
28
- @asynccontextmanager
29
- async def push_inject_scope(
30
- dependency_cache: DependencyCache | None = None,
31
- ) -> AsyncIterator[InjectScope]:
32
- async with AsyncExitStack() as stack:
33
- scope = InjectScope(
34
- dependency_cache or {},
35
- stack,
36
- stack,
37
- )
38
- token = _inject_scope.set(scope)
39
-
40
- try:
41
- yield scope
42
- finally:
43
- _inject_scope.reset(token)
44
-
45
-
46
- def current_inject_scope() -> InjectScope | None:
47
- return _inject_scope.get()
48
-
49
-
50
- @asynccontextmanager
51
- async def inside_inject_scope(
52
- *,
53
- new_scope: bool = False,
54
- ) -> AsyncIterator[InjectScope]:
55
- scope = current_inject_scope()
56
-
57
- _ctx: AbstractAsyncContextManager[Any]
58
-
59
- if scope is None or new_scope:
60
- stack = AsyncExitStack()
61
- scope = await stack.enter_async_context(push_inject_scope())
62
-
63
- _ctx = stack
64
- else:
65
- _ctx = nullcontext()
66
-
67
- async with _ctx:
68
- yield scope
69
-
70
-
71
- __all__ = [
72
- "InjectScope",
73
- "current_inject_scope",
74
- "inside_inject_scope",
75
- ]